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

6.6 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
    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
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 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:

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