ui: a Slider's thumb had no rect, so no scrollbar could be skinned or seen

"Half of the info on the right is out of the frame" was a scroll frame with
699 of content in a 451 window, a scroll range of 228, and a scrollbar
with nothing on it. The thumb's rect was only ever computed inside the
draw pass and never published: the region sweep skips anchor-less regions
and a thumb is anchor-less by construction, because its slider places it.

That is invisible until something anchors to the thumb, and pfUI does --
SkinScrollbar clears the stock knob and hangs its own off the thumb's two
corners, so every pfUI-skinned scrollbar in the interface was a bare
track, which is a scroll frame that silently cannot be scrolled.

The thumb is on the layout roster now, survives the unanchored prune, and
carries its slider's (vertical, fraction) in its node hash -- without the
last one a named touch moved no hash and seeded nothing, which
WOW_LAYOUT_VERIFY caught as an incremental pass disagreeing with a full
derivation. The draw pass keeps computing its own rect: extract takes
&self and cannot resolve, so a SetValue with no resolve behind it still
has to draw where the value now is.

map: the window has two layouts and the canvas only knew the maximized one

Pressing maximize left the art a 1002x668 patch in a full-screen window
and minimize could not put it back, because the container was anchored
once at build to WorldMapPositioningGuide TOP -502,-69 -- 1.12's
maximized anchor against a fixed 1024x768 guide. Read as insets instead
(15/4/33/20 windowed, 10/12/69/31 maximized) the same numbers reproduce
1.12 at 1.12's sizes and fit any other window, which is also the left-edge
bleed: -502 against a guide stretched to pfUI's 1017-wide window starts
the art 6.5 px in, not 10.

