feat(ui): D.2b — toolbar collapse-to-one-row (bottom-edge snap resize hides/shows row 2)

UiElement.MaxHeight + ResizableEdges mask; UiCollapsibleFrame snaps height to the nearer
of {collapsed,expanded} and toggles row-2 visibility; GameWindow computes the two heights
from the layout + top-anchors the content. Amends IA-17. UiNineSlicePanel unsealed to
allow subclassing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-20 18:33:35 +02:00
parent e58be3f030
commit f74e017509
8 changed files with 159 additions and 15 deletions

View file

@ -0,0 +1,63 @@
using AcDream.App.UI;
using Xunit;
namespace AcDream.App.Tests.UI;
public class UiCollapsibleFrameTests
{
private static UiCollapsibleFrame MakeFrame(out UiPanel row2a, out UiPanel row2b)
{
var f = new UiCollapsibleFrame(_ => (1u, 1, 1))
{
CollapsedHeight = 96f,
ExpandedHeight = 128f,
};
row2a = new UiPanel(); row2b = new UiPanel();
f.SecondRow = new UiElement[] { row2a, row2b };
return f;
}
[Fact]
public void Tick_belowMidpoint_snapsCollapsed_hidesSecondRow()
{
var f = MakeFrame(out var a, out var b);
f.Height = 100f; // nearer the collapsed stop (midpoint 112)
f.TickForTest(0.016);
Assert.Equal(96f, f.Height);
Assert.False(a.Visible);
Assert.False(b.Visible);
Assert.False(f.IsExpanded);
}
[Fact]
public void Tick_aboveMidpoint_snapsExpanded_showsSecondRow()
{
var f = MakeFrame(out var a, out var b);
f.Height = 120f; // nearer the expanded stop
f.TickForTest(0.016);
Assert.Equal(128f, f.Height);
Assert.True(a.Visible);
Assert.True(b.Visible);
Assert.True(f.IsExpanded);
}
[Fact]
public void Tick_notConfigured_isNoOp()
{
var f = new UiCollapsibleFrame(_ => (1u, 1, 1)); // Collapsed==Expanded==0
f.Height = 50f;
f.TickForTest(0.016);
Assert.Equal(50f, f.Height); // unchanged, no divide/no forced height
}
[Fact]
public void HitEdges_respectsResizableEdgesMask_bottomOnly()
{
var panel = new UiPanel { Left = 100, Top = 100, Width = 200, Height = 100,
Resizable = true, ResizableEdges = ResizeEdges.Bottom };
// bottom edge (y=200) → Bottom only
Assert.Equal(ResizeEdges.Bottom, UiRoot.HitEdges(panel, 200, 200, 5));
// top edge (y=100) → masked out → None
Assert.Equal(ResizeEdges.None, UiRoot.HitEdges(panel, 200, 100, 5));
}
}

View file

@ -157,7 +157,7 @@ public class UiRootInputTests
public void ResizeRect_RightBottom_GrowsSizeOnly()
{
var (x, y, w, h) = UiRoot.ResizeRect(10, 20, 100, 50,
ResizeEdges.Right | ResizeEdges.Bottom, dx: 30, dy: 15, minW: 40, minH: 40);
ResizeEdges.Right | ResizeEdges.Bottom, dx: 30, dy: 15, minW: 40, minH: 40, maxW: float.MaxValue, maxH: float.MaxValue);
Assert.Equal(10f, x); Assert.Equal(20f, y);
Assert.Equal(130f, w); Assert.Equal(65f, h);
}
@ -168,7 +168,7 @@ public class UiRootInputTests
// Drag left edge right by 80 on a 100-wide / min-40 window: width clamps to 40,
// origin shifts so the RIGHT edge (110) stays put → x = 70.
var (x, _, w, _) = UiRoot.ResizeRect(10, 20, 100, 50,
ResizeEdges.Left, dx: 80, dy: 0, minW: 40, minH: 40);
ResizeEdges.Left, dx: 80, dy: 0, minW: 40, minH: 40, maxW: float.MaxValue, maxH: float.MaxValue);
Assert.Equal(40f, w);
Assert.Equal(70f, x);
}