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

@ -78,6 +78,36 @@ Magic are children of one `gmPanelUI`. The switch changes child visibility;
there is no child move and no per-panel top-level position. Dragging the parent
therefore moves the one position subsequently used by every child.
## Resize follow-up
The production end-of-retail LayoutDesc supplies the missing geometry oracle:
```text
LayoutDesc 0x2100006E
gmPanelUI host 0x100005FE 310 x 372
content parent 0x10000180 300 x 362, anchored on all edges
Inventory child 0x1000018B 300 x 362, anchored on all edges
Character child 0x1000018E 300 x 362, anchored on all edges
Magic child 0x10000190 300 x 362, anchored on all edges
top-center 0x1000065C Type 2 Dragbar, cursor 0x06006119
bottom-center 0x10000660 Type 9 Resizebar, cursor 0x06005E66
```
Every primary child therefore participates in the shared host's vertical
resize; Magic is not a fixed-height exception. Named
`gmPanelUI::ResizeTo @ 0x004BC6E0` delegates directly to
`UIElement::ResizeTo @ 0x00463C30`, whose only size limits are effective DAT
properties `0x3C..0x3F`. There is no retail 760-pixel Character limit and no
maximum captured from the host's original screen position.
acdream retains independent wrappers under IA-12, so the faithful adaptation
is to give each primary wrapper the same bottom-only vertical resize policy,
leave its maximum unbounded by an authored substitute, and calculate the
available desktop extent from its current position during the resize. The
viewport remains the physical limit; an old static maximum must not become a
smaller limit after the shared panel moves.
## acdream port shape
acdream imports these child LayoutDesc roots independently, so each currently
@ -105,7 +135,9 @@ The controller owns this synchronization and subscribes to typed
`RetailWindowHandle.Moved` events. Toolbar commands, keyboard commands,
restore-previous effects, persistence restore, and direct window dragging all
therefore pass through the same owner. Panel-authored size/resize policy stays
with the active child; only the retail parent placement is shared.
with the active child, but the three primary wrappers apply the shared host's
same vertical resize contract; only placement is synchronized between the
independent frames.
ACE contains no client UI implementation, and the in-tree WorldBuilder
reference contains no `gmPanelUI` port, so neither supplies a competing

View file

@ -0,0 +1,137 @@
# Retail window-control cursor pseudocode
Date: 2026-07-17
## Question
Which cursors does the end-of-retail client show while hovering or operating
window dragbars and resizebars, and how are they selected?
## Named-retail mechanism
### Widget cursor ownership
```text
MediaMachine::Update_Cursor(owner, cursorMedia) // 0x00465A80
if cursorMedia.file is valid
owner.SetCursor(file, hotspotX, hotspotY)
else
owner.UnSetCursor()
UIElement::SetCursor(file, hotspotX, hotspotY) // 0x0045FF50
m_cursorDID = file
m_cursorHotX = hotspotX
m_cursorHotY = hotspotY
UIElementManager.CheckCursor()
UIElementManager::CheckCursor() // 0x0045ABF0
if mouseCapture != null and mouseCapture.HasCursor()
apply mouseCapture cursor
else if lastEntered != null and lastEntered.HasCursor()
apply lastEntered cursor
else
apply ClientUISystem default cursor
```
The important ownership rule is that the captured widget wins. A resize or
move cursor therefore remains visible after the pointer leaves the original
five-pixel control while the button is still held.
### Window controls
```text
UIElement_Resizebar::StartMouseResizing(point) // 0x0046B7E0
parent = GetParent()
edge = authored Left/Right/Top/Bottom attributes
m_mousePressed = true
parent.StartResizing(edge, point.x, point.y)
SetState(Pressed)
UIElement_Resizebar::StopMouseResizing() // 0x0046B8C0
SetState(Normal)
m_mousePressed = false
GetParent().StopResizing()
UIElement_Dragbar::StartMouseMoving(message) // 0x0046C760
if primary mouse button and parent is not already moving/resizing
m_mousePressed = true
parent.StartMovement(message.windowX, message.windowY)
SetState(Pressed)
UIElement_Dragbar::StopMouseMoving(message) // 0x0046C7C0
if pressed and primary mouse button
SetState(Normal)
m_mousePressed = false
GetParent().StopMovement()
```
Dragbar and Resizebar do not synthesize OS cursors in these methods. Their
LayoutDesc state media supplies `MD_Data_Cursor`; the generic
`MediaMachine::Update_Cursor` path above installs it on the element.
## Installed end-of-retail DAT oracle
The production LayoutDesc records and portal DAT resolve these direct cursor
surfaces, all with hotspot `(16, 16)` pixels:
| Intent | RenderSurface DID | Observed authored use |
|---|---:|---|
| Move window | `0x06006119` | Type-2 Dragbar controls |
| Resize vertically | `0x06005E66` | horizontal Type-9 Resizebar (main chat top edge and combat bottom edge) |
| Resize horizontally | `0x06006128` | vertical Type-9 Resizebar |
| Resize NW-SE | `0x06006126` | top-left and bottom-right Type-9 corners |
| Resize NE-SW | `0x06006127` | bottom-left / opposite diagonal Type-9 corner |
All five surfaces are 32×32 `PFID_A8R8G8B8` cursor images. Representative
LayoutDesc fixtures independently preserve the same media:
- main chat `0x21000006`, element `0x1000000F`: vertical cursor
`0x06005E66` at `(16,16)`;
- combat `0x21000073`, Dragbar `0x100006AE`: move cursor
`0x06006119` at `(16,16)`;
- combat corner/edge Resizebars: `0x06006126..0x06006128`.
`DatReaderWriter` supplies these cursor records through the existing
LayoutDesc media decode. WorldBuilder's backend exposes the same architectural
boundary (`SetCursor(cursorDataId, hotspot)`) but leaves it unimplemented; it
does not provide a competing behavior interpretation.
## acdream root cause and port
`UiRoot` already computes the active or hovered resize edges and window-move
ownership. `CursorFeedbackController` already translates those states into
`WindowMove`, `ResizeHorizontal`, `ResizeVertical`, and the two diagonal
semantic kinds. However, `RetailCursorManager` renders only a valid
`UiCursorMedia` override or the `ClientUISystem` global cursor. The semantic
window-control kind was never converted to media, so synthetic wrapper borders
and whole-window drag regions retained the normal pointer.
Port shape:
```text
ResolveWindowControlCursor(kind):
WindowMove -> 0x06006119 @ (16,16)
ResizeHorizontal -> 0x06006128 @ (16,16)
ResizeVertical -> 0x06005E66 @ (16,16)
ResizeDiagonalNWSE -> 0x06006126 @ (16,16)
ResizeDiagonalNESW -> 0x06006127 @ (16,16)
UpdateCursor(root):
kind = resolve semantic interaction state
authored = captured-or-hovered element cursor, using retail precedence
if a synthetic resize edge owns the pointer or a resize/move operation owns capture
cursor = ResolveWindowControlCursor(kind)
else if authored is valid
cursor = authored
else
cursor = ResolveWindowControlCursor(kind)
render cursor; otherwise retain the ClientUISystem global cursor
```
Authored widget media therefore remains the first hover-time oracle. The
shared fallback exists for retained wrapper borders and the accepted IA-12
whole-window drag adaptation, which have no authored Dragbar/Resizebar child.
Once a synthetic operation starts, its control cursor wins just as retail's
captured Dragbar or Resizebar wins.