The zoom ladder's floor follows: it was the constant 1, true only while
the container was the art's own size, and is the measured fit now. A view
outside a rebuilt ladder is pulled onto its nearest end, since ZoomIn and
ZoomOut walk it by value -- OpenMapClassic's whole ladder sits below ours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-03 14:27:51 -07:00
co-authored by Claude Opus 5
parent 41512a04ef
commit abb4dc1dd3
6 changed files with 414 additions and 19 deletions
+97
View File
@@ -2628,3 +2628,100 @@ anchor is `WorldMapFrame TOPLEFT 15,-33` at scale 0.7, which would fill 70% of h
pfUI sizes that window from the **unscaled** `WorldMapButton` — it is written for the maximized
map and he is running the windowed one. Flipping the map to maximized is the one-click test of
that reading.
## No scrollbar in the interface had a thumb anything could anchor to
grom: "half of the info on the right is out of the frame" about the character panel. It was not
out of the frame. `BCPPFUIStatsContent` is 699 tall inside a 451-tall scroll frame, with a
scrollbar and a scroll range of 228 — the information was behind a scrollbar he could not see,
could not grab, and had no reason to think was there.
A Slider's thumb texture had **no resolved rect**. Measured, with his own addons loaded:
```
bar 16x419 L=487.0 B=881.0
realthumb 16x16 L=nil B=nil
pfUIthumb 8x8 L=nil B=nil <- pfUI's own knob, anchored to the thumb
anchored-to-thumb 0x0 L=nil <- a fresh texture, same anchors, same answer
```
The rect was only ever computed inside the **draw** pass (`extract`'s thumb branch, from
`slider::thumb_rect`), never published, and the region sweep skipped the thumb outright: it
carries no anchors, because its slider places it, and anchor-less regions are not layout inputs.
A thumb that draws but has no rect is invisible for exactly as long as nobody anchors to it —
and pfUI anchors to it. `SkinScrollbar` clears the stock knob's texture and hangs its own off the
thumb's two corners (`pfUI/api/ui-widgets.lua:813-820`). Stock knob blanked, replacement
unplaceable: **every pfUI-skinned scrollbar in the interface was a bare track**, which is a scroll
frame that silently cannot be scrolled. The character panel is where he noticed; the Cine settings
page he reported two rounds ago is the same fault.
### What it took
Three things, because the thumb had fallen out of the layout at three separate points:
1. **The roster.** `if data.anchors.is_empty() { continue; }` in the region walk — the thumb is
anchor-less by construction, so it never entered the sweep. It is on the roster now.
2. **The prune.** `region_resolved.retain(…)` drops unanchored regions, which would have taken
the rect back off the thumb the moment the sweep gave it one.
3. **The node hash.** The incremental pass re-hashes only the ids a write *named*, and seeds only
those whose hash moved — "a named write that did not move the hash seeds nothing". The thumb's
hash is over its anchors, size and measured extent, and a `SetValue` moves none of them, so a
named touch alone changed nothing. The slider's `(vertical, fraction)` is now part of that
hash, for thumbs only, and `Slider:SetValue`/`SetMinMaxValues`/`SetValueStep`/`SetOrientation`
and the drag name the thumb through `touch_layout_region`.
`WOW_LAYOUT_VERIFY=1` caught stage 3 outright — it panicked with "an INCREMENTAL layout pass
disagreed with a full derivation" — which is exactly what that gate is for and the reason the
first two stages alone looked like they had worked.
**The draw pass still computes its own rect, and that is deliberate.** `extract` takes `&self` and
cannot resolve, so a `SetValue` with no resolve behind it must still draw the thumb where the
value now is; the published rect is for `GetLeft` and for anchoring, and is refreshed on the next
resolve, which the client runs every frame. Two call sites of one formula, like the StatusBar fill
beside it. Reading the published rect in `extract` instead broke
`slider_thumb_draws_at_value_fraction_along_the_track`, which is the test that says so.
Measured after, on the same scrollbar: thumb at the track top for value 0, and for value 200 of
0..228, `thumbB=930.49` against 881 + (1 − 200/228) × (419 − 16) = 930.5 by hand, with pfUI's
replacement knob 4 px inside it.
## The map window has two layouts, and the canvas only knew one
grom pressed the maximize button and "it got worse, doesn't go back to what it was". Both halves
are one bug: the canvas container was anchored once, at build, to `WorldMapPositioningGuide TOP
-502,-69` — 1.12's **maximized** anchor, against a guide that is a fixed 1024x768 frame.
1.12 lays the map art out twice and the two have nothing in common:
| | window | art anchor | art size |
|---|---|---|---|
| `WorldMapFrame_Minimize` | 720x521 | `WorldMapFrame TOPLEFT 15,-33` | 1002x668 at scale **0.7** |
| `WorldMapFrame_Maximize` | the whole screen | guide `TOP -502,-69` | 1002x668 at scale 1, inside the fixed 1024x768 guide, `BlackoutWorld` around it |
So maximize left the art a 1002x668 patch in a full-screen window, and minimize put the window
back at 720x521 with the art still where the maximized sheet had it.
Read as **insets** rather than as the one size each produces — 15/4/33/20 windowed, 10/12/69/31
maximized — the same two numbers reproduce 1.12 exactly at 1.12's sizes and do the obvious thing
at any other, which matters because pfUI resizes the window to `WorldMapButton`'s size plus its own
chrome (`pfUI/modules/map.lua:111`). That is also the answer to the left-edge bleed left open in
the previous section: it was the maximized `-502` being read against a guide stretched to a
1017-wide window, so the art started 6.5 px in instead of 10.
Two consequences had to follow it:
- **The zoom ladder's floor was the constant 1**, and the comment there said why: the canvas is
sized to the art layer and the container *was* the rect that layer filled, so 1 was the whole
map by construction. The container is the window's content area now, so the floor is measured
(`GetFitScale`) — and comes out exactly 1 when the window is 1.12's own size.
- **A view outside the new ladder is pulled onto its nearest end.** `ZoomIn`/`ZoomOut` walk the
ladder by value, so a scale outside it cannot be climbed back onto by scrolling. Both ends
happen: a window that grew raises the floor, and a canvas addon that replaces the ladder
wholesale lowers the ceiling — OpenMapClassic's stitched world is reached by zooming out far
past this file's own scale 1, so its entire ladder sits below ours. Measured with OpenMap
loaded: `minZoom=0.0125 maxZoom=0.899 current=1.0`, a view above its own ceiling.
The no-art-layer fallback changed with them: the canvas used to take the **container's** size when
a map had no art layer, which was safe only while the container was 1002x668. It is 1.12's own art
rect now in every case, and the fitted floor is what keeps it visible in a smaller window.