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

@ -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.