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:
parent
3f3cfdac30
commit
06016014bc
16 changed files with 578 additions and 27 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ public sealed class RetailWindowFrameTests
|
|||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
ConstrainResizeToParent = true,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right,
|
||||
ContentClickThrough = true,
|
||||
});
|
||||
|
|
@ -101,6 +102,7 @@ public sealed class RetailWindowFrameTests
|
|||
Assert.False(frame.ResizeX);
|
||||
Assert.True(frame.ResizeY);
|
||||
Assert.Equal(ResizeEdges.Bottom, frame.ResizableEdges);
|
||||
Assert.True(frame.ConstrainResizeToParent);
|
||||
Assert.True(content.ClickThrough);
|
||||
Assert.False(content.Draggable);
|
||||
Assert.False(content.Resizable);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using SysEnv = System.Environment;
|
||||
|
||||
|
|
@ -8,6 +9,47 @@ namespace AcDream.App.Tests.UI;
|
|||
|
||||
public sealed class RetailCursorCatalogTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(CursorFeedbackKind.WindowMove, 0x06006119u)]
|
||||
[InlineData(CursorFeedbackKind.ResizeHorizontal, 0x06006128u)]
|
||||
[InlineData(CursorFeedbackKind.ResizeVertical, 0x06005E66u)]
|
||||
[InlineData(CursorFeedbackKind.ResizeDiagonalNwse, 0x06006126u)]
|
||||
[InlineData(CursorFeedbackKind.ResizeDiagonalNesw, 0x06006127u)]
|
||||
public void WindowControlCursorSpecs_matchRetailLayoutDescMedia(
|
||||
CursorFeedbackKind kind,
|
||||
uint expectedDid)
|
||||
{
|
||||
Assert.True(RetailCursorCatalog.TryGetWindowControlCursor(kind, out var cursor));
|
||||
Assert.Equal(new UiCursorMedia(expectedDid, 16, 16), cursor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowControlCursorSpecs_resolveGoldenDatSurfaces()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null)
|
||||
return;
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
CursorFeedbackKind[] kinds =
|
||||
[
|
||||
CursorFeedbackKind.WindowMove,
|
||||
CursorFeedbackKind.ResizeHorizontal,
|
||||
CursorFeedbackKind.ResizeVertical,
|
||||
CursorFeedbackKind.ResizeDiagonalNwse,
|
||||
CursorFeedbackKind.ResizeDiagonalNesw,
|
||||
];
|
||||
|
||||
foreach (CursorFeedbackKind kind in kinds)
|
||||
{
|
||||
Assert.True(RetailCursorCatalog.TryGetWindowControlCursor(kind, out var cursor));
|
||||
Assert.True(dats.Portal.TryGet<RenderSurface>(cursor.File, out var surface), kind.ToString());
|
||||
Assert.NotNull(surface);
|
||||
Assert.Equal(32, surface.Width);
|
||||
Assert.Equal(32, surface.Height);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(RetailGlobalCursorKind.Default, 0x01u, 0, 0)]
|
||||
[InlineData(RetailGlobalCursorKind.DefaultFound, 0x02u, 0, 0)]
|
||||
|
|
|
|||
|
|
@ -118,6 +118,30 @@ public sealed class RetailWindowManagerTests
|
|||
Assert.Equal(1, resized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProgrammaticResize_ConstrainedToParent_UsesCurrentWindowPosition()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var frame = new UiPanel
|
||||
{
|
||||
Left = 40,
|
||||
Top = 125,
|
||||
Width = 300,
|
||||
Height = 200,
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ConstrainResizeToParent = true,
|
||||
MinHeight = 100,
|
||||
};
|
||||
root.AddChild(frame);
|
||||
RetailWindowHandle handle = root.RegisterWindow("main-panel", frame);
|
||||
|
||||
Assert.True(handle.ResizeTo(frame.Width, 2_000f));
|
||||
|
||||
Assert.Equal(475f, frame.Height);
|
||||
Assert.Equal(root.Height, frame.Top + frame.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FocusAndCaptureTransitions_AreScopedToOwningHandle()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -349,6 +349,56 @@ public class UiRootInputTests
|
|||
root.OnMouseUp(UiMouseButton.Left, 338, 150);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BottomResize_ConstrainedToParent_ReachesCurrentScreenEdge()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var window = new UiPanel
|
||||
{
|
||||
Left = 100,
|
||||
Top = 80,
|
||||
Width = 300,
|
||||
Height = 200,
|
||||
Draggable = true,
|
||||
Resizable = true,
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
ConstrainResizeToParent = true,
|
||||
MinHeight = 100,
|
||||
};
|
||||
root.AddChild(window);
|
||||
|
||||
root.OnMouseDown(UiMouseButton.Left, 250, 279);
|
||||
root.OnMouseMove(250, 2_000);
|
||||
|
||||
Assert.Equal(520f, window.Height);
|
||||
Assert.Equal(root.Height, window.Top + window.Height);
|
||||
root.OnMouseUp(UiMouseButton.Left, 250, 2_000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TopWithoutTopResizeEdge_IsWindowMoveAffordance()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var window = new UiPanel
|
||||
{
|
||||
Left = 100,
|
||||
Top = 80,
|
||||
Width = 300,
|
||||
Height = 200,
|
||||
Draggable = true,
|
||||
Resizable = true,
|
||||
ResizableEdges = ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Bottom,
|
||||
};
|
||||
root.AddChild(window);
|
||||
|
||||
root.OnMouseMove(250, 80);
|
||||
|
||||
Assert.Equal(ResizeEdges.None, root.HoverResizeEdges);
|
||||
Assert.True(root.HoverWindowMove);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizeRect_RightBottom_GrowsSizeOnly()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue