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:
parent
4acecffcd6
commit
de4f0167ef
4 changed files with 145 additions and 10 deletions
|
|
@ -4,6 +4,10 @@ using System.Numerics;
|
|||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Which edges of a window a resize-drag is affecting (corners combine two).</summary>
|
||||
[System.Flags]
|
||||
public enum ResizeEdges { None = 0, Left = 1, Right = 2, Top = 4, Bottom = 8 }
|
||||
|
||||
/// <summary>
|
||||
/// Top-level UI container. Implements the retail "Device" responsibilities
|
||||
/// (mouse cursor tracking, keyboard focus, modal overlay, mouse capture,
|
||||
|
|
@ -68,6 +72,11 @@ public sealed class UiRoot : UiElement
|
|||
private bool _dragCandidate;
|
||||
private UiElement? _windowDragTarget;
|
||||
private int _windowDragOffX, _windowDragOffY;
|
||||
private UiElement? _resizeTarget;
|
||||
private ResizeEdges _resizeEdges;
|
||||
private float _resizeStartX, _resizeStartY, _resizeStartW, _resizeStartH;
|
||||
private int _resizeMouseX, _resizeMouseY;
|
||||
private const int ResizeGrip = 5; // px proximity to an edge to start a resize
|
||||
private const int DragDistanceThreshold = 3; // pixels, retail-observed
|
||||
|
||||
// Hover / tooltip tracking.
|
||||
|
|
@ -133,6 +142,18 @@ public sealed class UiRoot : UiElement
|
|||
MouseX = x;
|
||||
MouseY = y;
|
||||
|
||||
// Window resize takes precedence over move / drag-drop / hover.
|
||||
if (_resizeTarget is not null)
|
||||
{
|
||||
var (nx, ny, nw, nh) = ResizeRect(
|
||||
_resizeStartX, _resizeStartY, _resizeStartW, _resizeStartH,
|
||||
_resizeEdges, x - _resizeMouseX, y - _resizeMouseY,
|
||||
_resizeTarget.MinWidth, _resizeTarget.MinHeight);
|
||||
_resizeTarget.Left = nx; _resizeTarget.Top = ny;
|
||||
_resizeTarget.Width = nw; _resizeTarget.Height = nh;
|
||||
return;
|
||||
}
|
||||
|
||||
// Window-move drag takes precedence over drag-drop / hover / fall-through.
|
||||
if (_windowDragTarget is not null)
|
||||
{
|
||||
|
|
@ -188,15 +209,30 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
SetCapture(target);
|
||||
|
||||
// Window-move: if the target or an ancestor is Draggable, a left-drag
|
||||
// repositions that window instead of starting a drag-drop.
|
||||
var draggable = FindDraggable(target);
|
||||
if (btn == UiMouseButton.Left && draggable is not null)
|
||||
// Window resize / move: find the window (Draggable or Resizable ancestor).
|
||||
// A left-drag starting near an edge resizes; interior drag repositions;
|
||||
// otherwise it's a normal drag-drop candidate.
|
||||
var window = FindWindow(target);
|
||||
if (btn == UiMouseButton.Left && window is not null)
|
||||
{
|
||||
_windowDragTarget = draggable;
|
||||
_windowDragOffX = x - (int)draggable.Left;
|
||||
_windowDragOffY = y - (int)draggable.Top;
|
||||
_dragCandidate = false;
|
||||
var edges = window.Resizable ? HitEdges(window, x, y, ResizeGrip) : ResizeEdges.None;
|
||||
if (edges != ResizeEdges.None)
|
||||
{
|
||||
_resizeTarget = window;
|
||||
_resizeEdges = edges;
|
||||
_resizeStartX = window.Left; _resizeStartY = window.Top;
|
||||
_resizeStartW = window.Width; _resizeStartH = window.Height;
|
||||
_resizeMouseX = x; _resizeMouseY = y;
|
||||
_dragCandidate = false;
|
||||
}
|
||||
else if (window.Draggable)
|
||||
{
|
||||
_windowDragTarget = window;
|
||||
_windowDragOffX = x - (int)window.Left;
|
||||
_windowDragOffY = y - (int)window.Top;
|
||||
_dragCandidate = false;
|
||||
}
|
||||
else { _dragCandidate = true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -221,6 +257,13 @@ public sealed class UiRoot : UiElement
|
|||
MouseX = x; MouseY = y;
|
||||
UpdateButtonFlag(btn, down: false);
|
||||
|
||||
if (_resizeTarget is not null)
|
||||
{
|
||||
_resizeTarget = null;
|
||||
ReleaseCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_windowDragTarget is not null)
|
||||
{
|
||||
_windowDragTarget = null;
|
||||
|
|
@ -477,16 +520,46 @@ public sealed class UiRoot : UiElement
|
|||
return (null, 0, 0);
|
||||
}
|
||||
|
||||
private static UiElement? FindDraggable(UiElement? e)
|
||||
private static UiElement? FindWindow(UiElement? e)
|
||||
{
|
||||
while (e is not null)
|
||||
{
|
||||
if (e.Draggable) return e;
|
||||
if (e.Draggable || e.Resizable) return e;
|
||||
e = e.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Which edges of <paramref name="w"/>'s screen rect the point
|
||||
/// (<paramref name="x"/>,<paramref name="y"/>) is within <paramref name="grip"/> px of.
|
||||
/// None if the point is outside the grip-expanded box entirely.</summary>
|
||||
internal static ResizeEdges HitEdges(UiElement w, int x, int y, int grip)
|
||||
{
|
||||
float l = w.Left, t = w.Top, r = w.Left + w.Width, b = w.Top + w.Height;
|
||||
if (x < l - grip || x > r + grip || y < t - grip || y > b + grip) return ResizeEdges.None;
|
||||
var e = ResizeEdges.None;
|
||||
if (System.Math.Abs(x - l) <= grip) e |= ResizeEdges.Left;
|
||||
if (System.Math.Abs(x - r) <= grip) e |= ResizeEdges.Right;
|
||||
if (System.Math.Abs(y - t) <= grip) e |= ResizeEdges.Top;
|
||||
if (System.Math.Abs(y - b) <= grip) e |= ResizeEdges.Bottom;
|
||||
return e;
|
||||
}
|
||||
|
||||
/// <summary>Compute a resized rect from a start rect + drag delta + which edges,
|
||||
/// clamping to (<paramref name="minW"/>,<paramref name="minH"/>). Left/Top edges
|
||||
/// move the origin so the opposite edge stays put.</summary>
|
||||
public static (float x, float y, float w, float h) ResizeRect(
|
||||
float startX, float startY, float startW, float startH,
|
||||
ResizeEdges edges, float dx, float dy, float minW, float minH)
|
||||
{
|
||||
float x = startX, y = startY, w = startW, h = startH;
|
||||
if ((edges & ResizeEdges.Right) != 0) w = System.Math.Max(minW, startW + dx);
|
||||
if ((edges & ResizeEdges.Bottom) != 0) h = System.Math.Max(minH, startH + dy);
|
||||
if ((edges & ResizeEdges.Left) != 0) { float nw = System.Math.Max(minW, startW - dx); x = startX + (startW - nw); w = nw; }
|
||||
if ((edges & ResizeEdges.Top) != 0) { float nh = System.Math.Max(minH, startH - dy); y = startY + (startH - nh); h = nh; }
|
||||
return (x, y, w, h);
|
||||
}
|
||||
|
||||
private static bool ContainsAbsolute(UiElement e, int x, int y)
|
||||
{
|
||||
var sp = e.ScreenPosition;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue