1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

PopupMenu: Fix some issues with positioning of initially-visible items

The old implementation had a few problems:

- For a target area near but not touching the bottom of the parent area,
  and a long menu with an initially-visible item halfway through the
  list, the initially-visible item would sometimes be obscured by the
  scroller area. The new implementation adjusts the size of the menu to
  ensure that there's enough additional room for the scroller areas.
- For a target area at the very top or very bottom of the target area,
  the menu would be positioned inconsistently; sometimes it would
  overlap with the target area, but other times it would be positioned
  with a gap separating the menu and target area. In the new
  implementation, if there's not enough room for scrollers to be
  positioned above/below the target area, the menu will always be
  positioned so that it touches but does not overlap the target area.
- The initially-selected item would normally be
  positioned as close as possible to the target area, but this wasn't
  always applied consistently for long menus, and the menu would
  sometimes only be scrolled enough to make the item visible at the end
  of the menu furthest from the target area. In the new implementation,
  the initially-visible item will always be positioned on top of the
  target area, or adjacent to it, depending on the available space.
This commit is contained in:
reuk 2025-08-19 11:56:07 +01:00
parent d929adc9e8
commit fdaf71b50f
No known key found for this signature in database

View file

@ -439,8 +439,7 @@ struct MenuWindow final : public Component
return targetArea.getTopLeft();
});
const auto y = targetPosition.getY() - windowPos.getY();
ensureItemComponentIsVisible (**iter, isPositiveAndBelow (y, windowPos.getHeight()) ? std::optional (y) : std::nullopt);
ensureItemComponentIsVisible (**iter, targetPosition.getY() - windowPos.getY());
}
}
@ -1095,7 +1094,7 @@ struct MenuWindow final : public Component
{
const auto parentArea = getParentArea (windowPos.getPosition(), options.getParentComponent()) / scaleFactor;
if (const auto posAndOffset = computeInitialPosAndOffset (windowPos, parentArea, itemComp.getBounds(), wantedY))
if (const auto posAndOffset = computePosAndOffsetToEnsureVisibility (windowPos, parentArea, itemComp.getBounds(), contentHeight, wantedY))
{
std::tie (windowPos, childYOffset) = std::tie (posAndOffset->windowPos, posAndOffset->childYOffset);
updateYPositions();
@ -1108,40 +1107,49 @@ struct MenuWindow final : public Component
int childYOffset = 0;
};
static std::optional<PosAndOffset> computeInitialPosAndOffset (Rectangle<int> windowPos,
const Rectangle<int>& parentArea,
const Rectangle<int>& itemCompBounds,
std::optional<int> wantedY)
static std::optional<PosAndOffset> computePosAndOffsetToEnsureVisibility (Rectangle<int> windowPos,
const Rectangle<int>& parentArea,
const Rectangle<int>& itemCompBounds,
int contentHeight,
std::optional<int> wantedY)
{
if (windowPos.getHeight() <= PopupMenuSettings::scrollZone * 4
|| (! wantedY.has_value() && 0 <= itemCompBounds.getY() && itemCompBounds.getBottom() <= windowPos.getHeight()))
{
// If there's no specific wantedY, and the item component is already visible, then we don't
// need to make any adjustments.
if (! wantedY.has_value() && 0 <= itemCompBounds.getY() && itemCompBounds.getBottom() <= windowPos.getHeight())
return {};
const auto spaceNeededAboveItem = jmin (PopupMenuSettings::scrollZone, itemCompBounds.getY());
const auto spaceNeededBelowItem = jmin (PopupMenuSettings::scrollZone, contentHeight - itemCompBounds.getBottom());
const auto parentSpaceTargetY = windowPos.getY() + wantedY.value_or (itemCompBounds.getY());
// In order to display the visible item over the target area, we need to make sure that
// there's enough space above and below to hold the scroll areas if they're showing.
// Ideally, we want to avoid the case where the menu opens with the scroll area over the
// target area.
const auto isSpaceToOverlay = spaceNeededAboveItem <= (parentSpaceTargetY - parentArea.getY())
&& spaceNeededBelowItem <= (parentArea.getBottom() - (parentSpaceTargetY + itemCompBounds.getHeight()));
if (wantedY.has_value() && isSpaceToOverlay)
{
windowPos = windowPos.withY (parentSpaceTargetY - itemCompBounds.getY())
.withHeight (contentHeight)
.constrainedWithin (parentArea);
const auto menuSpaceTargetY = parentSpaceTargetY - windowPos.getY();
const auto offset = itemCompBounds.getY() - menuSpaceTargetY;
return PosAndOffset { windowPos, offset };
}
const auto adjustedY = std::invoke ([&]
{
if (wantedY.has_value())
return *wantedY;
// If there's not enough space to overlay the menu, then just use the provided menu
// bounds but try to position the visible item as close to the target area as possible,
// while avoiding the scroll areas.
const auto menuSpaceTargetY = jlimit (spaceNeededAboveItem,
windowPos.getHeight() - spaceNeededBelowItem - itemCompBounds.getHeight(),
parentSpaceTargetY - windowPos.getY());
const auto offset = itemCompBounds.getY() - menuSpaceTargetY;
return jlimit (PopupMenuSettings::scrollZone,
jmax (PopupMenuSettings::scrollZone,
windowPos.getHeight() - (PopupMenuSettings::scrollZone + itemCompBounds.getHeight())),
itemCompBounds.getY());
});
const auto deltaY = windowPos.getY() + adjustedY - itemCompBounds.getY();
windowPos.setSize (jmin (windowPos.getWidth(), parentArea.getWidth()),
jmin (windowPos.getHeight(), parentArea.getHeight()));
const auto newY = jlimit (parentArea.getY(),
parentArea.getBottom() - windowPos.getHeight(),
deltaY);
windowPos.setY (newY);
return PosAndOffset { windowPos, newY - deltaY };
return PosAndOffset { windowPos, offset };
}
void resizeToBestWindowPos()