Skip to content

Commit e137f9b

Browse files
committed
Merge commit '0ec7e486b6e2315b12df663759660828d63728ae'
2 parents 13600f6 + 0ec7e48 commit e137f9b

2 files changed

Lines changed: 141 additions & 19 deletions

File tree

Gui/Menu/PopupWidget.cs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -380,29 +380,35 @@ private void RecalculatePosition(object sender, EventArgs e)
380380
// we only check for the scroll bar one time (the first time we open)
381381
if (checkIfNeedScrollBar)
382382
{
383-
var minimumOpenHeight = 50 * GuiWidget.DeviceScale;
384-
385-
// If the bottom of the popup is below the bottom of the screen
386-
if (direction == Direction.Down)
383+
// Opening Down puts the popup between the bottom of the anchor and the bottom of the window,
384+
// opening Up puts it between the top of the anchor and the top of the window. Measure both,
385+
// then prefer the requested direction, fall back to the other one, and only squeeze in a
386+
// scroll bar when the popup fits in neither. windowToAddTo is the window the popup was
387+
// actually added to (the outermost SystemWindow), which is the space these screen space
388+
// coordinates are expressed in - a nearer SystemWindow ancestor would give the wrong height.
389+
var spaceBelow = bottomLeftScreenSpace.Y;
390+
var spaceAbove = windowToAddTo.Height - (bottomLeftScreenSpace.Y + widgetRelativeTo.Height);
391+
var neededHeight = popupWidget.LocalBounds.Height;
392+
393+
var preferredSpace = direction == Direction.Down ? spaceBelow : spaceAbove;
394+
var oppositeSpace = direction == Direction.Down ? spaceAbove : spaceBelow;
395+
396+
if (neededHeight > preferredSpace)
387397
{
388-
if (bottomLeftScreenSpace.Y - popupWidget.LocalBounds.Height < 0)
398+
if (neededHeight <= oppositeSpace)
389399
{
390-
if (bottomLeftScreenSpace.Y <= minimumOpenHeight)
391-
{
392-
direction = Direction.Up;
393-
}
394-
else
395-
{
396-
popupWidget.MakeMenuHaveScroll(bottomLeftScreenSpace.Y - 5);
397-
}
400+
direction = direction == Direction.Down ? Direction.Up : Direction.Down;
398401
}
399-
}
400-
else
401-
{
402-
SystemWindow windowToAddTo = widgetRelativeTo.Parents<SystemWindow>().FirstOrDefault();
403-
if (bottomLeftScreenSpace.Y + popupWidget.LocalBounds.Height > windowToAddTo.Height)
402+
else
404403
{
405-
popupWidget.MakeMenuHaveScroll(bottomLeftScreenSpace.Y - 5);
404+
// It fits nowhere, so open toward whichever side has more room (keeping the
405+
// preferred direction on a tie) and scroll within that space
406+
if (oppositeSpace > preferredSpace)
407+
{
408+
direction = direction == Direction.Down ? Direction.Up : Direction.Down;
409+
}
410+
411+
popupWidget.MakeMenuHaveScroll(Math.Max(preferredSpace, oppositeSpace) - 5);
406412
}
407413
}
408414

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
Copyright (c) 2026, Lars Brubaker
3+
All rights reserved.
4+
*/
5+
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using MatterHackers.GuiAutomation;
9+
using MatterHackers.VectorMath;
10+
using TUnit.Assertions;
11+
using TUnit.Core;
12+
13+
namespace MatterHackers.Agg.UI.Tests
14+
{
15+
/// <summary>
16+
/// A drop down opened near the bottom of the window used to stay open downward and grow a scroll
17+
/// bar as long as there were more than 50 pixels below it, even when the whole list would have fit
18+
/// above the anchor. These tests pin the symmetric rule: use the preferred direction if it fits,
19+
/// otherwise the opposite direction if it fits, otherwise the roomier side with a scroll bar.
20+
/// </summary>
21+
[NotInParallel(nameof(AutomationRunner.ShowWindowAndExecuteTests))]
22+
public class PopupDirectionFlipTests
23+
{
24+
private const string PopupName = "_OpenMenuContents";
25+
private const string ContentName = "_topToBottom";
26+
27+
[Test]
28+
public async Task FitsAboveButNotBelowOpensUpWithoutScroll()
29+
{
30+
var (systemWindow, dropDown) = OpenDropDown(itemCount: 2, itemHeight: 50, anchorBottom: 60, direction: Direction.Down);
31+
32+
var popup = FindPopup(systemWindow);
33+
var content = FindContent(popup);
34+
35+
// It did not fit below (60 of space for a ~100 tall list) but fits easily above
36+
await Assert.That(popup.Height).IsEqualTo(content.Height).Within(0.001);
37+
await Assert.That(popup.Position.Y).IsEqualTo(dropDown.Position.Y + dropDown.Height).Within(0.001);
38+
}
39+
40+
[Test]
41+
public async Task FitsBelowOpensDown()
42+
{
43+
var (systemWindow, dropDown) = OpenDropDown(itemCount: 2, itemHeight: 50, anchorBottom: 200, direction: Direction.Down);
44+
45+
var popup = FindPopup(systemWindow);
46+
var content = FindContent(popup);
47+
48+
await Assert.That(popup.Height).IsEqualTo(content.Height).Within(0.001);
49+
50+
// Opening down puts the top of the popup at the bottom of the anchor
51+
await Assert.That(popup.Position.Y + popup.Height).IsEqualTo(dropDown.Position.Y).Within(0.001);
52+
}
53+
54+
[Test]
55+
public async Task FitsNeitherSideOpensTowardTheLargerSideWithScroll()
56+
{
57+
// 5 x 50 = ~250 tall in a 300 tall window - 140 below the anchor, ~134 above it
58+
var (systemWindow, dropDown) = OpenDropDown(itemCount: 5, itemHeight: 50, anchorBottom: 140, direction: Direction.Down);
59+
60+
var popup = FindPopup(systemWindow);
61+
var content = FindContent(popup);
62+
63+
await Assert.That(popup.Height).IsLessThan(content.Height);
64+
await Assert.That(popup.Height).IsEqualTo(dropDown.Position.Y - 5).Within(0.001);
65+
66+
// Below is the roomier side, so it stays open downward
67+
await Assert.That(popup.Position.Y + popup.Height).IsEqualTo(dropDown.Position.Y).Within(0.001);
68+
}
69+
70+
[Test]
71+
public async Task UpPreferredButOnlyFitsBelowFlipsDown()
72+
{
73+
var (systemWindow, dropDown) = OpenDropDown(itemCount: 2, itemHeight: 50, anchorBottom: 210, direction: Direction.Up);
74+
75+
var popup = FindPopup(systemWindow);
76+
var content = FindContent(popup);
77+
78+
await Assert.That(popup.Height).IsEqualTo(content.Height).Within(0.001);
79+
await Assert.That(popup.Position.Y + popup.Height).IsEqualTo(dropDown.Position.Y).Within(0.001);
80+
await Assert.That(popup.Position.Y).IsGreaterThanOrEqualTo(0);
81+
}
82+
83+
private static (SystemWindow systemWindow, DropDownList dropDown) OpenDropDown(int itemCount, double itemHeight, double anchorBottom, Direction direction)
84+
{
85+
var systemWindow = new SystemWindow(400, 300);
86+
87+
var dropDown = new DropDownList("no selection", Color.Black, direction)
88+
{
89+
Name = "dropDown",
90+
};
91+
92+
for (int i = 0; i < itemCount; i++)
93+
{
94+
var item = dropDown.AddItem($"Item {i}");
95+
item.MinimumSize = new Vector2(150, itemHeight);
96+
}
97+
98+
systemWindow.AddChild(dropDown);
99+
dropDown.Position = new Vector2(10, anchorBottom);
100+
101+
dropDown.InvokeClick();
102+
103+
return (systemWindow, dropDown);
104+
}
105+
106+
private static GuiWidget FindPopup(SystemWindow systemWindow)
107+
{
108+
return systemWindow.Descendants<GuiWidget>().First(w => w.Name == PopupName);
109+
}
110+
111+
private static GuiWidget FindContent(GuiWidget popup)
112+
{
113+
return popup.Descendants<GuiWidget>().First(w => w.Name == ContentName);
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)