acdream/docs/research/2026-07-23-retail-spellbar-overflow-pseudocode.md
Erik 0134122c28 fix(ui): match retail spell bar controls
Place favorite-bar arrows by their authored sides, import rollover and pressed media through the shared scrollbar, and preserve manual offsets across passive refreshes. Carry the mixed-parent DAT anchor chain to a fixed 18-cell favorite viewport so overflow controls and the Cast button remain inside the retail-sized combat frame.

Co-authored-by: Codex <codex@openai.com>
2026-07-23 07:49:09 +02:00

179 lines
6.6 KiB
Markdown

# Retail favorite spell-bar overflow pseudocode
This note is the implementation oracle for world-interaction completion Slice
1. Addresses refer to the September 2013 EoR named retail client. The exact
element graph and inherited properties were verified against combat LayoutDesc
`0x21000073` and the existing imported fixture
`combat_21000073.json`.
## Authored element graph
Each of the eight `SpellCastSubMenu` groups contains:
```text
favorite group
scrollbar 0x100000B5, 685x36
increment reference 0x10000072, authored x=0, 23x36
Normal 0x06004CDE (left-facing arrow)
Rollover 0x06004CDF
decrement reference 0x10000071, authored x=63, 23x36
Normal 0x06004CDC
Rollover 0x06004CDD
item list 0x100000B6, x=23, y=2, 639x32
scrollbar property 0x77 = increment button 0x10000072
scrollbar property 0x78 = decrement button 0x10000071
scrollbar property 0x79 = HideDisabled true
item-list property 0x71 = horizontal scrollbar 0x100000B5
```
The spell-bar scrollbar has no authored track or thumb. Its 23-pixel buttons
occupy the space immediately outside the 639-pixel item-list viewport. The
property names do not determine their drawn side:
`UIElement_Scrollbar::UpdateScrollingArea @ 0x00470AA0` places the 0x77
increment reference at the leading/top-left edge and the 0x78 decrement
reference at the trailing/bottom-right edge. For this horizontal fixture that
means `0x06004CDE` on the left and `0x06004CDC` on the right.
## Initialization and ownership
Retail:
- `SpellCastSubMenu::Init @ 0x004C5A90`
- `UIElement_Scrollable::PostInit @ 0x004744B0`
- `UIElement_Scrollable::OnSetAttribute @ 0x00474630`
```text
SpellCastSubMenu.Init(parent, groupId, tabId, tabIndex):
group = parent.GetChildRecursive(groupId)
spellItemList = group.GetChildRecursive(0x100000B6)
register this as the item-list drag handler
spellTabElement = parent.GetChildRecursive(tabId)
UIElement_Scrollable.OnSetAttribute(property):
if property id == 0x71:
horizontalScrollbarId = property element id
UIElement_Scrollable.PostInit():
horizontalScrollbar = resolve horizontalScrollbarId
register for scrollbar messages
UpdateScrollbarSize(horizontal=true)
```
The controller does not synthesize arrow buttons. LayoutDesc owns them; the
ordinary scrollable/listbox relationship owns the offset.
## Size, disabled state, and visibility
Retail:
- `UIElement_Scrollable::UpdateScrollbarSize_ @ 0x004741A0`
- `UIElement_Scrollbar::SetDisabled @ 0x004703D0`
- `UIElement_Scrollbar::SetHideDisabled @ 0x00470400`
- `UIElement_Scrollbar::UpdateLayout @ 0x004710D0`
```text
UpdateScrollbarSize(horizontal):
content = scrollableWidth
viewport = element.Width
if content <= viewport:
content = viewport
scrollOffsetX = 0
disabled = true
else:
disabled = false
scrollbar.Horizontal = true
scrollbar.Disabled = disabled
scrollbar.Proportion = min(1, viewport / content)
Scrollbar.UpdateLayout():
scrollingArea = own rectangle
position property-0x77 button at the start and subtract its authored width
position property-0x78 button at the end and subtract its authored width
update thumb/track geometry, when those children exist
Visible = NOT (Disabled AND HideDisabled)
```
For the favorite bar, disabled plus HideDisabled removes both arrows when every
favorite/empty cell fits. With overflow, the arrows remain visible and the
item list clips the horizontally translated cells.
The combat LayoutDesc deliberately mixes design extents: its outer combat root
is 610 pixels wide, while the magic page's children are authored against an
800-pixel parent. Applying retail
`UIElement::UpdateForParentSizeChange @ 0x00462640` through that complete
raw-edge chain gives a 439-pixel favorite viewport at the 610-pixel root—the
13-slot short bar seen before this slice.
The retail presentation exposes 18 favorite cells: nine shortcut-numbered
cells plus nine unnumbered cells. At 32 pixels per cell the target viewport is
576 pixels. Because every horizontal raw-edge stage carries the parent delta
one-for-one, solving the imported tree for that viewport yields:
```text
outer combat root = 747 px
magic page = 737 px (5 px inset on each side)
favorite group = 622 px (x=40, followed by Cast)
scrollbar = 622 px (23 + 576 + 23)
favorite list = 576 px (18 * 32)
Cast = x=662, width=75 inside the magic page
```
This is a fixed visible-capacity HUD, not a desktop-wide strip. Stretching it
to the display width makes the favorite content fit, which legitimately
disables and hides the `HideDisabled` overflow arrows.
## Scroll delta and selection exposure
Retail:
- `UIElement_Scrollable::HandleScrollbarMessage_ @ 0x00474370`
- `UIElement_ListBox::InqScrollDelta @ 0x0046DC10`
- `SpellCastSubMenu::SetSelected @ 0x004C5B00`
```text
on decrement:
delta = -itemList.InqScrollDelta(horizontal, line)
set horizontal pixel offset += delta
on increment:
delta = +itemList.InqScrollDelta(horizontal, line)
set horizontal pixel offset += delta
ItemList.InqScrollDelta(horizontal, line):
if column count is nonzero:
return min(viewportWidth, scrollableWidth / columnCount)
return 0
SpellCastSubMenu.SetSelected(spellId):
for each item in list order:
item.Selected = item.spellId == spellId
if selected:
endowmentSelected = false
itemList.ScrollToView(itemIndex)
selectedSpellId = spellId
```
The favorite cells are 32 pixels wide, so a line arrow moves exactly 32
pixels. An actual programmatic, keyboard, or pointer selection exposes the
selected cell even when it was outside the prior viewport. Passive endowment
or object-table refresh does not re-run `SetSelected`; doing so would undo a
manual scrollbar offset.
## Port constraints
- Preserve the imported `0x71`/`0x77`/`0x78` relationships.
- Preserve each authored button's dimension; do not assume the 16-pixel chat
scrollbar button size.
- Preserve each authored button's Normal, Normal_rollover, and Normal_pressed
media through the shared scrollbar presentation path.
- Do not fabricate a track/thumb for this arrow-only control.
- Derive disabled state from the shared model's content and viewport extents.
- Hide disabled controls from both drawing and hit-testing.
- Keep every favorite tab's scroll offset independent.
- Carry the DAT raw-edge policies through the full imported combat tree and
solve the root width for an 18-cell (576-pixel) favorite viewport.
- Keep the resulting 747-pixel HUD width fixed across desktop resizes; do not
turn the favorite bar into a desktop-wide strip.