fix(ui): complete retail inventory scroll polish

Preserve pixel scroll offsets across inventory rebuilds, crop partially visible rows with nested geometry/UV clips, and replace the obsolete 560px resize ceiling with available screen height. Keep retail's row-sized wheel step while allowing continuous scrollbar thumb positions.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:56:33 +02:00
parent ea72c395c9
commit 15aa3b9aff
15 changed files with 379 additions and 34 deletions

View file

@ -1933,7 +1933,7 @@ its open/closed sprites when picked up.
## #147 — Inventory item-grid scrolling polish
**Status:** OPEN
**Status:** IMPLEMENTED 2026-07-11 — live visual gate pending
**Severity:** LOW
**Filed:** 2026-06-22
@ -1942,7 +1942,7 @@ whole-row clip via `UiScrollable`) works but needs visual/UX polish — smoothne
wheel step, row-clip edges. Confirm the exact symptoms with the user when picked up. Filed from the
D.2b inventory visual gate.
**2026-07-09 triage:** investigated, verdict STILL_OPEN — `UiItemList.LayoutCells` still does whole-row visibility clipping and `UiScrollable.cs` is still an unanimated integer-pixel scroll model; no polish commit has landed since filing, and the roadmap/memory crib both still list it as pending.
**2026-07-11 implementation:** retained list rebuilds now defer layout until all cells are restored, preserving the exact `ScrollY` across inventory changes. Whole-row visibility clipping is replaced by intersecting-row visibility plus a nested child viewport clip; sprite geometry and UVs are cropped together, so thumb dragging shows clean partially visible rows instead of black gaps. Named retail confirms the position itself is an integer pixel coordinate (`ScrollToY`/`SetScrollableXY`), while wheel/arrow line steps remain one row (`InqScrollDelta = scrollableHeight / rowCount`). The inventory's old 560 px maximum-height cap was also the reason a persisted 560 px window could not expand further; its maximum now uses available screen height (AP-54 remains for exact keystone bounds). Automated tests cover partial top/bottom rows, UV crop, scroll preservation through refresh, and resize geometry; warning-free App Release plus all 4,702 tests pass. Research: `docs/research/2026-07-11-retail-inventory-scroll-pseudocode.md`.
---
@ -2050,7 +2050,7 @@ See divergence register **AP-59**.
## #141 — Toolbar interactivity — selected-object display
**Status:** IN PROGRESS (health/name/flash DONE and visually confirmed 2026-06-20; formatted stack count + entry/slider visually confirmed 2026-07-11; partial stack-transfer live gate pending; mana still deferred). Renumbered from #140 on the 2026-06-20 main merge — A7 Fix D held #140 on main; this branch's commits/spec still reference #140.
**Status:** IN PROGRESS (health/name/flash DONE and visually confirmed 2026-06-20; formatted stack count, entry/slider, and partial stack transfers visually confirmed 2026-07-11; mana still deferred). Renumbered from #140 on the 2026-06-20 main merge — A7 Fix D held #140 on main; this branch's commits/spec still reference #140.
**Severity:** MEDIUM
**Filed:** 2026-06-17
**Component:** ui — D.5 toolbar / selection
@ -2060,7 +2060,7 @@ See divergence register **AP-59**.
**Root cause / status:** The selection-state wire was deferred out of D.5.1 scope; the meter/slider elements are present in LayoutDesc 0x21000016 but hidden (no backing data). D.5.3 is the planned port.
- **D.5.3a (2026-06-18):** the Health meter (0x100001A1) + the object-name line (0x1000019F) + the overlay state (0x100001A0) are wired via `SelectedObjectController` (port of `gmToolbarUI::HandleSelectionChanged`); `SelectionChanged` event on `GameWindow`; `QueryHealth (0x01BF)` sent on select. Spec/plan: `docs/superpowers/specs|plans/2026-06-18-d53a-*`. **Still deferred:** the Mana meter (0x100001A2 — owned-item-only; no remote-target mana path yet). Divergence row AP-46.
- **D.5.3a visual gate PASSED (2026-06-20):** name top-aligned in the bar sprite's black band, friendly NPCs/Doors name-only, players/monsters get the bar (gated on PWD BF_ATTACKABLE/BF_PLAYER), bar appears on assess/damage (UpdateHealth-driven, AP-47 retired), brief green selection flash. Fixed during the gate: the two magenta end-lines (UiMeter.DrawHBar resolved slice id 0 → 1x1 magenta placeholder → 1px caps), the stack-entry black box (hid 0x100001A3), and the flash being eaten by a framebuffer-dump diagnostic. Commits `8f627cc` (fixes), `0796585` (CLI apparatus). **Remaining for #141:** Mana meter (0x100001A2).
- **D.5.3b implementation (2026-07-11):** stacked selection formats the retail `"%d %hs"` count/name, now preserving the wire `PluralName` selected by `NAME_APPROPRIATE`; reveals authored entry `0x100001A3` and horizontal DAT slider `0x100001A4`; initializes to the full stack; clamps entry edits; honors the entry's DAT right alignment; and uses the exact 1000-step slider conversion. The thumb retains its raw pointer position so minimum/maximum reach both endpoints. One Core `StackSplitQuantityState` feeds the controls plus selected-source merge, container split (`0x0055`), and ground split (`0x0056`). `GetObjectSplitSize @ 0x00586F00` ensures unselected objects still move their full stack. Partial transfers do not optimistically move the original because ACE creates the destination object with a new guid. Warning-free App Release + 4,697 tests; controls/polish visually confirmed, partial-transfer live action gate pending.
- **D.5.3b implementation (2026-07-11):** stacked selection formats the retail `"%d %hs"` count/name, now preserving the wire `PluralName` selected by `NAME_APPROPRIATE`; reveals authored entry `0x100001A3` and horizontal DAT slider `0x100001A4`; initializes to the full stack; clamps entry edits; honors the entry's DAT right alignment; and uses the exact 1000-step slider conversion. The thumb retains its raw pointer position so minimum/maximum reach both endpoints. One Core `StackSplitQuantityState` feeds the controls plus selected-source merge, container split (`0x0055`), and ground split (`0x0056`). `GetObjectSplitSize @ 0x00586F00` ensures unselected objects still move their full stack. Partial transfers do not optimistically move the original because ACE creates the destination object with a new guid. Warning-free App Release + 4,697 tests; controls/polish, partial inventory split, and partial ground drop visually confirmed.
**Files:** `src/AcDream.App/UI/Layout/ToolbarController.cs` + the selection/WorldPicker state (see `claude-memory/project_interaction_pipeline.md`).
@ -6720,9 +6720,9 @@ Lessons: `memory/feedback_render_perf_measurement.md`.
**Component:** ui — D.2b inventory window (UiItemList scroll, UiNineSlicePanel frame, vertical resize)
The inventory window now matches retail's 2D presentation (minus the 3D paperdoll doll = Stage 2). Shipped `366af0c``1be7e65` (build + full suite green: Core.Net 334 / App 543 / UI 425 / Core 1530; spec/plan `docs/superpowers/{specs,plans}/2026-06-21-d2b-inventory-window-finish*.md`):
- **Scroll**`UiItemList` clip+scroll via the shared `UiScrollable` (whole-row clip; cells exempted from the per-frame anchor pass — that was the "grid escapes the window" root cause); gutter scrollbar `0x100001C7` bound like ChatWindowController; mouse-wheel.
- **Scroll**`UiItemList` clip+scroll via the shared `UiScrollable`; cells remain exempt from the per-frame anchor pass (the original "grid escapes the window" root cause). Issue #147 later completed pixel-clipped partial rows and refresh offset preservation; gutter scrollbar `0x100001C7` remains bound like ChatWindowController; mouse-wheel line steps remain one row.
- **Frame** — wrapped in the 8-piece bevel chrome (`UiNineSlicePanel`) like vitals/chat/toolbar.
- **Vertical resize** — bottom-edge drag, horizontal blocked (`ResizeX=false`); grid/sub-window/scrollbar/backdrop stretch (`Left|Top|Bottom`), paperdoll + side-bags pinned; scrollbar thumb reflects view/content. Expand-only Min=default..Max=560 (AP-54).
- **Vertical resize** — bottom-edge drag, horizontal blocked (`ResizeX=false`); grid/sub-window/scrollbar/backdrop stretch (`Left|Top|Bottom`), paperdoll + side-bags pinned; scrollbar thumb reflects view/content. Expand-only Min=default; issue #147 replaced the obsolete fixed 560 px maximum with available screen height (AP-54).
- **102-slot grid** — contents grid pads empty frames to the main-pack capacity (default 102, AP-53); side-bag column pads to 7 (AP-52).
**Visually confirmed** (scroll, frame, resize, 102 slots). **Remaining (handoff `docs/research/2026-06-21-d2b-inventory-finish-handoff.md`):** wrong empty-slot background art (inventory + equip slots — the OPEN issue above) + main-pack backpack icon (AP-51); Stage 2 = paperdoll `UiViewport` doll + per-slot equip silhouettes.