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>
This commit is contained in:
Erik 2026-07-23 07:01:01 +02:00
parent 02c29e67c8
commit 0134122c28
17 changed files with 424 additions and 59 deletions

View file

@ -14,8 +14,12 @@ bodies begin: spell-bar overflow controls, status-bar Use/Assess commands,
assessment information, equipped-child picking, vendor browsing, and
server-authoritative vendor transactions. Slice 1 is active and ports the
DAT-authored arrow-only favorite-spell scrollbar through the shared retained
scrollbar/list model. Its implementation and complete Release suite pass;
connected overflow-arrow visual acceptance is pending.
scrollbar/list model. The corrected import preserves authored arrow sides and
rollover/pressed media, passive refresh preserves manual offsets, and the
mixed-parent DAT anchor chain produces the fixed retail capacity of 18 visible
favorite cells (nine numbered plus nine unnumbered). Its
implementation and complete Release suite pass; connected overflow-arrow
visual acceptance is pending.
---

View file

@ -43,12 +43,18 @@ Each slice begins with named-retail research, produces pseudocode and
conformance tests, updates the divergence register if required, and lands as a
separate bisectable commit. A visual gate follows each UI-bearing slice.
Slice 1 now imports both 23-pixel arrow buttons and HideDisabled from the real
combat fixture, shares the list's single pixel-scroll model, advances one
32-pixel cell per press, and exposes programmatically selected spells. The
focused 57-test set, all 3,466 App tests / 3 skips, the Release solution build,
and all 7,839 solution tests / 5 skips pass. Only the connected visual gate
remains before Slice 2 starts.
Slice 1 now imports both 23-pixel arrow buttons, their rollover/pressed media,
and HideDisabled from the real combat fixture; places their artwork by the
authored leading/trailing positions; shares the list's single pixel-scroll
model; advances one 32-pixel cell per press; and exposes only actively selected
spells. Passive object/endowment refresh preserves a manual offset. The mixed
610/800-pixel DAT anchor chain is solved to a fixed 747-pixel combat root,
producing exactly 18 visible 32-pixel favorite cells (nine numbered plus nine
unnumbered) without consuming the overflow. The initial 57-test/full-suite gate passed;
the corrective 104-test focus set, 3,472 App tests / 3 skips, Release solution
build, and 7,845 complete-solution tests / 5 skips pass. The connected gate is
pending after user feedback on arrow sides, rollover, width, and offset
persistence.
## Slice 1 — spell-bar overflow arrows
@ -74,7 +80,8 @@ Named retail references and executable pseudocode are recorded in
1. Generalize `DatWidgetFactory.BuildScrollbar` so horizontal and vertical
scrollbars both import the referenced decrement/increment children,
including their authored dimensions and HideDisabled property.
including their authored positions, dimensions, Normal/rollover/pressed
media, and HideDisabled property.
2. Generalize `UiScrollbar` to use distinct authored decrement/increment
extents for rendering, hit-testing, track geometry, and dragging. Preserve
the existing 16-pixel default for layouts whose button children are absent.
@ -85,10 +92,14 @@ Named retail references and executable pseudocode are recorded in
`UiItemList.Scroll`, enable horizontal scrolling, and retain one independent
pixel offset per favorite tab.
5. Match `SpellCastSubMenu::SetSelected`: selecting a spell through keyboard,
shortcut, or code scrolls that item into view.
6. Pin the importer, arrow hit extents, 32-pixel step, no-overflow behavior,
shortcut, or code scrolls that item into view; passive state refresh does
not re-expose it.
6. Carry the combat root's mixed-parent raw-edge policies through the complete
imported tree and solve for the retail 18-cell favorite viewport; keep that
HUD capacity fixed across desktop resizes.
7. Pin the importer, arrow hit extents, 32-pixel step, no-overflow behavior,
controller binding, and selection exposure with focused App tests.
7. Run the App Release suite, solution Release build, and complete Release
8. Run the App Release suite, solution Release build, and complete Release
suite. Then update the roadmap/memory and request the connected visual gate:
place more favorites than fit, scroll both directions, change tabs, and
verify arrows disappear on a non-overflowing tab.

View file

@ -13,12 +13,12 @@ Each of the eight `SpellCastSubMenu` groups contains:
```text
favorite group
scrollbar 0x100000B5, 685x36
decrement button 0x10000071, 23x36
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
increment button 0x10000072, 23x36
Normal 0x06004CDE
Rollover 0x06004CDF
item list 0x100000B6, x=23, y=2, 639x32
scrollbar property 0x77 = increment button 0x10000072
@ -28,7 +28,12 @@ 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.
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
@ -85,8 +90,8 @@ UpdateScrollbarSize(horizontal):
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
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)
```
@ -95,6 +100,31 @@ 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:
@ -127,15 +157,23 @@ SpellCastSubMenu.SetSelected(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.
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.