acdream/docs/research/2026-07-23-retail-spellbar-overflow-pseudocode.md
Erik 02c29e67c8 feat(ui): port favorite spell bar overflow
Import the retail arrow-only spell bar scrollbar from LayoutDesc, preserve authored end-button extents and HideDisabled behavior in the shared retained widget, and bind each favorite list to its sole horizontal pixel-scroll model. Match retail selection exposure for off-screen spells and pin the behavior with real-fixture conformance tests.

Document the six-slice world-interaction completion program as the active pre-M4 work order.

Co-authored-by: Codex <codex@openai.com>
2026-07-23 06:35:53 +02:00

4.5 KiB

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:

favorite group
  scrollbar 0x100000B5, 685x36
    decrement button 0x10000071, 23x36
      Normal   0x06004CDC
      Rollover 0x06004CDD
    increment button 0x10000072, 23x36
      Normal   0x06004CDE
      Rollover 0x06004CDF
  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.

Initialization and ownership

Retail:

  • SpellCastSubMenu::Init @ 0x004C5A90
  • UIElement_Scrollable::PostInit @ 0x004744B0
  • UIElement_Scrollable::OnSetAttribute @ 0x00474630
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
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 decrement button at the start and subtract its authored width
    position increment 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.

Scroll delta and selection exposure

Retail:

  • UIElement_Scrollable::HandleScrollbarMessage_ @ 0x00474370
  • UIElement_ListBox::InqScrollDelta @ 0x0046DC10
  • SpellCastSubMenu::SetSelected @ 0x004C5B00
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. Programmatic or keyboard selection exposes the selected cell even when it was outside the prior viewport.

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