fix(ui): restore retail spell tabs and spellbook

Bind the spellbook and component-book tabs as their authored stateful text controls, propagate Closed/Open state into the retained DAT child tree, and keep authored label colors live across state changes.

Match retail UIElement_Text zero-margin construction so the Magic favorite captions I through VIII are no longer clipped. Add a real portal.dat spellbook fixture plus controller, state-propagation, and text-layout regression coverage.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 15:54:11 +02:00
parent 07be994d97
commit ad30c37a48
15 changed files with 13492 additions and 38 deletions

View file

@ -198,6 +198,43 @@ component item list 0x10000464
component scrollbar 0x10000465
```
The two top-level tabs are not `UIElement_Button` instances. They resolve from
base `0x1000043A` as `UIElement_Text` (type 12), with state 11 `Closed` and
state 12 `Open`. Both states set `PassToChildren`; the three child chrome
elements carry the left/center/right Closed/Open RenderSurface media.
Retail state and text construction used by these tabs:
```text
UIElement_Text::UIElement_Text @ 0x00468570
construct UIElement_Scrollable
bitField = 0x300 // OneLine bit 0x2 is clear
horizontalJustification = 2
verticalJustification = 4
marginUp/Down/Left/Right = 0
UIElement_Text::OnSetAttribute @ 0x0046A640
property 0x20 only:
SetOneLine(value)
// absent 0x20 therefore remains multi-line; do not force it true
UIElement::SetState @ 0x00464E70
next = desc.AccessStateDesc(stateId)
if next != current:
current = next
if next.PassToChildren:
for each child:
child.SetState(stateId)
diff old/new effective properties
call OnSetAttribute for changed properties
```
The Magic-combat favorite tab captions are authored text (`I` through `VIII`)
and intentionally omit property `0x20`. Their small rectangles still render
because retail begins with zero text margins. A generic four-pixel inset on
all sides leaves less than one DAT-font line and clips the captions completely;
that inset is not retail behavior.
Spellbook rows come only from the authoritative learned-spell set. Retail
sorts by `CSpellBase::_display_order`; school and level buttons apply the
server-persisted filter bitfield. Component rows use the installed component