Comments in benilla-ui: one line of fact each, no history, no record numbers (#143)

Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161.

- **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal.
- **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them:
  - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one.
  - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`.
  - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale.
  - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent.
  - A StatusBar fills from its internal normalised fraction, not from `GetValue()`.
  - The addresses of `StartDuel` and `StartDuelUnit` were swapped.
  - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default.
  - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names.
- **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12.
- **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference.

Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-25 01:58:49 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8505187668
commit fd386e75ae
233 changed files with 14108 additions and 42094 deletions
+46 -143
View File
@@ -1,32 +1,9 @@
//! The `ScrollFrame` method surface (`CSimpleScrollFrame`) — the ScrollFrame mechanism (decision
//! 0112, the engine's last structural gap): `SetScrollChild`/`GetScrollChild`, the two scroll
//! offsets (`Set`/`GetVerticalScroll`, `Set`/`GetHorizontalScroll`), and the live ranges
//! (`Get{Vertical,Horizontal}ScrollRange`/`UpdateScrollChildRect`). The offset setters and the
//! ranges are byte-pinned (decisions 1338, 2017): `0x786db0` stores the offset VERBATIM — the
//! engine never clamps it — and `0x786e30` measures both ranges off the scroll child's subtree. The
//! rest is spec-faithful to the documented widget contract (the Era `ScrollFrameTemplate` Lua
//! drives its scrollbar off exactly these), same posture as StatusBar's fill.
//!
//! **The two axes are one mechanism, not a mechanism and a special case.** The reference's nine
//! ScrollFrame bindings are four symmetric pairs plus `UpdateScrollChildRect`, and the C++ under
//! them is literally cloned: `0x786d30` (horizontal) is the byte-clone of `0x786db0` (vertical)
//! against `[+0x324]`/`[+0x32c]` instead of `[+0x328]`/`[+0x334]`, and one `0x786e30` computes
//! both ranges out of one subtree union. This module
//! is written that way too — one helper per concept, taken on the axis asked for. That the
//! horizontal half went unwritten until the corpus asked for it (`aux-addon` reads and writes both
//! axes in one function) is why this doc used to say horizontal scroll "is out of scope: no 1.12
//! template drives it" — true of the stock templates, and never true of the addons.
//!
//! The actual geometry — the scroll child's anchors pinned to the frame + the scroll offsets, and
//! the clip every descendant of the child draws/hits within — lives in [`super::mod@super`]'s
//! `resolve`/`extract`/`hit_test` (the layout-graph override + the ancestor-walk clip helper); this
//! module is the thin Lua binding over the per-frame [`ScrollFrameState`], plus the three script
//! hooks (`OnVerticalScroll`/`OnHorizontalScroll`/`OnScrollRangeChanged`) a scrollbar's
//! `OnValueChanged`/`OnLoad` wires to.
//!
//! The methods live in their own registry table, consulted by the frame `__index` dispatcher only
//! for ScrollFrame frames — so duck-typing addons (`if frame.SetScrollChild then …`) see `nil` on
//! every other kind, exactly as against the client's per-class method sets.
//! The `ScrollFrame` methods (`CSimpleScrollFrame`): the scroll child, the two offsets, the two
//! ranges and `UpdateScrollChildRect`, over [`ScrollFrameState`]; the layout's `resolve`,
//! `extract` and `hit_test` place and clip the child. The axes are one mechanism: `0x786d30`
//! (horizontal) is a byte-clone of `0x786db0` (vertical) on `[+0x324]`/`[+0x32c]` for
//! `[+0x328]`/`[+0x334]`, and `0x786e30` measures both ranges in one pass. The methods sit in
//! their own table, so on every other frame kind `SetScrollChild` is nil, as in the client.
use mlua::{Lua, Table, Value};
@@ -34,12 +11,11 @@ use super::object::{decode_id, frame_handle_of, frame_wrapper};
use super::{event, Model};
use crate::widget::{FrameHandle, KindState, ScrollFrameState};
/// Registry key of the ScrollFrame method table (the MAXCSTACK discipline: Lua-side root, named key).
/// Registry key of the ScrollFrame method table.
pub(super) const REG_SCROLLFRAME_METHODS: &str = "__benilla_scrollframe_methods";
/// Run `f` over a frame's ScrollFrame state under one short write borrow. Errors if `this` is not a
/// live ScrollFrame (unreachable through the kind dispatcher, but the method table is a plain Lua
/// value — a caller could fish it out and misapply it).
/// Run `f` over a frame's ScrollFrame state; errors when `this` is not a live ScrollFrame, which a
/// script reaches only by applying a method taken off the table to another frame.
fn with_scroll<T>(
lua: &Lua,
this: &Table,
@@ -57,47 +33,21 @@ fn with_scroll<T>(
}
}
/// Which axis a scroll offset, span or range is taken on.
///
/// One type rather than two code paths, because the reference computes both ranges in ONE pass
/// over ONE subtree union (`0x786e30`: `[+0x31c] = max(0, maxRIGHT − minLEFT − width) / scale`,
/// `[+0x320] = max(0, maxTOP − minBOTTOM − height) / scale`) — the axes differ only in which pair
/// of edges the union tracks and which frame dimension is subtracted.
/// The axis of an offset or a range. `0x786e30` computes both ranges in one pass over one subtree
/// union: `[+0x31c] = max(0, maxRIGHT − minLEFT − width) / scale`, and `[+0x320]` the same with
/// top, bottom and height.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Axis {
/// `maxRIGHT − minLEFT` against the frame's width — `GetHorizontalScrollRange`, `[+0x31c]`.
Horizontal,
/// `maxTOP − minBOTTOM` against the frame's height — `GetVerticalScrollRange`, `[+0x320]`.
Vertical,
}
/// `Get{Vertical,Horizontal}ScrollRange()`'s live computation: `max(0, contentExtent − frameExtent)`
/// from the **resolved** rects — `0.0` when the frame is unresolved or there is no child. Never
/// cached: the scroll offset always clamps against the current layout, not a stale snapshot.
///
/// **The content extent is the scroll child's whole SUBTREE, not the child frame's own height**
/// (decision 1338 corrects what 0112 shipped). `0x786e30` seeds a bounding box
/// `{FLT_MAX, FLT_MAX, 0, 0}` and calls the **recursive** `0x786f80(scrollChild, &bbox)`, which
/// unions the frame's REGION list (head `+0x1b4`, link `+0x1b8`, guard `[entry+8]+0xc4`) and
/// re-enters itself for every CHILD FRAME (head `+0x2fc`, link `+0x300`, guard `[entry+8]+0xd0`);
/// the range stored at `[+0x31c]` is `max(0, bboxExtent − viewportExtent)`.
///
/// The distinction is not academic: stock `ItemTextPageScrollChild` is declared **10×10**
/// (`ItemTextFrame.xml` l.198) around a 270×304 SimpleHTML whose blocks extend well past it.
/// Measured as the child's own height the range is zero and a book cannot be scrolled at all;
/// measured as the subtree box it tracks the rendered text. Every ScrollFrame whose child is sized
/// directly — the guild-info EditBox, the options panes, every faux list — is unchanged, because
/// there its own rect already IS its subtree.
///
/// In **LOCAL units**, each extent divided by the owning frame's effective scale — i.e. exactly
/// `child:GetHeight() - self:GetHeight()`, the widget contract this method states. The resolved
/// rects are screen px, and the scroll offset is NOT: `SetVerticalScroll` becomes the scroll
/// child's anchor y-offset, which the solver multiplies by that child's scale (`resolve`'s
/// override; `anchor_resolve_y`). Reporting a screen-px range against a local-unit offset makes
/// every SCALED ScrollFrame under-scroll by exactly its scale — the bar's max is the range, so it
/// stops short of the end and the tail of the content is unreachable. Invisible at scale 1.0,
/// which is every 1.12-native window; the era-scaled options window (ERA_WINDOW_SCALE 0.78) is the
/// first real-scroll customer that isn't, and it lost 22% of its travel.
/// `Get{Vertical,Horizontal}ScrollRange()`: `max(0, content − frame)` from the resolved rects,
/// computed on every call, where the reference caches both (`[+0x31c]`, `[+0x320]`) and recomputes
/// them only once marked dirty. The content is the scroll child's whole subtree, not its own rect
/// (`0x786e30` calls the recursive `0x786f80`): stock `ItemTextPageScrollChild` is 10×10 around a
/// 270×304 SimpleHTML (`ItemTextFrame.xml:198`). Each extent is divided by its frame's effective
/// scale, since the offset is in local units; a range in screen px under-scrolls a scaled frame.
fn scroll_range(model: &Model, h: FrameHandle, axis: Axis) -> f32 {
let child = match model.arena.frame(h).map(|f| &f.kind_state) {
Some(KindState::Scroll(s)) => s.child,
@@ -127,13 +77,9 @@ fn scroll_range(model: &Model, h: FrameHandle, axis: Axis) -> f32 {
((far - near) / scale_of(child) - viewport).max(0.0)
}
/// The span of `f`'s subtree along `axis`, in SCREEN px, as `(far, near)` — `(top, bottom)`
/// vertically, `(right, left)` horizontally — its own resolved rect unioned with every VISIBLE
/// descendant region and child frame, recursively (`0x786f80`). `None` when nothing in the subtree
/// resolved.
///
/// Visibility is the client's own guard on both lists: a hidden block contributes no scroll range,
/// which is what stops a window's parked art from inventing travel nobody can use.
/// The span of `f`'s subtree along `axis` in screen px, as `(far, near)`: its own rect unioned with
/// its shown regions and visible child frames, recursively (`0x786f80`). Hidden parts add no range:
/// the client guards both lists on visibility.
fn subtree_span(model: &Model, f: FrameHandle, axis: Axis) -> Option<(f32, f32)> {
let edges = |r: &crate::layout::Rect| match axis {
Axis::Horizontal => (r.right, r.left),
@@ -193,14 +139,12 @@ fn subtree_font_strings(model: &Model, f: FrameHandle) -> Vec<crate::widget::Reg
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let m = lua.create_table()?;
// SetScrollChild(frame|name|nil) — a wrapper table or a name string resolves like every other
// frame-target arg (SetParent, SetPoint's relativeTo); nil (or an unresolvable target) clears.
// SetScrollChild(frame | name | nil): nil or an unresolvable target clears.
m.set(
"SetScrollChild",
lua.create_function(|lua, (this, target): (Table, Value)| {
// `_G[name]` before the guard, no `$parent` — `0x790fa0`'s string arm calls `0x76c760`
// directly, the same narrow-Frame-tag resolver `SetParent` uses
// (`object::NamedTarget`).
// A name is `_G[name]` with no `$parent`: `0x790fa0` calls `SetParent`'s resolver
// `0x76c760`.
let named = match &target {
Value::String(s) => s
.to_str()
@@ -227,8 +171,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
changed
})?;
if changed {
// The child override is part of the resolve's read set (decision 0112's local
// anchor map — a fingerprint input), so a real re-target dirties tier 1.
// The child is an input of the layout resolve, so a real change dirties it.
lua.app_data_mut::<Model>().expect("model").touch_layout();
}
Ok(())
@@ -251,20 +194,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// SetVerticalScroll(px) — stores the offset VERBATIM and, only when it actually changed,
// re-lays the child out and fires OnVerticalScroll(self, offset) — the scrollbar's
// OnValueChanged wiring's other half.
//
// **No clamp** (decision 2017). The reference's `0x786db0` compares the new value against the
// OLD one alone (an epsilon change-gate: unchanged → nothing happens, not even the script),
// writes it to `[+0x328]`, re-anchors the scroll child off it (`0x787100` →
// `SetPoint(child, self, hScroll, vScroll)`) and fires the handler. `[+0x320]` — the range —
// is never read on this path. Every clamp the reference exhibits is FrameXML's own, through
// the scroll bar's `[min, max]` (`ScrollFrameTemplate_OnMouseWheel`,
// `ScrollFrame_OnScrollRangeChanged`). The clamp this carried from 07-05 (a3fce6a76) to 2017
// cost every faux list whose frame is taller than `rows × step` its last rows (B370): the
// reference's `FauxScrollFrame_Update` drives the bar to `(n − rows) × step`, which is past
// `n × step − frameHeight` whenever the frame has slack under its last row.
// SetVerticalScroll(px) (`0x786db0`): stores the offset unclamped (`[+0x328]`), re-anchors the
// child (`0x787100`) and fires `OnVerticalScroll(self, offset)`, all only on a change; the
// reference skips a change under 2⁻²² from the old offset, where this compares bits. The range
// is never read here: every clamp is FrameXML's, through the scroll bar's min and max.
m.set(
"SetVerticalScroll",
lua.create_function(|lua, (this, px): (Table, f32)| {
@@ -293,22 +226,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// ── The horizontal axis: the same three verbs on `[+0x324]`/`[+0x31c]` ─────────────────────
//
// `SetHorizontalScroll(px)` `0x7912f0` → `0x786d30`, which is the BYTE-CLONE of
// `SetVerticalScroll`'s `0x786db0` against the horizontal fields.
// So every law stated above holds verbatim on this axis: no clamp, the same 2⁻²² change-gate
// against the OLD offset alone, the same re-anchor, and `OnHorizontalScroll(self, offset)`
// fired only on a real change. Nothing here is a horizontal special case.
//
// The one thing that IS specific to this axis is the SIGN, and it is the reference's:
// `0x787100` passes both offsets to `SetPoint(TOPLEFT, self, TOPLEFT, +hScroll, +vScroll)`
// unnegated, so positive x pushes the child right (revealing nothing) where positive y lifts it
// (revealing content below). `aux-addon/tabs/search/filter.lua:315-322` — the corpus caller
// that made this pair worth writing, reading both axes in one line and writing both in the next
// — bounds x into `[min(0, frameWidth - contentWidth - 10), 0]` and y into
// `[0, contentHeight - frameHeight]`, which is the same asymmetry read off the other side of
// the API. See [`ScrollFrameState::horizontal`].
// ── The horizontal axis ──
// SetHorizontalScroll(px) (`0x7912f0` → `0x786d30`) is the vertical setter's byte-clone on
// `[+0x324]`, firing `OnHorizontalScroll`. The sign is the reference's: `0x787100` anchors the
// child at `(+hScroll, +vScroll)`, so a positive x moves it right, where a positive y lifts it.
m.set(
"SetHorizontalScroll",
lua.create_function(|lua, (this, px): (Table, f32)| {
@@ -336,28 +257,17 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(scroll_range(&model, h, Axis::Horizontal))
})?,
)?;
// UpdateScrollChildRect() — the Era template calls this after resizing its content to re-drive
// its scrollbar. Our range is always live (never cached) against the RESOLVED rects — but a
// script resize (`child:SetHeight(...)`) lands in `LayoutInput` immediately, while `resolved`
// only catches up on the app's own next `resolve()` call (Lua runs before that each tick,
// `ui_script/mod.rs`'s drive loop). This is precisely the Era idiom this method exists to
// serve — resize the content, then immediately ask for the new range — so a stale `resolved`
// would make its very own primary caller see a one-tick-old (often wrong-by-a-lot, e.g. still
// "nothing to scroll") answer. Force a fresh resolve first: cheap on a quiet graph (the
// fixpoint's own doc — an unrelated resize elsewhere converges in one round), and correctness
// here matters more than dodging a resolve pass a caller who wants nothing stale already asked
// for by name.
// UpdateScrollChildRect(): recomputes both ranges and fires `OnScrollRangeChanged` at once,
// where the reference only marks them dirty (`0x791943`) for its next pump, which fires on a
// change. A script resize reaches the resolved rects only at the app's next resolve, so this
// resolves first.
m.set(
"UpdateScrollChildRect",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
// The reference derives the range from a child whose text is already laid out — its
// measure is synchronous. Ours measures on the metric reads and on the host's
// round-trip, which lands a frame later; stock `QuestLog_UpdateQuestDetails` calls
// this right after the SetTexts that fill the pane, and read a range of 0 (1944). So
// the child's FontStrings are measured here, through the installed measurer, before
// the layout that the range reads; with no measurer installed the round-trip fills
// them as before.
// The reference measures text synchronously and ours lands a frame later, so the
// child's FontStrings are measured first: `QuestLog_UpdateQuestDetails` calls this
// right after its SetTexts (`QuestLogFrame.lua:458`).
let strings: Vec<crate::widget::RegionHandle> = {
let model = lua.app_data_ref::<Model>().expect("model app_data");
match model.arena.frame(h).map(|f| &f.kind_state) {
@@ -378,17 +288,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let y_range = scroll_range(&model, h, Axis::Vertical);
(model.frame_id(h), x_range, y_range)
};
// That forced resolve can have MOVED sizes — this method exists precisely because the
// caller just resized its content — so drain the queue before the range notify: the
// reference fires `OnSizeChanged` from `ApplyRect`, i.e. during the layout, not after.
// The resolve can move sizes, and the reference fires `OnSizeChanged` during layout, so
// those fire before the range notify.
event::fire_size_changes(lua);
// `OnScrollRangeChanged(self, xRange, yRange)` — **horizontal first**, and that order is
// byte-derived rather than assumed: at `0x786eff` the vertical result is on top of the
// x87 stack, is `fstp`'d first and therefore pushed DEEPEST, so it lands as `arg2`. It
// is why stock `UIPanelScrollFrameTemplate` hands `arg2` to
// `ScrollFrame_OnScrollRangeChanged`.
// `arg1` was a hardcoded `0.0` here for as long as there was no horizontal range to
// put in it.
// `OnScrollRangeChanged(self, xRange, yRange)`: horizontal first (`0x786eff`), which is
// why stock passes `arg2` on as the vertical range (`UIPanelTemplates.xml:188`).
if let Err(e) = event::fire_widget_handler(
lua,
id,
@@ -410,9 +314,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(())
}
/// Fire `On{Vertical,Horizontal}Scroll(self, offset)` (the ScrollFrame's own script slots — the
/// reference's `[+0x334]`/`[+0x32c]`, fired from the setter clones `0x786db0`/`0x786d30`). Fired
/// outside any model borrow; errors go to [`Model::errors`] like every other widget handler.
/// Fire `On{Vertical,Horizontal}Scroll(self, offset)`, the reference's script slots `[+0x334]` and
/// `[+0x32c]`, outside any model borrow.
fn fire_scroll(lua: &Lua, this: &Table, script: &str, value: f32) -> mlua::Result<()> {
let id = {
let h = frame_handle_of(lua, this)?;