acdream/docs/research/2026-07-10-retained-widget-foundations-pseudocode.md

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.