feat(D.2b): window resize (UiRoot edge-grip resize-drag mode)

Add parallel resize mode to the UiRoot retained-mode input state machine.
A left-drag starting within ResizeGrip=5px of a Resizable window's edge or
corner resizes it (min-size clamped); interior drags on a Draggable window
still reposition it.

Changes:
- UiElement: Resizable, MinWidth, MinHeight properties
- UiRoot: ResizeEdges flags enum; _resizeTarget state fields; FindWindow
  (replaces FindDraggable, matches Draggable||Resizable); HitEdges (static,
  internal, testable); ResizeRect (static, public, testable); OnMouseDown
  checks edge-grip before move; OnMouseMove resize branch precedes move;
  OnMouseUp clears _resizeTarget
- UiNineSlicePanel: Resizable = true (retail windows are resizable)
- UiRootInputTests: 4 new tests — ResizeRect_RightBottom, ResizeRect_LeftTop
  (min-clamp + origin shift), HitEdges_DetectsCornerAndInteriorNone,
  EdgeDrag_ResizesPanel_InteriorDragMoves (full integration path)

Note on test coordinate: right-edge grab uses x=298 (2px inside the panel's
hit-test boundary) rather than x=300 (exactly at edge, misses OnHitTest's
strict `<` check). This is intentional — the grip zone extends inward from
the edge boundary, so a click 2px inside correctly lands in both the
hit-test rect AND the resize-grip zone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-14 18:27:57 +02:00
parent 4acecffcd6
commit de4f0167ef
4 changed files with 145 additions and 10 deletions

View file

@ -49,4 +49,57 @@ public class UiRootInputTests
Assert.Equal(10f, panel.Left);
Assert.Equal(10f, panel.Top);
}
[Fact]
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);
Assert.Equal(10f, x); Assert.Equal(20f, y);
Assert.Equal(130f, w); Assert.Equal(65f, h);
}
[Fact]
public void ResizeRect_LeftTop_MovesOriginAndClampsToMin()
{
// 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);
Assert.Equal(40f, w);
Assert.Equal(70f, x);
}
[Fact]
public void HitEdges_DetectsCornerAndInteriorNone()
{
var panel = new UiPanel { Left = 100, Top = 100, Width = 200, Height = 100 };
// bottom-right corner (300,200)
Assert.Equal(ResizeEdges.Right | ResizeEdges.Bottom, UiRoot.HitEdges(panel, 300, 200, 5));
// deep interior → no edges
Assert.Equal(ResizeEdges.None, UiRoot.HitEdges(panel, 200, 150, 5));
}
[Fact]
public void EdgeDrag_ResizesPanel_InteriorDragMoves()
{
var root = new UiRoot { Width = 800, Height = 600 };
var panel = new UiPanel { Left = 100, Top = 100, Width = 200, Height = 100,
Draggable = true, Resizable = true, MinWidth = 40, MinHeight = 40 };
root.AddChild(panel);
// grab just inside the right edge (x=298, within ResizeGrip=5 of x=300) and drag right → wider, same origin
root.OnMouseDown(UiMouseButton.Left, 298, 150);
root.OnMouseMove(338, 150);
Assert.Equal(240f, panel.Width);
Assert.Equal(100f, panel.Left);
root.OnMouseUp(UiMouseButton.Left, 338, 150);
// grab the interior and drag → moves
root.OnMouseDown(UiMouseButton.Left, 200, 150);
root.OnMouseMove(220, 170);
Assert.Equal(120f, panel.Left);
Assert.Equal(120f, panel.Top);
root.OnMouseUp(UiMouseButton.Left, 220, 170);
}
}