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
|
|
@ -140,7 +140,9 @@ public sealed class CursorFeedbackController
|
|||
CombatMode: _combatModeProvider());
|
||||
|
||||
var kind = ResolveKind(snapshot);
|
||||
Current = new CursorFeedback(kind, ResolveCursor(root.Captured, hover), ResolveGlobalKind(snapshot));
|
||||
UiCursorMedia authoredCursor = ResolveCursor(root.Captured, hover);
|
||||
UiCursorMedia cursor = ResolveEffectiveCursor(kind, snapshot, authoredCursor);
|
||||
Current = new CursorFeedback(kind, cursor, ResolveGlobalKind(snapshot));
|
||||
return Current;
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +153,13 @@ public sealed class CursorFeedbackController
|
|||
}
|
||||
|
||||
public CursorFeedback Resolve(CursorFeedbackSnapshot snapshot)
|
||||
=> new(ResolveKind(snapshot), GlobalKind: ResolveGlobalKind(snapshot));
|
||||
{
|
||||
CursorFeedbackKind kind = ResolveKind(snapshot);
|
||||
return new(
|
||||
kind,
|
||||
ResolveEffectiveCursor(kind, snapshot, authoredCursor: default),
|
||||
ResolveGlobalKind(snapshot));
|
||||
}
|
||||
|
||||
private CursorFeedbackKind ResolveKind(CursorFeedbackSnapshot snapshot)
|
||||
{
|
||||
|
|
@ -310,6 +318,34 @@ public sealed class CursorFeedbackController
|
|||
return default;
|
||||
}
|
||||
|
||||
private static UiCursorMedia ResolveEffectiveCursor(
|
||||
CursorFeedbackKind kind,
|
||||
CursorFeedbackSnapshot snapshot,
|
||||
UiCursorMedia authoredCursor)
|
||||
{
|
||||
bool syntheticControlOwnsPointer = snapshot.ActiveResizeEdges != ResizeEdges.None
|
||||
|| snapshot.HoverResizeEdges != ResizeEdges.None
|
||||
|| snapshot.WindowMoveActive;
|
||||
|
||||
// Retail CheckCursor gives the hovered/captured Resizebar or captured
|
||||
// Dragbar first refusal. Our wrapper borders are geometry, not UIElements,
|
||||
// so a hovered synthetic resize edge or active synthetic control must take
|
||||
// that same precedence over the content under its five-pixel hit region.
|
||||
if (syntheticControlOwnsPointer
|
||||
&& RetailCursorCatalog.TryGetWindowControlCursor(kind, out UiCursorMedia capturedControl))
|
||||
return capturedControl;
|
||||
|
||||
// Ordinary imported elements retain their authored MD_Data_Cursor exactly.
|
||||
if (authoredCursor.IsValid)
|
||||
return authoredCursor;
|
||||
|
||||
// Synthetic wrapper edges and the IA-12 whole-window drag region have no
|
||||
// authored child, but use retail's exact control media and hotspot.
|
||||
return RetailCursorCatalog.TryGetWindowControlCursor(kind, out UiCursorMedia fallback)
|
||||
? fallback
|
||||
: default;
|
||||
}
|
||||
|
||||
private static UiItemSlot? FindHoveredItemSlot(UiElement? element)
|
||||
{
|
||||
while (element is not null)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public static class RetailWindowFrame
|
|||
public ResizeEdges ResizableEdges { get; init; } =
|
||||
ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Top | ResizeEdges.Bottom;
|
||||
public bool ConstrainDragToParent { get; init; }
|
||||
public bool ConstrainResizeToParent { get; init; }
|
||||
|
||||
public float Opacity { get; init; } = 1f;
|
||||
public bool Visible { get; init; } = true;
|
||||
|
|
@ -135,6 +136,7 @@ public static class RetailWindowFrame
|
|||
outerFrame.ResizeY = options.ResizeY;
|
||||
outerFrame.ResizableEdges = options.ResizableEdges;
|
||||
outerFrame.ConstrainDragToParent = options.ConstrainDragToParent;
|
||||
outerFrame.ConstrainResizeToParent = options.ConstrainResizeToParent;
|
||||
outerFrame.Opacity = Math.Clamp(options.Opacity, 0f, 1f);
|
||||
outerFrame.Visible = options.Visible;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Retail global cursor enum metadata used outside LayoutDesc widget states.</summary>
|
||||
/// <summary>
|
||||
/// Retail cursor metadata used outside ordinary LayoutDesc widget-state lookup.
|
||||
/// Global cursors resolve through enum table 6; synthetic retained-window control
|
||||
/// regions use the exact direct cursor media authored on retail Dragbar/Resizebar
|
||||
/// elements.
|
||||
/// </summary>
|
||||
internal static class RetailCursorCatalog
|
||||
{
|
||||
public const uint CursorEnumTable = 6;
|
||||
|
|
@ -32,6 +37,31 @@ internal static class RetailCursorCatalog
|
|||
|
||||
return spec.IsValid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct <c>MD_Data_Cursor</c> surfaces authored on retail Type-2 Dragbar and
|
||||
/// Type-9 Resizebar controls. The retained wrapper border has no LayoutDesc
|
||||
/// element of its own, so it uses the same media through this shared seam.
|
||||
/// </summary>
|
||||
public static bool TryGetWindowControlCursor(
|
||||
CursorFeedbackKind kind,
|
||||
out UiCursorMedia cursor)
|
||||
{
|
||||
// MediaMachine::Update_Cursor @ 0x00465A80 installs these element-local
|
||||
// cursor DIDs; UIElementManager::CheckCursor @ 0x0045ABF0 preserves the
|
||||
// captured Dragbar/Resizebar cursor for the complete operation.
|
||||
cursor = kind switch
|
||||
{
|
||||
CursorFeedbackKind.WindowMove => new UiCursorMedia(0x06006119u, 16, 16),
|
||||
CursorFeedbackKind.ResizeHorizontal => new UiCursorMedia(0x06006128u, 16, 16),
|
||||
CursorFeedbackKind.ResizeVertical => new UiCursorMedia(0x06005E66u, 16, 16),
|
||||
CursorFeedbackKind.ResizeDiagonalNwse => new UiCursorMedia(0x06006126u, 16, 16),
|
||||
CursorFeedbackKind.ResizeDiagonalNesw => new UiCursorMedia(0x06006127u, 16, 16),
|
||||
_ => default,
|
||||
};
|
||||
|
||||
return cursor.IsValid;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct RetailCursorSpec(uint EnumId, int HotspotX, int HotspotY)
|
||||
|
|
|
|||
|
|
@ -546,6 +546,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
MinWidth = 200f,
|
||||
ResizeX = true,
|
||||
ResizeY = true,
|
||||
// The imported 9px top Resizebar is deliberately removed by
|
||||
// ChatWindowController. Its replacement top chrome is the move
|
||||
// affordance; only the other three wrapper edges resize.
|
||||
ResizableEdges = ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Bottom,
|
||||
Opacity = 0.75f,
|
||||
Controller = controller,
|
||||
StateController = controller,
|
||||
|
|
@ -798,10 +802,12 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Left = 18f,
|
||||
Top = 18f,
|
||||
Visible = false,
|
||||
Resizable = false,
|
||||
ResizeX = false,
|
||||
ResizeY = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
ConstrainDragToParent = true,
|
||||
ConstrainResizeToParent = true,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
});
|
||||
|
|
@ -1147,10 +1153,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = 540f,
|
||||
Top = 18f,
|
||||
MaxHeight = 760f,
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
ConstrainResizeToParent = true,
|
||||
Visible = false,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
|
|
@ -1233,11 +1239,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Bottom,
|
||||
// The old 560px cap left a previously expanded window permanently
|
||||
// parked at its maximum. Let it grow to the available screen height.
|
||||
MaxHeight = Math.Max(
|
||||
root.Height + 2f * RetailChromeSprites.Border,
|
||||
Host.Root.Height - root.Top),
|
||||
ConstrainResizeToParent = true,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
});
|
||||
_panelUi.RegisterMainPanel(
|
||||
|
|
|
|||
|
|
@ -155,8 +155,15 @@ public sealed class RetailWindowManager : IDisposable
|
|||
var frame = handle.OuterFrame;
|
||||
if (!frame.ResizeX) width = frame.Width;
|
||||
if (!frame.ResizeY) height = frame.Height;
|
||||
width = Math.Clamp(width, frame.MinWidth, frame.MaxWidth);
|
||||
height = Math.Clamp(height, frame.MinHeight, frame.MaxHeight);
|
||||
float maxWidth = frame.MaxWidth;
|
||||
float maxHeight = frame.MaxHeight;
|
||||
if (frame.ConstrainResizeToParent && frame.Parent is { } parent)
|
||||
{
|
||||
maxWidth = MathF.Min(maxWidth, parent.Width - frame.Left);
|
||||
maxHeight = MathF.Min(maxHeight, parent.Height - frame.Top);
|
||||
}
|
||||
width = Math.Clamp(width, frame.MinWidth, MathF.Max(frame.MinWidth, maxWidth));
|
||||
height = Math.Clamp(height, frame.MinHeight, MathF.Max(frame.MinHeight, maxHeight));
|
||||
if (frame.Width == width && frame.Height == height) return true;
|
||||
frame.Width = width;
|
||||
frame.Height = height;
|
||||
|
|
|
|||
|
|
@ -188,6 +188,13 @@ public abstract class UiElement
|
|||
/// <c>gmRadarUI::MoveTo</c> enables this; most windows retain the toolkit default.</summary>
|
||||
public bool ConstrainDragToParent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Clamp resize growth to the current parent extent. This is distinct from
|
||||
/// <see cref="ConstrainDragToParent"/>: retail's shared main-panel host may be
|
||||
/// moved freely, but its bottom resizebar grows only to the desktop edge.
|
||||
/// </summary>
|
||||
public bool ConstrainResizeToParent { get; set; }
|
||||
|
||||
/// <summary>If true, a left-drag starting near this element's edge/corner
|
||||
/// resizes it (window resize). Intended for top-level panels.</summary>
|
||||
public bool Resizable { get; set; }
|
||||
|
|
|
|||
|
|
@ -341,11 +341,31 @@ public sealed class UiRoot : UiElement
|
|||
// Window resize takes precedence over move / drag-drop / hover.
|
||||
if (_resizeTarget is not null)
|
||||
{
|
||||
float maxWidth = _resizeTarget.MaxWidth;
|
||||
float maxHeight = _resizeTarget.MaxHeight;
|
||||
if (_resizeTarget.ConstrainResizeToParent
|
||||
&& _resizeTarget.Parent is { } resizeParent)
|
||||
{
|
||||
// The opposite edge remains fixed during a resize. Limit the
|
||||
// dragged edge to its current parent, using the interaction's
|
||||
// start rect so the clamp remains stable throughout the drag.
|
||||
maxWidth = MathF.Min(
|
||||
maxWidth,
|
||||
(_resizeEdges & ResizeEdges.Left) != 0
|
||||
? _resizeStartX + _resizeStartW
|
||||
: resizeParent.Width - _resizeStartX);
|
||||
maxHeight = MathF.Min(
|
||||
maxHeight,
|
||||
(_resizeEdges & ResizeEdges.Top) != 0
|
||||
? _resizeStartY + _resizeStartH
|
||||
: resizeParent.Height - _resizeStartY);
|
||||
}
|
||||
var (nx, ny, nw, nh) = ResizeRect(
|
||||
_resizeStartX, _resizeStartY, _resizeStartW, _resizeStartH,
|
||||
_resizeEdges, x - _resizeMouseX, y - _resizeMouseY,
|
||||
_resizeTarget.MinWidth, _resizeTarget.MinHeight,
|
||||
_resizeTarget.MaxWidth, _resizeTarget.MaxHeight);
|
||||
MathF.Max(_resizeTarget.MinWidth, maxWidth),
|
||||
MathF.Max(_resizeTarget.MinHeight, maxHeight));
|
||||
_resizeTarget.Left = nx; _resizeTarget.Top = ny;
|
||||
_resizeTarget.Width = nw; _resizeTarget.Height = nh;
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue