769 lines
28 KiB
Markdown
769 lines
28 KiB
Markdown
# Retained widget foundations -- named-retail pseudocode
|
|
|
|
**Date:** 2026-07-10
|
|
**Scope:** Wave 0 oracle for `UIElement_Text`, `UIElement_Button`, raw edge
|
|
layout, UI time delivery, tooltip cancellation, and focus ordering. This note
|
|
changes no runtime behavior.
|
|
|
|
## Sources and confidence
|
|
|
|
Primary evidence:
|
|
|
|
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`, the Sept 2013 EoR
|
|
PDB-named client decompilation.
|
|
- `docs/research/named-retail/acclient.h`, especially
|
|
`UIElement_Text::UIText_Flag`, `UIElement_Text`, `UIElement_Button`,
|
|
`UIElementManager`, `ElementDesc`, and `Box2D`.
|
|
- Static x86 disassembly of the matching `C:\Turbine\Asheron's Call\acclient.exe`
|
|
for the expressions that both Binary Ninja and Ghidra lost around x87
|
|
`_ftol2` calls in `UIElement::UpdateForParentSizeChange`.
|
|
|
|
Cross-checks:
|
|
|
|
- `docs/research/2026-06-15-layoutdesc-format.md` and the production LayoutDesc
|
|
data it records. Its property observations agree with the named code, but its
|
|
`AnchorEdges` reduction is intentionally lossy and its text-property table has
|
|
two margin fields transposed; the corrections are listed below.
|
|
- `docs/research/retail-ui/02-class-hierarchy.md` and
|
|
`docs/research/retail-ui/04-input-events.md`, the older `FUN_xxx` research.
|
|
These are useful as provenance but are superseded where they conflict with
|
|
the PDB-named client. In particular, the named client contains the full
|
|
`UIElement*` hierarchy and does **not** implement the inferred generic
|
|
`Device::RegisterTimerEvent` queue described there.
|
|
- The only vendored reference tree present in this worktree is WorldBuilder;
|
|
it does not implement the retail retained-widget system. No independent ACE,
|
|
AC2D, ACViewer, or holtburger UI implementation was available here to
|
|
corroborate these client-internal mechanics.
|
|
|
|
Confidence is **high** for every branch below unless it is explicitly marked
|
|
uncertain. Addresses were checked against `symbols.json` or the named function
|
|
header in the pseudo-C.
|
|
|
|
## Corrections that affect the implementation plan
|
|
|
|
1. Retail text interactivity is **two orthogonal capabilities**, not a
|
|
three-value exclusive mode. `Editable` (`0x16`) and `Selectable` (`0x27`)
|
|
can be independently false/true. An editable, non-selectable field accepts
|
|
edits but does not begin drag selection. The modern representation should
|
|
therefore be flags or two booleans; `Display` means both are false.
|
|
2. A newly constructed Type-12 text starts with `m_bitField = 0x300`
|
|
(`DIRTY | CURSOR_VISIBLE`), not editable and not selectable. Imported labels
|
|
must not be made interactive merely because their resolved type is 12.
|
|
3. There is no evidence for the old generic Device timer queue in this build.
|
|
Retail time-driven UI is a per-frame global message (`3`) plus explicit
|
|
deadline polling by `UIElementManager::CheckTooltip`. The unpumped
|
|
`UiRoot.RegisterTimerEvent` list is not a faithful retail mechanism.
|
|
4. Raw edge modes `3` and `4` cannot be reduced to `AnchorEdges`: `3` performs
|
|
exact inclusive-pixel centering and `4` proportionally scales the individual
|
|
edge coordinate with truncation toward zero.
|
|
5. In the older LayoutDesc note, the exact text margins should be
|
|
`0x23=left`, `0x24=right`, `0x25=upper`, `0x26=lower`, as shown by
|
|
`UIElement_Text::OnSetAttribute @ 0x0046A640`.
|
|
|
|
---
|
|
|
|
## 1. `UIElement_Text`
|
|
|
|
### 1.1 Flag model and defaults
|
|
|
|
`acclient.h` defines the exact flags:
|
|
|
|
| Flag | Value | Meaning |
|
|
|---|---:|---|
|
|
| `UITF_EDITABLE` | `0x0001` | Editing/input capability |
|
|
| `UITF_ONE_LINE` | `0x0002` | One-line layout |
|
|
| `UITF_SELECTABLE` | `0x0004` | Selection/copy capability |
|
|
| `UITF_NO_IME` | `0x0008` | Do not enable IME |
|
|
| `UITF_OUTLINE` | `0x0010` | Outline glyphs |
|
|
| `UITF_DROPSHADOW` | `0x0020` | Drop shadow glyphs |
|
|
| `UITF_MOUSE_SELECTING` | `0x0040` | Pointer drag is active |
|
|
| `UITF_SELECTING` | `0x0080` | Selection anchor/end are active |
|
|
| `UITF_DIRTY` | `0x0100` | Glyph/layout cache dirty |
|
|
| `UITF_CURSOR_VISIBLE` | `0x0200` | Caret blink phase visible |
|
|
| `UITF_FIT_TO_TEXT` | `0x0400` | Resize element to paper |
|
|
| `UITF_TRUNCATE_TEXT_TO_FIT` | `0x0800` | Trailer/truncation path |
|
|
| `UITF_LOSE_FOCUS_ON_ESCAPE` | `0x1000` | Escape relinquishes focus |
|
|
| `UITF_LOSE_FOCUS_ON_ACCEPT_INPUT` | `0x2000` | Accept-input action relinquishes focus |
|
|
|
|
`UIElement_Text::UIElement_Text @ 0x00468570` initializes the flag word to
|
|
`0x300`, selection/cursor positions to zero, horizontal justification to `2`,
|
|
vertical justification to `4`, margins to zero, and cursor/flash timestamps to
|
|
`-1.0`. Thus the retail default is a display-only label.
|
|
|
|
### 1.2 Interaction-relevant property IDs
|
|
|
|
`UIElement_Text::OnSetAttribute @ 0x0046A640` first delegates to
|
|
`UIElement_Scrollable::OnSetAttribute`, then handles these properties:
|
|
|
|
| Property | Value kind | Exact effect |
|
|
|---:|---|---|
|
|
| `0x14` | enum | Horizontal justification |
|
|
| `0x15` | enum | Vertical justification |
|
|
| `0x16` | bool | `SetEditable`; then recompute mouse visibility |
|
|
| `0x17` | `StringInfo` | Set contents, or clear when invalid |
|
|
| `0x1A` | array/data-id | Re-resolve current font |
|
|
| `0x1B` | array/color | Re-resolve current font color |
|
|
| `0x1D` | array/color | Re-resolve tag font color |
|
|
| `0x1E` | integer | Maximum characters |
|
|
| `0x1F` | bool | `SetNoIme` |
|
|
| `0x20` | bool | `SetOneLine` |
|
|
| `0x21` | bool | `SetOutline` |
|
|
| `0x22` | color | Outline color; then font reset |
|
|
| `0x23` | integer | Left margin |
|
|
| `0x24` | integer | Right margin |
|
|
| `0x25` | integer | Upper margin |
|
|
| `0x26` | integer | Lower margin |
|
|
| `0x27` | bool | `SetSelectable`; then recompute mouse visibility |
|
|
| `0x28` | bool | Glyph list `TrimFromTop` |
|
|
| `0x29` | bool | `SetFitToText` |
|
|
| `0xC7` | `StringInfo` | Enable/define truncation trailer; invalid disables it |
|
|
| `0xCB` | bool | Lose focus on Escape |
|
|
| `0xCC` | bool | Lose focus on accept-input |
|
|
|
|
### 1.3 Capability setters
|
|
|
|
Sources: `SetEditable @ 0x004680D0`, `SetOneLine @ 0x00468170`, and
|
|
`SetSelectable @ 0x004681C0`.
|
|
|
|
```text
|
|
SetEditable(value):
|
|
if value == current Editable: return
|
|
|
|
if value is false:
|
|
if this has global focus AND Selectable is false:
|
|
RelinquishFocus()
|
|
else if TruncateTextToFit is true:
|
|
clear TruncateTextToFit
|
|
mark dirty and dirty the root
|
|
|
|
set Editable = value
|
|
|
|
SetSelectable(value):
|
|
if value == current Selectable: return
|
|
|
|
if value is false:
|
|
if this has global focus AND Editable is false:
|
|
RelinquishFocus()
|
|
else if TruncateTextToFit is true:
|
|
clear TruncateTextToFit
|
|
mark dirty and dirty the root
|
|
|
|
set Selectable = value
|
|
if the internal selection-direction marker is active:
|
|
clear selecting/mouse-selecting and zero both selection endpoints
|
|
|
|
SetOneLine(value):
|
|
if value differs:
|
|
set OneLine = value
|
|
mark dirty and dirty the root
|
|
```
|
|
|
|
The high internal selection-direction bit checked by `SetSelectable` and
|
|
`SetCursorPosition` is not named in `UIText_Flag`; it is deliberately not given
|
|
a speculative public name here.
|
|
|
|
`SetTruncateTextToFit @ 0x00467480` refuses to enable truncation whenever
|
|
either `Editable` or `Selectable` is set.
|
|
|
|
### 1.4 Mouse visibility
|
|
|
|
Sources: `UIElement_Text::GetShouldBeMouseVisible @ 0x00467460` and
|
|
`UIElement::GetShouldBeMouseVisible @ 0x004601B0`.
|
|
|
|
```text
|
|
Text.GetShouldBeMouseVisible():
|
|
if Editable OR Selectable:
|
|
return true
|
|
return base.GetShouldBeMouseVisible()
|
|
|
|
Base.GetShouldBeMouseVisible():
|
|
return explicit mouse-visible/interaction flag OR valid tooltip text
|
|
```
|
|
|
|
Consequently, a plain imported label does not claim the mouse unless another
|
|
base property (for example a tooltip/explicit mouse role) says it should.
|
|
|
|
### 1.5 Mouse down, selection, and activation
|
|
|
|
Sources: `MouseDown @ 0x00469370`, `MouseUp @ 0x004694F0`, and
|
|
`MouseMove @ 0x004695F0`.
|
|
|
|
```text
|
|
MouseDown(screenX, screenY, mouseAction):
|
|
remember whether this already had focus
|
|
call Scrollable.MouseDown(...)
|
|
|
|
if hidden/blocked base flags reject input: return
|
|
if mouseAction is not one of the accepted text actions (5, 6, 7): return
|
|
if the base action-enable bit is false: return
|
|
|
|
local = screen point - screen origin - margins
|
|
|
|
if mouseAction == 7 AND (Editable OR Selectable):
|
|
read bool property 0xD1
|
|
if this did not previously have focus AND property 0xD1 is true:
|
|
SelectAll()
|
|
return
|
|
|
|
hitPos = DeterminePositionFromXY(local)
|
|
previousAnchor = selectionStart when the internal selection direction
|
|
is active, otherwise current cursor
|
|
SetCursorPosition(hitPos, DontSelectText)
|
|
Activate() // activate the owning root
|
|
|
|
if Selectable:
|
|
set Selecting and MouseSelecting
|
|
if Shift is down:
|
|
selectionStart = previousAnchor
|
|
selectionEnd = cursor
|
|
else:
|
|
selectionStart = cursor
|
|
|
|
MouseMove(screenX, screenY):
|
|
if not MouseSelecting:
|
|
call base MouseMove and return
|
|
local = screen point - screen origin - margins
|
|
SetCursorPosition(DeterminePositionFromXY(local), Default)
|
|
|
|
MouseUp(screenX, screenY, mouseAction):
|
|
remember whether a glyph callback is registered for mouseAction
|
|
call base MouseUp(...)
|
|
|
|
if MouseSelecting:
|
|
set cursor from release point using Default
|
|
clear MouseSelecting
|
|
|
|
if a glyph callback was registered:
|
|
glyph = glyph at release point
|
|
if glyph has a callback: invoke it with mouseAction
|
|
```
|
|
|
|
Important boundary: `Editable` alone allows caret placement and root
|
|
activation, but only `Selectable` starts pointer selection. The method calls
|
|
`Activate`, not `TakeFocus`, so the focus transition remains governed by the
|
|
root's remembered focus element and explicit `TakeFocus` requests.
|
|
|
|
### 1.6 Focus registration and key capabilities
|
|
|
|
Sources: `RegisterInputMaps @ 0x00467350`, `UnregisterInputMaps @
|
|
0x004673D0`, `ListenToElementMessage @ 0x004687C0`, and `OnAction @
|
|
0x0046A260`.
|
|
|
|
```text
|
|
RegisterInputMaps(priority):
|
|
unregister this callback first
|
|
result = Scrollable.RegisterInputMaps(priority)
|
|
if Editable:
|
|
register input map 1 at priority - 10
|
|
register input map 7 at priority
|
|
if Editable OR Selectable:
|
|
register input map 8 at priority
|
|
return OR of registration results
|
|
|
|
On focus message 0x2F for this element:
|
|
if neither Editable nor Selectable: delegate to base
|
|
|
|
if Editable:
|
|
on gain:
|
|
register as ICIDM input handler
|
|
turn IME on unless disabled
|
|
ask Keystone to lose its external focus
|
|
on loss:
|
|
unregister ICIDM input handler
|
|
|
|
on gain:
|
|
ICIDM.SetTextMode(true)
|
|
subscribe to global message 3
|
|
on loss:
|
|
Deselect()
|
|
ICIDM.SetTextMode(false)
|
|
unsubscribe from global message 3
|
|
|
|
delegate to Scrollable.ListenToElementMessage
|
|
```
|
|
|
|
Map 8 exposes cursor movement, line/document home/end, up/down/page movement,
|
|
and Copy for both editable and selectable text. Editable-only maps add character
|
|
editing. `Cut`, `Paste`, and deletion helpers independently guard on
|
|
`UITF_EDITABLE`, so merely selectable text cannot mutate its contents.
|
|
|
|
The action at `0x25` relinquishes focus when `0xCC` is set; the action at
|
|
`0x27` relinquishes focus when `0xCB` is set. Actions `0x26` and `0x28` are
|
|
forward-delete and backspace behavior respectively, deleting the active
|
|
selection first.
|
|
|
|
### 1.7 Per-frame caret work
|
|
|
|
`UIElement_Text::Global_Loop @ 0x00469FC0` runs on global message `3`.
|
|
It checks asynchronous string downloads, then, only while
|
|
`(Editable || Selectable) && HasFocus`, uses `GetCaretBlinkTime()` converted
|
|
from milliseconds to seconds to flip `UITF_CURSOR_VISIBLE` and dirty the last
|
|
caret rectangle. This is subscription-driven; it is not a registered timer
|
|
object.
|
|
|
|
---
|
|
|
|
## 2. `UIElement_Button`
|
|
|
|
### 2.1 State inputs and numeric states
|
|
|
|
Source: `UIElement_Button::UpdateState_ @ 0x00471CF0`, verified against
|
|
the matching x86 instructions.
|
|
|
|
The method reads these bool properties:
|
|
|
|
- `0x0D`: ghosted/disabled role;
|
|
- `0x0E`: selected/toggled role;
|
|
- `0x13`: allow rollover-state change role.
|
|
|
|
It combines them with `mousePressedOnButton` and the inherited
|
|
`mouseOverTop` bit. The standard state IDs are:
|
|
|
|
| Visual phase | Unselected | Selected |
|
|
|---|---:|---:|
|
|
| Normal | `1` | `6` |
|
|
| Rollover / pressed outside | `2` | `7` |
|
|
| Pressed while pointer is over | `3` | `8` |
|
|
| Ghosted | `13` | `13` |
|
|
|
|
```text
|
|
UpdateState_():
|
|
disabled = bool property 0x0D
|
|
if disabled:
|
|
requested = 13
|
|
else:
|
|
selected = bool property 0x0E
|
|
rolloverEnabled = bool property 0x13
|
|
|
|
requested = selected ? 6 : 1
|
|
|
|
if mousePressedOnButton OR (rolloverEnabled AND mouseOverTop):
|
|
requested = selected ? 7 : 2
|
|
|
|
if mousePressedOnButton AND mouseOverTop:
|
|
requested = selected ? 8 : 3
|
|
|
|
if ElementDesc contains requested state:
|
|
virtual SetState(requested)
|
|
else:
|
|
leave the current state unchanged
|
|
```
|
|
|
|
That last test is the exact fallback rule. There is no search for a nearest
|
|
standard state. A custom semantic state survives only when the newly requested
|
|
standard state is absent from the element's DAT state table.
|
|
|
|
### 2.2 `SetState` synchronization
|
|
|
|
Source: `UIElement_Button::SetState @ 0x00471DA0`.
|
|
|
|
```text
|
|
SetState(requested):
|
|
if bool property 0x0B is true: // toggle behavior
|
|
selected = bool property 0x0E
|
|
if requested == 6 AND not selected:
|
|
set property 0x0E = true
|
|
return true
|
|
if requested == 1 AND selected:
|
|
set property 0x0E = false
|
|
return true
|
|
|
|
wantsDisabled = requested == 13
|
|
disabled = bool property 0x0D
|
|
if wantsDisabled != disabled:
|
|
set property 0x0D = wantsDisabled
|
|
return true
|
|
|
|
return UIElement.SetState(requested)
|
|
```
|
|
|
|
Thus programmatic `SetState(1/6)` is also the selected-property API for a
|
|
toggle button, and `SetState(13)` is the disabled-property API. Property
|
|
changes call `UpdateState_` through `OnSetAttribute`.
|
|
|
|
### 2.3 Mouse state machine
|
|
|
|
Sources: `MouseDown @ 0x00471FF0`, `MouseUp @ 0x004720E0`, and
|
|
`MouseOverTop @ 0x004721F0`.
|
|
|
|
```text
|
|
MouseOverTop(isOver):
|
|
base.MouseOverTop(isOver)
|
|
UpdateState_()
|
|
|
|
MouseDown(x, y, mouseAction):
|
|
Text.MouseDown(...)
|
|
if inherited visibility/input gates reject: return
|
|
if mouseAction is neither 7 nor accepted alternate action 0x0A: return
|
|
|
|
mousePressedOnButton = true
|
|
UpdateState_()
|
|
|
|
disabled = bool property 0x0D
|
|
allowHotClickWhileDisabled = bool property 0x0C
|
|
if disabled AND not allowHotClickWhileDisabled: return
|
|
|
|
if bool property 0x0F: // hot-click/auto-repeat
|
|
BroadcastElementMessage(2, mouseAction, 0) // immediate activation
|
|
hotClickingInProgress = true
|
|
subscribe to global message 3
|
|
nextHotClickTime = Timer.cur_time + float property 0x10
|
|
|
|
MouseUp(x, y, mouseAction):
|
|
Text.MouseUp(...)
|
|
disabled = bool property 0x0D
|
|
|
|
if mousePressedOnButton AND mouseAction is 7 or 0x0A:
|
|
emitClick = false
|
|
|
|
if mouseOverTop AND not disabled:
|
|
if bool property 0x0B: // toggle
|
|
property 0x0E = !property 0x0E
|
|
if not hotClickingInProgress:
|
|
emitClick = true
|
|
|
|
mousePressedOnButton = false
|
|
if hotClickingInProgress:
|
|
hotClickingInProgress = false
|
|
unsubscribe from global message 3
|
|
|
|
UpdateState_()
|
|
if emitClick:
|
|
BroadcastElementMessage(1, 7, 0)
|
|
```
|
|
|
|
Release outside never emits message `1`. During a captured press, moving outside
|
|
changes state `3/8 -> 2/7`; moving back inside restores `3/8`. A hot-click press
|
|
emits message `2` immediately and suppresses the ordinary release click.
|
|
|
|
### 2.4 Hot-click deadline delivery and cancellation
|
|
|
|
Sources: `ListenToGlobalMessage @ 0x00471C60`, `OnSetAttribute @
|
|
0x00471F40`, and the button destructor at `0x00471BC0`.
|
|
|
|
```text
|
|
On global message 3 while hotClickingInProgress:
|
|
now = Timer.cur_time
|
|
|
|
if not mouseOverTop AND now >= nextHotClickTime:
|
|
nextHotClickTime = now
|
|
|
|
if mouseOverTop AND now >= nextHotClickTime:
|
|
BroadcastElementMessage(2, 0, 0)
|
|
nextHotClickTime += float property 0x11
|
|
```
|
|
|
|
Cancellation is explicit and idempotent at all three owner transitions:
|
|
|
|
- mouse up clears `hotClickingInProgress` and unsubscribes from global `3`;
|
|
- changing bool property `0x0F` to false does the same;
|
|
- destruction does the same if a hot click is active.
|
|
|
|
`OnSetAttribute` recalculates visual state for `0x0D`, `0x0E`, and `0x13`;
|
|
`0x0D` also recomputes mouse visibility.
|
|
|
|
The descriptive names for button properties `0x0B..0x13` are not present in
|
|
the recovered headers. Their behavioral roles above are exact; do not bake the
|
|
descriptive labels into a wire/DAT enum until the master property-name table is
|
|
recovered.
|
|
|
|
---
|
|
|
|
## 3. `UIElement::UpdateForParentSizeChange`
|
|
|
|
**Oracle:** `UIElement::UpdateForParentSizeChange @ 0x00462640`.
|
|
|
|
`Box2D` is inclusive: width is `x1 - x0 + 1`, height is
|
|
`y1 - y0 + 1`. For a root/no-parent element (or the relevant root-sizing flag),
|
|
the original parent box is `[0,0,layout.displayWidth-1,layout.displayHeight-1]`
|
|
and the current parent box is `[0,0,displayWidth-1,displayHeight-1]`.
|
|
Otherwise, both boxes come from the parent.
|
|
|
|
```text
|
|
orig = this.GetOriginalPosition()
|
|
origParent = parent.GetOriginalPosition() or layout display box
|
|
curParent = parent.GetCurrentPosition() or current display box
|
|
|
|
origParentWidth = origParent.x1 - origParent.x0 + 1
|
|
origParentHeight = origParent.y1 - origParent.y0 + 1
|
|
curParentWidth = curParent.x1 - curParent.x0 + 1
|
|
curParentHeight = curParent.y1 - curParent.y0 + 1
|
|
|
|
deltaX = curParentWidth - origParentWidth
|
|
deltaY = curParentHeight - origParentHeight
|
|
|
|
scaleX = origParentWidth != 0 ? curParentWidth / origParentWidth : 0.0
|
|
scaleY = origParentHeight != 0 ? curParentHeight / origParentHeight : 0.0
|
|
```
|
|
|
|
All ratios are floating point. Mode-4 conversion uses MSVC `_ftol2`, i.e.
|
|
truncate toward zero, not floor and not round-to-nearest.
|
|
|
|
Define `halfTowardZero(n) = truncate(n / 2)` for signed integers.
|
|
|
|
```text
|
|
newX0 = orig.x0
|
|
switch LeftEdge:
|
|
2: newX0 = orig.x0 + deltaX
|
|
3: newX0 = halfTowardZero(curParentWidth)
|
|
- halfTowardZero(orig.width)
|
|
4: newX0 = truncateTowardZero(orig.x0 * scaleX)
|
|
0,1: leave orig.x0 for now
|
|
|
|
newX1 = orig.x1
|
|
switch RightEdge:
|
|
1: newX1 = orig.x1 + deltaX
|
|
3: newX1 = halfTowardZero(curParentWidth)
|
|
+ halfTowardZero(orig.width) - 1
|
|
4: newX1 = truncateTowardZero(orig.x1 * scaleX)
|
|
0,2: leave orig.x1 for now
|
|
|
|
newY0 = orig.y0
|
|
switch TopEdge:
|
|
2: newY0 = orig.y0 + deltaY
|
|
3: newY0 = halfTowardZero(curParentHeight)
|
|
- halfTowardZero(orig.height)
|
|
4: newY0 = truncateTowardZero(orig.y0 * scaleY)
|
|
0,1: leave orig.y0 for now
|
|
|
|
newY1 = orig.y1
|
|
switch BottomEdge:
|
|
1: newY1 = orig.y1 + deltaY
|
|
3: newY1 = halfTowardZero(curParentHeight)
|
|
+ halfTowardZero(orig.height) - 1
|
|
4: newY1 = truncateTowardZero(orig.y1 * scaleY)
|
|
0,2: leave orig.y1 for now
|
|
```
|
|
|
|
Then retail applies mode `0` as a preservation policy:
|
|
|
|
```text
|
|
if current width != 0 OR current height != 0 OR preserve-current flag is set:
|
|
if LeftEdge == 0: newX0 = current.x0
|
|
if TopEdge == 0: newY0 = current.y0
|
|
if RightEdge == 0: newX1 = current.x1
|
|
if BottomEdge == 0: newY1 = current.y1
|
|
```
|
|
|
|
Finally:
|
|
|
|
```text
|
|
MoveTo(newX0, newY0)
|
|
ResizeTo(newX1 - newX0 + 1, newY1 - newY0 + 1)
|
|
|
|
if original z-level differs from current z-level:
|
|
restore original z-level
|
|
ask parent to remove/reinsert this child in z-order
|
|
return true
|
|
```
|
|
|
|
### Raw-mode summary
|
|
|
|
| Raw mode | Near edge (`Left`/`Top`) | Far edge (`Right`/`Bottom`) |
|
|
|---:|---|---|
|
|
| `0` | Preserve current edge after initialization | Preserve current edge after initialization |
|
|
| `1` | Keep original near edge | Add parent size delta (stretch) |
|
|
| `2` | Add parent size delta (move with far side) | Keep original far edge |
|
|
| `3` | Recompute centered near edge | Recompute centered far edge |
|
|
| `4` | Proportionally scale original coordinate | Proportionally scale original coordinate |
|
|
|
|
Mode `4` does **not** mean “both anchors.” That description in the earlier
|
|
LayoutDesc note was only an approximation of the resulting resize for common
|
|
assets and must not survive in the imported layout policy.
|
|
|
|
---
|
|
|
|
## 4. Time delivery, tooltip cancellation, and focus ordering
|
|
|
|
### 4.1 No generic Device timer queue in the named client
|
|
|
|
The old `04-input-events.md` inferred a Device vtable timer API from unnamed
|
|
calls. The recovered `UIElementManager` fields in `acclient.h` contain tooltip
|
|
timestamps but no timer collection, and the named execution path is explicit.
|
|
The exact per-frame method is `UIElementManager::UseTime @ 0x0045CFD0`:
|
|
|
|
```text
|
|
UseTime():
|
|
CleanDeleteQueue()
|
|
ProcessUIMessageRemovalData()
|
|
if mouse hit-test requested:
|
|
DoMouseUpdate()
|
|
CheckTooltip()
|
|
BroadcastGlobalMessage(3, 0)
|
|
if ICIDM exists:
|
|
ICIDM.UseTime()
|
|
DrawDirtyRegions()
|
|
```
|
|
|
|
Global message `3` is the shared time pulse used by text caret/download work and
|
|
button hot-click repeat. Subscription and unsubscription are the cancellation
|
|
mechanism. Deletions and deferred listener removals are processed **before** the
|
|
time pulse.
|
|
|
|
### 4.2 Tooltip deadlines
|
|
|
|
Sources: manager constructor `0x0045F5D0`, `CheckTooltip @ 0x0045B6E0`,
|
|
`StartHover @ 0x00459250`, `StopHover @ 0x00459280`, `SwitchMouseOver @
|
|
0x0045B560`, `MouseMoveHandler @ 0x0045E710`, and `ReleaseMouseCapture @
|
|
0x0045D2B0`.
|
|
|
|
Defaults are **0.25 seconds** delay and **10 seconds** duration. The delay is a
|
|
user preference (`Misc.TooltipDelay`, allowed range 0..10 seconds); per-element
|
|
float property `0x50` overrides it.
|
|
|
|
```text
|
|
MouseMoveHandler(...):
|
|
lastMouseMoveTime = Timer.local_time
|
|
refresh the element under the pointer and call SwitchMouseOver
|
|
continue drag processing
|
|
|
|
ReleaseMouseCapture(owner):
|
|
if owner holds capture:
|
|
lastMouseMoveTime = Timer.local_time
|
|
decrement/release capture
|
|
|
|
CheckTooltip():
|
|
now = Timer.local_time
|
|
|
|
if hover has not started AND mouse is inside the window:
|
|
if no element has mouse capture AND lastEntered exists:
|
|
delay = lastEntered.floatProperty(0x50) or manager default
|
|
if now >= lastMouseMoveTime + delay:
|
|
StartHover(currentMouseX, currentMouseY)
|
|
|
|
if tooltip element exists:
|
|
if now - tooltipStart >= tooltipDuration:
|
|
queue tooltip element for deletion
|
|
tooltipElement = null
|
|
SwitchMouseOver(null)
|
|
|
|
StartHover(x, y):
|
|
if hover has not started:
|
|
hoverStarted = lastEntered.MouseHover(x, y)
|
|
|
|
StopHover():
|
|
if hoverStarted:
|
|
hoverStarted = false
|
|
lastEntered?.MouseUnhover()
|
|
if tooltipElement exists:
|
|
queue it for deletion
|
|
tooltipElement = null
|
|
```
|
|
|
|
`SwitchMouseOver(old -> new)` calls `StopHover` before sending
|
|
`old.MouseOverTop(false)`, then sends `new.MouseOverTop(true)`. Capture prevents
|
|
a new tooltip deadline from starting. `UIElementManager::DeletingElement @
|
|
0x0045E520` also clears capture/hover/drag/tooltip ownership and removes queued
|
|
element messages that target the deleted element.
|
|
|
|
There is therefore no “remove a due timer before callback” retail rule to port
|
|
for tooltips. The faithful rule is owner-state cancellation plus subscription
|
|
teardown before the global time pulse.
|
|
|
|
### 4.3 Focus transfer order
|
|
|
|
Sources: `UIElementManager::SetFocusElement @ 0x0045B890`,
|
|
`UIElement::TakeFocus @ 0x004601E0`, `UIElement::RelinquishFocus @
|
|
0x00461DE0`, and `UIElementManager::BroadcastElementMessage @ 0x0045AB40`.
|
|
|
|
```text
|
|
SetFocusElement(newFocus):
|
|
oldFocus = focusElement
|
|
if oldFocus == newFocus: return
|
|
|
|
if oldFocus exists:
|
|
BroadcastElementMessage(oldFocus, 0x2F, param1=0, param2=0)
|
|
oldFocus.UnregisterInputMaps()
|
|
|
|
focusElement = newFocus
|
|
|
|
if newFocus exists:
|
|
BroadcastElementMessage(newFocus, 0x2F, param1=1, param2=0)
|
|
newFocus.RegisterInputMaps(priority=0x0BB8) // 3000
|
|
|
|
TakeFocus(element):
|
|
root = element.GetRootElement()
|
|
if root is null: return false
|
|
set element's owns-focus marker
|
|
if root is active:
|
|
SetFocusElement(element)
|
|
becameGlobal = true
|
|
root.rememberedFocusDescendant = element
|
|
return becameGlobal
|
|
|
|
RelinquishFocus(element):
|
|
root = element.GetRootElement()
|
|
if root exists:
|
|
root.rememberedFocusDescendant = null
|
|
clear element's owns-focus marker
|
|
if manager.focusElement == element:
|
|
SetFocusElement(null)
|
|
return true
|
|
```
|
|
|
|
Focus messages are synchronous. If a handler broadcasts another element message
|
|
while one is already broadcasting, `BroadcastElementMessage` appends it to a
|
|
FIFO list and drains that list before clearing the broadcasting flag. This
|
|
prevents recursive listener traversal while retaining deterministic order.
|
|
|
|
Active-root changes are separate from text focus. `UIElement::Activate @
|
|
0x00461C80` activates/registers the root and restores its remembered focus
|
|
descendant through `SetFocusElement`; `Deactivate @ 0x00461D50` broadcasts root
|
|
deactivation and clears global focus when the root owned it.
|
|
|
|
### 4.4 Keyboard routing relative to focus
|
|
|
|
`UIElementManager::KeyPressEvent @ 0x0045C300` uses this order when the debug
|
|
console is not intercepting input:
|
|
|
|
```text
|
|
target = focusElement if present else activeElement
|
|
if target exists:
|
|
consumed = target.KeyDown(action, extent)
|
|
if not consumed:
|
|
BroadcastGlobalMessage(1, action)
|
|
else:
|
|
BroadcastGlobalMessage(1, action)
|
|
|
|
DoVisibilityToggleAction(action) // still runs after the route above
|
|
```
|
|
|
|
That final visibility-toggle call is unconditional within the normal input path;
|
|
it is not gated by whether the focus/active target consumed `KeyDown`.
|
|
|
|
---
|
|
|
|
## 5. Conformance vectors to carry into Wave 1
|
|
|
|
1. **Text defaults:** construct Type 12 with no interaction properties; assert
|
|
editable=false, selectable=false, and no mouse claim absent tooltip/base role.
|
|
2. **Orthogonal capabilities:** cover all four `(editable, selectable)` pairs.
|
|
Editable-only places the caret but does not drag-select; selectable-only
|
|
selects/copies but cut/paste/delete do not mutate.
|
|
3. **Focus teardown:** disabling the last active text capability while focused
|
|
relinquishes focus; disabling one while the other remains does not.
|
|
4. **Button states:** golden table for states `1/2/3/6/7/8/13`, captured release
|
|
outside, re-entry, toggle on/off, and missing-state fallback.
|
|
5. **Hot click:** immediate message `2`, initial delay `0x10`, repeat interval
|
|
`0x11`, pause/reset while pointer is outside, and cancellation on mouse-up,
|
|
property disable, and destruction.
|
|
6. **Layout:** one vector per raw mode on each individual edge; include odd and
|
|
even parent/child sizes, negative intermediate centered values, a zero-sized
|
|
original parent, mode-0 preservation after initialization, and nested parents.
|
|
7. **Time order:** deletion/removal before global tick `3`; tooltip check before
|
|
tick `3`; capture release restarts the tooltip idle deadline.
|
|
8. **Focus order:** old loss message -> old map removal -> pointer assignment ->
|
|
new gain message -> new map registration; nested focus messages drain FIFO.
|
|
|
|
## Remaining uncertainties
|
|
|
|
- The recovered symbols do not name button properties `0x0B..0x13`; this note
|
|
records exact observed behavior but intentionally does not assert canonical
|
|
property names for all of them.
|
|
- Mouse action `7` is the normal primary-button path in these methods. Alternate
|
|
action `0x0A` is accepted by `UIElement_Button`; its user-facing input name was
|
|
not recovered in this bounded pass.
|
|
- The high text selection-direction bit used in `SetCursorPosition` is outside
|
|
`UIText_Flag` and remains unnamed.
|
|
- This pass did not reconstruct every `ICIDM` input action name, only the exact
|
|
routing and behavior needed to separate display/selectable/editable text.
|