fix(ui): port retail window control feedback

Map synthetic move and resize affordances to the exact DAT cursors, make chat top chrome movable, and replace stale primary-panel height caps with a dynamic screen-edge constraint. This keeps the retained wrapper adaptation aligned with retail Dragbar/Resizebar behavior.
This commit is contained in:
Erik 2026-07-17 09:40:08 +02:00
parent 3f3cfdac30
commit 06016014bc
16 changed files with 578 additions and 27 deletions

View file

@ -32,17 +32,25 @@ public sealed class CursorFeedbackControllerTests
}
[Theory]
[InlineData(ResizeEdges.Right, CursorFeedbackKind.ResizeHorizontal)]
[InlineData(ResizeEdges.Bottom, CursorFeedbackKind.ResizeVertical)]
[InlineData(ResizeEdges.Right | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNwse)]
[InlineData(ResizeEdges.Left | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNesw)]
public void ResizeEdges_chooseExpectedCursor(ResizeEdges edges, CursorFeedbackKind expected)
[InlineData(ResizeEdges.Left, CursorFeedbackKind.ResizeHorizontal, 0x06006128u)]
[InlineData(ResizeEdges.Right, CursorFeedbackKind.ResizeHorizontal, 0x06006128u)]
[InlineData(ResizeEdges.Top, CursorFeedbackKind.ResizeVertical, 0x06005E66u)]
[InlineData(ResizeEdges.Bottom, CursorFeedbackKind.ResizeVertical, 0x06005E66u)]
[InlineData(ResizeEdges.Left | ResizeEdges.Top, CursorFeedbackKind.ResizeDiagonalNwse, 0x06006126u)]
[InlineData(ResizeEdges.Right | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNwse, 0x06006126u)]
[InlineData(ResizeEdges.Left | ResizeEdges.Bottom, CursorFeedbackKind.ResizeDiagonalNesw, 0x06006127u)]
[InlineData(ResizeEdges.Right | ResizeEdges.Top, CursorFeedbackKind.ResizeDiagonalNesw, 0x06006127u)]
public void ResizeEdges_chooseExpectedRetailCursor(
ResizeEdges edges,
CursorFeedbackKind expected,
uint expectedDid)
{
var c = new CursorFeedbackController();
var feedback = c.Resolve(new CursorFeedbackSnapshot(HoverResizeEdges: edges));
Assert.Equal(expected, feedback.Kind);
Assert.Equal(new UiCursorMedia(expectedDid, 16, 16), feedback.Cursor);
}
[Fact]
@ -52,10 +60,93 @@ public sealed class CursorFeedbackControllerTests
Assert.Equal(CursorFeedbackKind.WindowMove,
c.Resolve(new CursorFeedbackSnapshot(HoverWindowMove: true)).Kind);
Assert.Equal(new UiCursorMedia(0x06006119u, 16, 16),
c.Resolve(new CursorFeedbackSnapshot(HoverWindowMove: true)).Cursor);
Assert.Equal(CursorFeedbackKind.Text,
c.Resolve(new CursorFeedbackSnapshot(HoverTextEdit: true)).Kind);
}
[Fact]
public void ActiveSyntheticResize_keepsControlCursorOverAuthoredCapturedElement()
{
var root = new UiRoot { Width = 400, Height = 300 };
var window = new UiPanel
{
Left = 50,
Top = 40,
Width = 150,
Height = 100,
Draggable = true,
Resizable = true,
};
var content = new UiPanel { Width = 150, Height = 100 };
var authored = new UiCursorMedia(0x06009999u, 3, 4);
content.SetStateCursors(new Dictionary<string, UiCursorMedia> { [""] = authored });
window.AddChild(content);
root.AddChild(window);
var controller = new CursorFeedbackController();
root.OnMouseMove(199, 90); // inside the five-pixel right-edge resizebar region
Assert.Equal(new UiCursorMedia(0x06006128u, 16, 16), controller.Update(root).Cursor);
root.OnMouseDown(UiMouseButton.Left, 199, 90);
root.OnMouseMove(100, 90); // leave the edge while resize capture remains active
CursorFeedback active = controller.Update(root);
Assert.Equal(CursorFeedbackKind.ResizeHorizontal, active.Kind);
Assert.Equal(new UiCursorMedia(0x06006128u, 16, 16), active.Cursor);
}
[Fact]
public void AuthoredWidgetCursor_winsUntilSyntheticWindowMoveStarts()
{
var root = new UiRoot { Width = 400, Height = 300 };
var window = new UiPanel
{
Left = 50,
Top = 40,
Width = 150,
Height = 100,
Draggable = true,
};
var dragRegion = new UiPanel { Width = 150, Height = 100 };
var authored = new UiCursorMedia(0x06008888u, 7, 8);
dragRegion.SetStateCursors(new Dictionary<string, UiCursorMedia> { [""] = authored });
window.AddChild(dragRegion);
root.AddChild(window);
var controller = new CursorFeedbackController();
root.OnMouseMove(100, 90);
Assert.Equal(authored, controller.Update(root).Cursor);
root.OnMouseDown(UiMouseButton.Left, 100, 90);
Assert.Equal(new UiCursorMedia(0x06006119u, 16, 16), controller.Update(root).Cursor);
root.OnMouseMove(300, 200);
Assert.Equal(new UiCursorMedia(0x06006119u, 16, 16), controller.Update(root).Cursor);
}
[Fact]
public void UiLock_suppressesSyntheticWindowControlCursors()
{
var root = new UiRoot { Width = 400, Height = 300, UiLocked = true };
root.AddChild(new UiPanel
{
Left = 50,
Top = 40,
Width = 150,
Height = 100,
Draggable = true,
Resizable = true,
});
root.OnMouseMove(199, 90);
CursorFeedback feedback = new CursorFeedbackController().Update(root);
Assert.Equal(CursorFeedbackKind.Default, feedback.Kind);
Assert.False(feedback.Cursor.IsValid);
}
[Fact]
public void TargetMode_reportsValidInvalidAndPendingTargets()
{