acdream/docs/research/2026-07-17-retail-window-control-cursor-pseudocode.md
Erik 06016014bc 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.
2026-07-17 09:40:08 +02:00

137 lines
5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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