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:
co-authored by
Claude Opus 5.5
parent
8505187668
commit
fd386e75ae
@@ -1,21 +1,8 @@
|
||||
//! The `Button`/`CheckButton` method surface — per-kind behavior over the frame arena
|
||||
//! (`CSimpleButton` `0x6eeab0` / `CSimpleCheckbox` `0x6eeb30`).
|
||||
//!
|
||||
//! Grounded in the client's LoadXML table (`0x7788c0`): the four state textures
|
||||
//! (`Normal/Pushed/Disabled/Highlight`), the `ButtonText` fontstring + `text` attribute, the
|
||||
//! `OnClick` script slot (`+0x4cc`); CheckButton runs Button's loader first and adds
|
||||
//! `CheckedTexture`/`DisabledCheckedTexture` + the `checked` bool (`+0x4dc`). Which texture *shows*
|
||||
//! is **latched on the transition**, not resolved at paint: [`ButtonState::set_state`] is the
|
||||
//! client's `SetState 0x779790` and [`settle`] is where the derived inputs reach it. Two stated v1
|
||||
//! gaps: the highlight draws with normal blending (the client ADD-blends it; the quad pass has no
|
||||
//! blend modes yet), and `PushedTextOffset` is not modeled. Per-state label fonts *are*: the
|
||||
//! `*FontObject` trio picks which object each state inherits and `extract` re-resolves it every
|
||||
//! frame, while `SetFont` writes the button's own face/size/flags over all of them
|
||||
//! ([`crate::widget::ButtonFont`]).
|
||||
//!
|
||||
//! Method resolution: CheckButton's table is consulted first, then Button's, then the shared frame
|
||||
//! table — mirroring the client's class chain, and keeping duck-typing honest (`frame.SetChecked`
|
||||
//! is nil on a plain Button; `frame.GetText` is nil on a plain Frame).
|
||||
//! The `Button` and `CheckButton` methods (`CSimpleButton` `0x6eeab0`, `CSimpleCheckbox`
|
||||
//! `0x6eeb30`). The shown state texture latches at each state edge (`SetState` `0x779790`), not at
|
||||
//! paint. `<PushedTextOffset>` is not built: a pushed label does not shift.
|
||||
//! Lookup runs CheckButton's table, then Button's, then the frame table, the client's class chain,
|
||||
//! so `SetChecked` is nil on a plain Button and `GetText` on a plain Frame.
|
||||
|
||||
use mlua::{Lua, MultiValue, ObjectLike, Table, Value};
|
||||
|
||||
@@ -32,7 +19,7 @@ use crate::widget::{
|
||||
pub(super) const REG_BUTTON_METHODS: &str = "__benilla_button_methods";
|
||||
pub(super) const REG_CHECKBUTTON_METHODS: &str = "__benilla_checkbutton_methods";
|
||||
|
||||
/// The Button state-texture/text slots (each an arena region created on first set).
|
||||
/// A Button's texture and label slots, each an arena region created on first set.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Slot {
|
||||
Normal,
|
||||
@@ -57,10 +44,8 @@ impl Slot {
|
||||
}
|
||||
}
|
||||
|
||||
/// The three STATE slots go through [`ButtonState::set_state_slot`] — the client's
|
||||
/// `0x778fd0` family, which stores the slot *and* pushes it to the shown pointer when it is
|
||||
/// the current state's. Every other slot is a plain field: the highlight, the checked pair
|
||||
/// and the label are not in the state array and have show rules of their own.
|
||||
/// The three state slots go through [`ButtonState::set_state_slot`] (`0x778fd0`), which also
|
||||
/// shows the texture when it is the current state's; the others are plain fields.
|
||||
fn set(self, bs: &mut ButtonState, rh: Option<crate::widget::RegionHandle>) {
|
||||
match self {
|
||||
Slot::Normal => bs.set_state_slot(ButtonVisualState::Normal, rh),
|
||||
@@ -73,8 +58,8 @@ impl Slot {
|
||||
}
|
||||
}
|
||||
|
||||
/// The slot's region kind + default draw layer: state textures under the text, the highlight
|
||||
/// in its own top layer (drawLayer table `0x811a84`), the checked marks above the state.
|
||||
/// The region kind and default layer: state textures under the label, the highlight in its own
|
||||
/// layer (the draw-layer table `0x811a84`), the checked marks above the state.
|
||||
fn shape(self) -> (RegionKind, DrawLayer) {
|
||||
match self {
|
||||
Slot::Normal | Slot::Pushed | Slot::Disabled => {
|
||||
@@ -87,11 +72,8 @@ impl Slot {
|
||||
}
|
||||
}
|
||||
|
||||
/// The NORMAL embedded font's justify word — `[button+0x390]` (`+0x33c`, the normal `CSimpleFont`,
|
||||
/// plus its `+0x54` justify cell): the element-level `<NormalFont justifyH=>` when the template
|
||||
/// wrote one (a local write on that instance, severed from what it inherits), else what the
|
||||
/// instance inherits from its font object, else the ctor default (CENTER). The word the label
|
||||
/// adopter anchors by — decision 1996.
|
||||
/// The normal font's justify (`[button+0x390]`): its own `<NormalFont justifyH=>`, else its font
|
||||
/// object's, else CENTER. The label adopter anchors by it.
|
||||
fn normal_font_justify(model: &Model, bs: &ButtonState) -> Justify {
|
||||
let mut word = Justify::default();
|
||||
let inherited = || {
|
||||
@@ -106,19 +88,9 @@ fn normal_font_justify(model: &Model, bs: &ButtonState) -> Justify {
|
||||
word
|
||||
}
|
||||
|
||||
/// `CSimpleButton::SetFontString 0x778d20`'s tail — the ONE path a button label enters by,
|
||||
/// whether `SetText` made it (`0x778dc0` allocates, then calls this) or Lua handed one over
|
||||
/// (`SetFontString`): anchor it to the button **only if it has no anchor of its own** and **by the
|
||||
/// NORMAL font's justify word** (`[button+0x390]`: LEFT→LEFT, RIGHT→RIGHT, else CENTER — never
|
||||
/// the string's own `+0x120`, which on a fresh string is the ctor's CENTER), then apply the
|
||||
/// button's per-state font to it on the spot (`0x779810`).
|
||||
///
|
||||
/// The word's source is the whole bug this fixes (decision 1996): a `UIMenuButtonTemplate` row
|
||||
/// has no `<ButtonText>` — its label is born from the reference's `UIMenu_AddButton` →
|
||||
/// `button:SetText(text)` — and its `<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>` is
|
||||
/// the only thing that puts the label at the row's left edge. Reading the fresh string's own
|
||||
/// word instead seated every row CENTER, and with the shortcut string anchored RIGHT in a fixed
|
||||
/// 104-wide row, "Macro" ran into "/macro".
|
||||
/// `SetFontString`'s tail (`0x778d20`), which `SetText` also creates its label through
|
||||
/// (`0x778dc0`): an unanchored label anchors LEFT, RIGHT or CENTER by the normal font's justify,
|
||||
/// never its own (`+0x120`, CENTER on a fresh string), then takes the per-state font (`0x779810`).
|
||||
fn adopt_label(model: &mut Model, owner: FrameHandle, rh: RegionHandle) {
|
||||
let point = {
|
||||
let Some(frame) = model.arena.frame(owner) else {
|
||||
@@ -133,34 +105,12 @@ fn adopt_label(model: &mut Model, owner: FrameHandle, rh: RegionHandle) {
|
||||
apply_normal_font(model, owner);
|
||||
}
|
||||
|
||||
/// Point the ButtonText at the button's NORMAL font — object and local justify — so the label's
|
||||
/// *query* surface answers what its paint already shows. The live link the reference's per-state
|
||||
/// applier `0x779810` → `0x770c60` installs: the instance's field block flows onto the label
|
||||
/// behind the label's own severance mask.
|
||||
///
|
||||
/// Before this, the two disagreed. `extract` resolves the per-state font object every frame and
|
||||
/// overlays it onto a CLONE of the region's data (`extract.rs` l.114-122), so the label painted
|
||||
/// correctly while `region_data.font_object` stayed `None` — and a dropdown row's
|
||||
/// `GetFontObject()` answered **nil**, `GetFont()` answered **nil, nil**. Nothing errored: a
|
||||
/// FontString with no font of its own is a legal state, so this was invisible in exactly the way
|
||||
/// 1205's silent-drop class predicts.
|
||||
///
|
||||
/// The measured consequence is smaller than it first looks, and that is stated rather than implied.
|
||||
/// The corpus's 65 `GetFontObject` sites are on FontStrings the addon created and linked itself
|
||||
/// (Dewdrop-2.0 calls `text:SetFontObject(GameFontHighlightSmall)` one line before it reads the
|
||||
/// object back), so none of them were reaching this. What WAS reaching it is the reference's own
|
||||
/// `DropDownList1` OnLoad, which derives `UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT` from
|
||||
/// `DropDownList1Button1NormalText:GetFont()` and got nil.
|
||||
///
|
||||
/// **Normal only, deliberately.** The highlight and disabled objects are transient states that
|
||||
/// extract overlays at paint time; linking either here would make a resting button report its hover
|
||||
/// font. The remaining divergence, stated: while a button is disabled or hovered, the reference's
|
||||
/// label reports that state's font and ours still reports the normal one. The paint is unaffected
|
||||
/// either way, and no corpus site reads a label's font mid-state.
|
||||
///
|
||||
/// `font::repaint` honours the severance mask (`CSimpleFontString+0xd4`), so a `<FontHeight>` or
|
||||
/// `SetTextColor` the label set for itself survives the link, and the
|
||||
/// local justify goes behind the same mask.
|
||||
/// Link the label to the normal font object and local justify, the reference's `0x779810` →
|
||||
/// `0x770c60`, so its `GetFont` answers what it paints; the stock `DropDownList1` reads it
|
||||
/// (`UIDropDownMenu.xml:11`). Deviation: a hovered or disabled label still reports the normal
|
||||
/// font, since the state fonts are overlaid at paint and a link made here would outlive the state.
|
||||
/// `font::repaint` keeps what the label set itself (its severance mask, `CSimpleFontString+0xd4`),
|
||||
/// and the local justify obeys the same mask.
|
||||
fn apply_normal_font(model: &mut Model, owner: FrameHandle) {
|
||||
let (rh, name, local_justify) = {
|
||||
let Some(frame) = model.arena.frame(owner) else {
|
||||
@@ -174,8 +124,7 @@ fn apply_normal_font(model: &mut Model, owner: FrameHandle) {
|
||||
};
|
||||
(rh, bs.normal_font.clone(), bs.normal_justify_h)
|
||||
};
|
||||
// An unregistered name is not an error here: `SetTextFontObject` already accepted it, and the
|
||||
// loader's own log-and-continue rule (0068) owns the reporting.
|
||||
// An unregistered name is no error here: the setter accepted it and the loader reports it.
|
||||
let fo = name.as_deref().and_then(|n| model.font_object(n).cloned());
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
match (&name, fo) {
|
||||
@@ -202,14 +151,9 @@ pub(crate) enum LabelFont {
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// `<NormalFont justifyH=>` / `<HighlightFont justifyH=>` / `<DisabledFont justifyH=>` — the
|
||||
/// loader-facing write of a per-state embedded font's own justify (`CSimpleButton::LoadXML
|
||||
/// 0x7788c0` → the `<Font>` loader `0x783c30` on `+0x33c`/`+0x3b8`/`+0x434`: a `<Font>`-typed
|
||||
/// element, so the attribute lands on the instance itself, severed from whatever it inherits).
|
||||
/// No 1.12 Lua verb writes this — the API trio sets the OBJECT each instance inherits, never its
|
||||
/// local fields — which is why it is crate-internal rather than a method. A label already
|
||||
/// adopted keeps its anchor (the reference decided that at adoption) while its own justify word
|
||||
/// follows the normal instance, as the live link propagates the write.
|
||||
/// The loader's `<NormalFont justifyH=>` and kin: `LoadXML` (`0x7788c0`) runs the `<Font>` loader
|
||||
/// (`0x783c30`) on the embedded font, so the justify is its own, not inherited; no Lua verb
|
||||
/// writes it. An adopted label keeps its anchor and takes the new normal justify.
|
||||
pub(crate) fn set_label_font_justify_h_lua(
|
||||
lua: &Lua,
|
||||
wrapper: &Table,
|
||||
@@ -229,14 +173,8 @@ pub(crate) fn set_label_font_justify_h_lua(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `f` over a frame's Button state under one short write borrow.
|
||||
///
|
||||
/// **It no longer settles a state machine afterwards, because there is no longer one to settle**
|
||||
/// (decision 2134). The button's state is a latch the engine's own edges write; a Lua write moves
|
||||
/// it only when the write IS an edge (`Enable`/`Disable`, `SetButtonState`), and those call the
|
||||
/// edge themselves. `RegisterForClicks` and the texture setters no longer disturb the state at
|
||||
/// all — which is the reference's behaviour: re-registering a held button's clicks does not
|
||||
/// un-press it.
|
||||
/// Run `f` over a frame's Button state under one short write borrow. It moves no state, only an
|
||||
/// edge does: re-registering a held button's clicks does not un-press it, as in the reference.
|
||||
fn with_button<T>(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
@@ -254,53 +192,11 @@ fn with_button<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/// `Enable()` / `Disable()` — and the second thing they do, which is not the state texture.
|
||||
///
|
||||
/// `Disable 0x77ffd0` ends `0x78009a call [vtbl+0x90](0)`, reaching the shared helper
|
||||
/// `0x779160`; the constructor reaches the same helper with `1` (`0x778766`), which is what puts
|
||||
/// a fresh button in NORMAL. That helper does **two** things:
|
||||
///
|
||||
/// - `[vtbl+0x9c](state, 0)` = `SetButtonState 0x779790` — [`ButtonState::set_enabled`]'s half,
|
||||
/// whose two early-outs (nothing happens if the button is already in the state being asked for)
|
||||
/// are the helper's own; and
|
||||
/// - `0x7791bb push 4; call 0x76a730` — the per-layer enable for layer **4 = HIGHLIGHT**, written
|
||||
/// into the very `[frame+0x198]` array that `Enable/DisableDrawLayer` writes and that
|
||||
/// `0x76b3a0` reads back at draw time.
|
||||
///
|
||||
/// So a disabled button's highlight is switched off **at the layer**, which takes every region
|
||||
/// the frame owns there with it — not only the HighlightTexture. That is why the highlight is no
|
||||
/// longer gated on `enabled` inside [`ButtonState::region_visible`]: one mechanism, in the place
|
||||
/// the client keeps it, instead of a second rule that agreed with it on the common case and
|
||||
/// disagreed on `<Layer level="HIGHLIGHT">` art the button did not put there itself.
|
||||
///
|
||||
/// **The restore is NOT symmetric, and this comment used to claim it was** — *"one array and one
|
||||
/// writer, so an addon's `DisableDrawLayer("HIGHLIGHT")` is undone by the next `Enable()`"*. Both
|
||||
/// halves are false (`0x779160`). The indexed form
|
||||
/// `[reg + 4*reg + 0x198]` has **three** sites image-wide — `0x76a717` (=1), `0x76a737` (=0) and
|
||||
/// `DrawLayer 0x76b3a6`'s read — so there are **two** writers; and the enable arm calls
|
||||
/// *neither* helper. It reaches the ON one only one hop away and only when the button is the
|
||||
/// frame under the cursor:
|
||||
///
|
||||
/// ```text
|
||||
/// 779183 mov ecx,[esi+0xa0] ; the CSimpleTop singleton
|
||||
/// 779189 cmp esi,[ecx+0x7c] ; am I the hover target?
|
||||
/// 77918c jne 0x7791ce ; no -> return, layer untouched
|
||||
/// 779192 call [edx+0x4c] ; = 0x779490 -> 0x76b6a0 -> 0x76b6c9 push 4; call 0x76a710
|
||||
/// ```
|
||||
///
|
||||
/// — i.e. `Enable()` on a hovered button restores the layer **by re-running the frame's Lua
|
||||
/// `<OnEnter>`**, and on a non-hovered one restores nothing (the next hover-enter does it). Both
|
||||
/// restore paths are gated on the LockHighlight flag `[frame+0xf8]`, and the disable arm's
|
||||
/// `0x7791bb call 0x76a730(4)` is the one site in the image that is *not* — so
|
||||
/// `LockHighlight()` → `Disable()` → `Enable()` leaves the layer off in the reference, and only
|
||||
/// `UnlockHighlight()`+`LockHighlight()` or an explicit `EnableDrawLayer` brings it back.
|
||||
///
|
||||
/// **Ours is the symmetric bit-flip**, which draws the same in the ordinary case — a highlight is
|
||||
/// only visible while hovered, and every hover-enter sets it — and diverges in two script-visible
|
||||
/// ways: we do not fire the `<OnEnter>` the reference synthesises when you `Enable()` a hovered
|
||||
/// button, and we restore a locked-then-disabled highlight the reference leaves dark. Named here
|
||||
/// rather than fixed, because the faithful shape needs the layer restore moved onto the hover
|
||||
/// path where the reference keeps it.
|
||||
/// `Enable()`/`Disable()`. `Disable` (`0x77ffd0`) reaches `SetEnabled` (`0x779160`), which also
|
||||
/// turns draw layer 4, HIGHLIGHT, off (`0x7791bb`) in the array `DisableDrawLayer` writes, so every
|
||||
/// region in that layer goes dark, not only the HighlightTexture. `Enable()` here turns it back
|
||||
/// on; the reference restores it only on a hovered button, by re-running its `<OnEnter>`
|
||||
/// (`0x779183`), and never while `LockHighlight` holds (`[frame+0xf8]`).
|
||||
fn set_enabled(lua: &Lua, this: &Table, on: bool) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
@@ -308,9 +204,6 @@ fn set_enabled(lua: &Lua, this: &Table, on: bool) -> mlua::Result<()> {
|
||||
.arena
|
||||
.frame_mut(h)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
// The state half is [`ButtonState::set_enabled`], whose two early-outs are the client's:
|
||||
// `Enable()` on a button that is not DISABLED does nothing at all, and neither does
|
||||
// `Disable()` on one that already is.
|
||||
match &mut frame.kind_state {
|
||||
KindState::Button(bs) => bs.set_enabled(on),
|
||||
_ => return Err(mlua::Error::runtime("not a Button")),
|
||||
@@ -324,15 +217,9 @@ fn set_enabled(lua: &Lua, this: &Table, on: bool) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run one of the engine's own state edges over frame `h`, if it is a live Button.
|
||||
///
|
||||
/// The reference's edge set is exactly seven `SetButtonState 0x779790` call sites and **six of
|
||||
/// them are engine-side**: Enable, Disable, the hide
|
||||
/// notify, mouse-down, mouse-up and the drag-threshold crossing. There is **no enter or leave
|
||||
/// edge** — which is why the pointer path no longer calls anything here when the cursor crosses a
|
||||
/// button's boundary (decision 2134).
|
||||
///
|
||||
/// Harmless on a non-Button handle, and on a stale one.
|
||||
/// Run an engine state edge over frame `h` if it is a live Button. The reference has six, all of
|
||||
/// `0x779790`'s call sites but Lua's: enable, disable, hide, press, release and drag start; the
|
||||
/// cursor entering or leaving a button is not one.
|
||||
pub(super) fn edge(model: &mut Model, h: FrameHandle, f: impl FnOnce(&mut ButtonState)) {
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if let KindState::Button(bs) = &mut frame.kind_state {
|
||||
@@ -341,37 +228,12 @@ pub(super) fn edge(model: &mut Model, h: FrameHandle, f: impl FnOnce(&mut Button
|
||||
}
|
||||
}
|
||||
|
||||
/// Does a hover edge on frame `h` reach the **base** enter/leave notify — the HIGHLIGHT layer and
|
||||
/// the Lua `<OnEnter>`/`<OnLeave>` script slots — or is it swallowed on the way there?
|
||||
///
|
||||
/// `CSimpleButton::OnEnter 0x779490` and `OnLeave 0x7794e0` both open on the state word, and the
|
||||
/// branch skips the *whole* body except the sound:
|
||||
///
|
||||
/// ```text
|
||||
/// 779493 mov eax,[esi+0x328]
|
||||
/// 779499 test eax,eax
|
||||
/// 77949b je 0x7794b0 ; DISABLED -> skip the base notify AND the label restyle
|
||||
/// 77949d call 0x76b6a0 ; CSimpleFrame::OnEnter — highlight layer on, <OnEnter> at +0x140
|
||||
/// ```
|
||||
///
|
||||
/// — so **a DISABLED Button runs neither script**, and `0x7794b0`'s hover sound is the only thing
|
||||
/// past the branch; the base pair's
|
||||
/// script slots are `+0x140`/`+0x148`. Every path that delivers a hover
|
||||
/// edge dispatches through the object's own vtable, so there is no way around the guard: the hover
|
||||
/// walk (`0x766218`–`0x76623c`, `[vt+0x50]` then `[vt+0x4c]`), `SetMouseFocus 0x764dc0`
|
||||
/// (`0x764ddd push 0`), and the hide/removal tail (`0x764cce mov edx,[edi]; push 1;
|
||||
/// call [edx+0x50]`). `CheckButton` inherits both slots; the Hyperlink and NamePlate buttons
|
||||
/// override them and tail straight back into this pair (`0x7cb8a5`), so the guard covers the
|
||||
/// family.
|
||||
///
|
||||
/// The hover **target** still moves — the walk reassigns `[root+0x7c]` *before* it fires either
|
||||
/// notify — so `GetMouseFocus()` answers a disabled button; it is the scripts alone that stay
|
||||
/// quiet. This is what makes AtlasLoot's unset QuickLook buttons safe on the reference: their
|
||||
/// `<OnEnter>` guards with `if this:IsEnabled() then`, which is a NUMBER `1`/`0` and therefore
|
||||
/// always truthy (`IsEnabled 0x7800b0`), so the body would index a nil
|
||||
/// `QuickLooks[n]` — the reference never runs it at all.
|
||||
///
|
||||
/// `None` (the cursor over no frame) and every non-Button kind notify normally.
|
||||
/// Whether a hover edge on `h` reaches the base notify, the HIGHLIGHT layer and the Lua
|
||||
/// `<OnEnter>`/`<OnLeave>`: `OnEnter`/`OnLeave` (`0x779490`/`0x7794e0`) skip it on a DISABLED
|
||||
/// Button, leaving only the hover sound (`0x7794b0`). Every hover path goes through that pair
|
||||
/// (the hover walk `0x766218`, `SetMouseFocus` `0x764dc0`, the hide tail `0x764cce`), and the
|
||||
/// Hyperlink and NamePlate overrides chain back (`0x7cb8a5`). The hover target still moves, so
|
||||
/// `GetMouseFocus()` answers a disabled button.
|
||||
pub(super) fn hover_notify_runs(model: &Model, h: Option<FrameHandle>) -> bool {
|
||||
let Some(h) = h else { return true };
|
||||
match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
@@ -380,8 +242,7 @@ pub(super) fn hover_notify_runs(model: &Model, h: Option<FrameHandle>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write [`ButtonState::loot_slot`] — `LootButton:SetSlot`'s whole body, kept here beside the
|
||||
/// other `ButtonState` writers rather than reaching into the arena from `script::loot`.
|
||||
/// `LootButton:SetSlot`: writes [`ButtonState::loot_slot`].
|
||||
pub(super) fn set_loot_slot(lua: &Lua, this: &Table, slot: Option<u32>) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
@@ -394,9 +255,7 @@ pub(super) fn set_loot_slot(lua: &Lua, this: &Table, slot: Option<u32>) -> mlua:
|
||||
bs.loot_slot = slot;
|
||||
Ok(())
|
||||
}
|
||||
// The reference's own guard: `SetSlot` type-checks `this` against the LootButton tag via
|
||||
// vtable slot 4 (`0x4c18ee`), so calling it on a plain Button raises rather than writing a
|
||||
// field nothing would ever read.
|
||||
// `SetSlot` type-checks `this` as a LootButton (`0x4c18ee`) and raises on anything else.
|
||||
_ => Err(mlua::Error::runtime(
|
||||
"SetSlot: 'this' is not a LootButton widget",
|
||||
)),
|
||||
@@ -424,9 +283,10 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result<u32> {
|
||||
.arena
|
||||
.create_region(h, kind, layer, 0)
|
||||
.ok_or_else(|| mlua::Error::runtime("stale frame handle"))?;
|
||||
// The highlight draws ADD by contract (the live API's SetHighlightTexture default
|
||||
// blend; highlight art is authored for it — under straight alpha it reads as a
|
||||
// dark box over the icon).
|
||||
// A new highlight blends ADD, `SetHighlightTexture`'s default (`0x781d92`). The
|
||||
// reference applies that mode, or a third argument's, on every call and to a handed-in
|
||||
// Texture too (`0x7703f0` from `0x779050` and `0x779110`); here only a new region
|
||||
// takes it.
|
||||
model.region_data.insert(
|
||||
rh,
|
||||
RegionData {
|
||||
@@ -438,20 +298,14 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result<u32> {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
model.touch_layout(); // a region entered the layout gate's read set (decision 0740)
|
||||
model.touch_layout(); // a region entered the layout gate's read set
|
||||
if let Some(frame) = model.arena.frame_mut(h) {
|
||||
if let KindState::Button(bs) = &mut frame.kind_state {
|
||||
slot.set(bs, Some(rh));
|
||||
}
|
||||
}
|
||||
// A freshly built slot region gets its creation-path implicit anchor (decision 1310)
|
||||
// — an EXISTING slot region is never touched here; the get half of get-or-create
|
||||
// changes no geometry. Which anchor depends on the slot: the reference's C++ string
|
||||
// setters SetAllPoints a fresh state texture outright (`0x778f9d`/`0x7790db` — fresh
|
||||
// means zero anchors, so the conditional form is equivalent), while a fresh LABEL is
|
||||
// handed to the adopter (`SetText 0x778dc0` → `0x778d20`), which anchors it by the
|
||||
// button's normal font and links it (decision 1996). The XML loader re-derives after
|
||||
// applying authored `<Anchors>` (see `loader/widgets.rs`).
|
||||
// Only a new region gets an implicit anchor: a state texture fills the button
|
||||
// (`0x778f9d`/`0x7790db`), a label goes to the adopter (`0x778dc0` → `0x778d20`).
|
||||
match slot {
|
||||
Slot::Text => adopt_label(&mut model, h, rh),
|
||||
_ => super::region::implicit_creation_anchor(&mut model, rh),
|
||||
@@ -462,9 +316,8 @@ fn ensure_slot(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result<u32> {
|
||||
Ok(model.region_id(rh))
|
||||
}
|
||||
|
||||
/// Point `slot` at `rh` (or at nothing) and hand back whatever it held before, when that is a
|
||||
/// different region. The reference's slot store `0x778fd0` ends in the outgoing object's
|
||||
/// vtable-slot-0 dtor, so the caller frees it.
|
||||
/// Point `slot` at `rh` or at nothing, returning the different region it held for the caller to
|
||||
/// free, as the slot store `0x778fd0` destroys the outgoing object.
|
||||
fn swap_slot(
|
||||
model: &mut Model,
|
||||
h: FrameHandle,
|
||||
@@ -484,7 +337,7 @@ fn swap_slot(
|
||||
Ok(outgoing)
|
||||
}
|
||||
|
||||
/// Free the region a slot just gave up — never the one that just moved in.
|
||||
/// Free the region a slot gave up, never the one that moved in.
|
||||
fn free_outgoing(lua: &Lua, outgoing: Option<RegionHandle>, incoming: RegionHandle) {
|
||||
if let Some(old) = outgoing.filter(|old| *old != incoming) {
|
||||
super::region::free_region(
|
||||
@@ -494,26 +347,18 @@ fn free_outgoing(lua: &Lua, outgoing: Option<RegionHandle>, incoming: RegionHand
|
||||
}
|
||||
}
|
||||
|
||||
/// The shared body of `Set<State>Texture(texture | "path" | nil)` / `(r, g, b [, a])`.
|
||||
///
|
||||
/// The reference forks on `lua_type(L, 2)` into four legs (`0x781970`).
|
||||
/// **Two of them never touch the
|
||||
/// slot's own region at all**, so they are resolved *before* [`ensure_slot`] — which would
|
||||
/// otherwise lazily build a region for a call whose entire purpose is to replace or drop one.
|
||||
/// `Set<State>Texture(texture | "path" | nil)`, forked on the argument's type (`0x781970`). The
|
||||
/// object and nil legs never touch the slot's own region, so they run before [`ensure_slot`]
|
||||
/// would create one. The `(r, g, b [, a])` form is not 1.12's, which reads a number as a path
|
||||
/// (`0x781b23`).
|
||||
fn set_slot_texture(lua: &Lua, this: &Table, slot: Slot, args: &MultiValue) -> mlua::Result<()> {
|
||||
match args.front() {
|
||||
// **A widget object installs THAT region into the slot** (`0x781b0b`: `push the Texture
|
||||
// object; push idx; call 0x778fd0`) — it does not load a path into the slot's own region.
|
||||
// Three corpus addons build a highlight exactly this way and every one of them was a
|
||||
// silent no-op here: Bongos' bar-drag button (`bar.lua:64-71`), _Nameplates' mouseover
|
||||
// glow (`_Nameplates.lua:217-222`) and Quiver's option rows (`Quiver.bundle.lua:8949-53`)
|
||||
// each `CreateTexture` on the button, colour it, `SetAllPoints`, and hand it over.
|
||||
// A Texture object is itself installed into the slot (`0x781b0b` → `0x778fd0`).
|
||||
Some(Value::Table(t)) => {
|
||||
let rh = super::region::region_handle_of(lua, t)?;
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
// The reference type-checks the handed object and raises rather than installing a
|
||||
// FontString into a texture slot (`0x781ab1`, `0x87a1e0`).
|
||||
// Any other widget raises (`0x781ab1`, message `0x87a1e0`).
|
||||
if model.arena.region(rh).map(|r| r.kind) != Some(RegionKind::Texture) {
|
||||
return Err(mlua::Error::runtime("Wrong object type, expected texture"));
|
||||
}
|
||||
@@ -522,15 +367,9 @@ fn set_slot_texture(lua: &Lua, this: &Table, slot: Slot, args: &MultiValue) -> m
|
||||
free_outgoing(lua, outgoing, rh);
|
||||
return Ok(());
|
||||
}
|
||||
// **nil CLEARS the slot** (`0x781b5a`: `push 0; push idx; call 0x778fd0`, whose tail runs
|
||||
// the outgoing object's dtor). Clearing the pointer alone would be worse than doing
|
||||
// nothing: `ButtonState::region_visible` shows any region that is not one of the button's
|
||||
// own slots unconditionally, so an unhooked state texture would start drawing in *every*
|
||||
// state instead of none. `TheoryCraft/TheoryCraftUI.lua:215-217` strips a talent-rank
|
||||
// button's normal/pushed/highlight art with three of these.
|
||||
//
|
||||
// A call with NO argument is left alone: `LUA_TNONE` is not `LUA_TNIL`, and the
|
||||
// reference's own nil test is `0x781b4f call 0x6f3400 == 0`.
|
||||
// nil clears the slot and destroys the old region (`0x781b5a` → `0x778fd0`): unhooked but
|
||||
// alive, it would draw in every state. No argument is not nil (`0x781b4f`) and clears
|
||||
// nothing.
|
||||
Some(Value::Nil) => {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
@@ -552,9 +391,7 @@ fn set_slot_texture(lua: &Lua, this: &Table, slot: Slot, args: &MultiValue) -> m
|
||||
let rh = *model.id_to_region.get(&id).expect("slot region id");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
match args.front() {
|
||||
// `Set<State>Texture("")` CLEARS — the live API's blank form (ref QuestLogFrame.lua:165
|
||||
// clears the quest rows' +/- slot with exactly this), same as SetTexture(nil)/("") on a
|
||||
// plain region.
|
||||
// `""` clears the texture, which `QuestLogFrame.lua:165` relies on.
|
||||
Some(Value::String(s)) if s.to_str()?.is_empty() => {
|
||||
data.texture = None;
|
||||
data.fill = None;
|
||||
@@ -563,8 +400,7 @@ fn set_slot_texture(lua: &Lua, this: &Table, slot: Slot, args: &MultiValue) -> m
|
||||
data.texture = Some(s.to_str()?.to_string());
|
||||
data.fill = None;
|
||||
}
|
||||
// The colour form generates a solid texture into the same slot the path form loads into
|
||||
// ([`RegionData::fill`]) — never a tint, so each clears the other.
|
||||
// The colour form is a solid texture, not a tint: it and the path clear each other.
|
||||
Some(v @ (Value::Number(_) | Value::Integer(_))) => {
|
||||
let arg = |i: usize| args.get(i).map(as_f32);
|
||||
data.fill = Some([
|
||||
@@ -580,7 +416,7 @@ fn set_slot_texture(lua: &Lua, this: &Table, slot: Slot, args: &MultiValue) -> m
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The shared body of `Get<State>Texture` — the region wrapper, or nil while unset.
|
||||
/// `Get<State>Texture`: the region wrapper, or nil while unset.
|
||||
fn get_slot_texture(lua: &Lua, this: &Table, slot: Slot) -> mlua::Result<Value> {
|
||||
let rh = with_button(lua, this, |bs| slot.get(bs))?;
|
||||
let id = {
|
||||
@@ -616,17 +452,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
texture_pair(lua, &m, "Disabled", Slot::Disabled)?;
|
||||
texture_pair(lua, &m, "Highlight", Slot::Highlight)?;
|
||||
|
||||
// SetText/GetText target the ButtonText fontstring (`0x778dc0`: the `text` attr routes there).
|
||||
// SetText/GetText go to the ButtonText fontstring, as the `text` attribute does (`0x778dc0`).
|
||||
m.set(
|
||||
"SetText",
|
||||
lua.create_function(|lua, (this, text): (Table, Option<Value>)| {
|
||||
let text = super::binding_abi::text_arg(lua, text)?;
|
||||
// `CSimpleButton::SetText 0x778dc0` opens `if (!text) return;` (`0x778dcc`) — a nil
|
||||
// never reaches the label at all, so
|
||||
// it neither clears the text nor lazily creates the FontString. That guard is the
|
||||
// BUTTON's own: a `FontString:SetText(nil)` is not a no-op, it truncates the cell
|
||||
// (`0x771d80`, decision 2110). Below the guard the button is a pass-through to
|
||||
// `[button+0x338]->0x771d80`.
|
||||
// A nil returns at once (`0x778dcc`): no clear, no label created. The guard is the
|
||||
// button's own; `FontString:SetText(nil)` clears (`0x771d80`).
|
||||
let Some(text) = text else { return Ok(()) };
|
||||
let id = ensure_slot(lua, &this, Slot::Text)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
@@ -636,10 +468,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// GetText — like the FontString's own (`region::text`, decision 2110), an **empty label reads
|
||||
// back nil**: `Button:GetText 0x780e10` carries the same first-byte substitution at `0x780ec5`,
|
||||
// over three nil conditions rather than two (no label region, a NULL cell, an empty cell).
|
||||
// Its EditBox neighbour deliberately does not — the law is per binding.
|
||||
// GetText (`0x780e10`): nil for no label, and for an empty one (`0x780ec5`).
|
||||
m.set(
|
||||
"GetText",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -661,50 +490,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| get_slot_texture(lua, &this, Slot::Text))?,
|
||||
)?;
|
||||
|
||||
// SetFontString(fontString) — ADOPT a caller-made FontString as this Button's label.
|
||||
//
|
||||
// End to end (decision 1505): the binding `0x780a60` is gates + a delegate to
|
||||
// `CSimpleButton::SetFontString 0x778d20`, which is the SAME function `SetText`'s lazy creation
|
||||
// path funnels through — so adopting and creating share their whole tail, and only the
|
||||
// allocation differs.
|
||||
//
|
||||
// The idiom it exists for: build the label yourself so you can style and place it, then hand
|
||||
// it to the button so `SetText` and the per-state font machinery drive it. Quiver's
|
||||
// `Component/Select.wow.lua` builds every dropdown option row that way, and with the method
|
||||
// nil the row died on its first line — the second of B267's three walls.
|
||||
//
|
||||
// **It raises, in three distinct ways, and never silently no-ops on a bad argument** — the
|
||||
// first shape I built here was a lenient no-op and the bytes say otherwise:
|
||||
//
|
||||
// · a **missing** argument, `nil`, a number, a string or a boolean → `lua_type(L,2) != TABLE`
|
||||
// → `Usage: %s:SetFontString(fontstring)` (`0x87a100`). Missing and nil take the identical
|
||||
// leg (`index2adr` answers NULL → type −1), so `SetFontString(nil)` **cannot clear the
|
||||
// label from Lua**; the C++ clear exists but no binding reaches it.
|
||||
// · a **table that is not a widget** → `%s:SetFontString(): Couldn't find 'this' in
|
||||
// fontstring` (`0x87a160`).
|
||||
// · a **Frame or a Texture** — anything that is not a FontString → `%s:SetFontString():
|
||||
// Wrong object type, expected fontstring` (`0x87a124`), gated by an `IsA` against the
|
||||
// FontString token, which a Texture fails.
|
||||
//
|
||||
// `%s` is the BUTTON's name (or `<unnamed>`), never the argument's. Zero return values.
|
||||
//
|
||||
// What `0x778d20` then does, all four clauses VERIFIED and all four here:
|
||||
//
|
||||
// · **`new == old` is a total no-op** — the first compare, before anything is touched.
|
||||
// · **the previous label is DESTROYED**, not orphaned (`old->vtable[0](1)`, the scalar
|
||||
// deleting destructor, returning the storage to the FontString pool). So a button whose
|
||||
// `SetText` already lazily made one does not leak a stray string behind the new label.
|
||||
// · **the string is RE-PARENTED to the button and its draw layer forced to ARTWORK**
|
||||
// (`0x77fd10(parent, 2, 1)` — layer id 2 in the client's own `.rdata 0x811a80` name table).
|
||||
// Always, whatever it was parented to or drawn in before.
|
||||
// · **it is anchored only if it has NO anchors of its own** (a scan of all nine
|
||||
// `anchorPoints` slots): LEFT/RIGHT/CENTER by the justify bits of the **button's normal
|
||||
// font** (`[button+0x390]`), to the matching point on the button — [`adopt_label`], the
|
||||
// same `& 7` → LEFT(1)/RIGHT(4)/else-CENTER chain the FontString post-step runs, over the
|
||||
// other word. (It used to reuse the post-step and read the string's own word; the two
|
||||
// only agree when the normal font has no justify — decision 1996.)
|
||||
// · **the button's per-state font is applied immediately** (`0x779810`) — the same
|
||||
// [`adopt_label`] links the label to the normal font, so its query surface answers.
|
||||
// SetFontString(fontString) (`0x780a60` → `0x778d20`) raises, naming the button, on a
|
||||
// non-table (`0x87a100`), nil included, so Lua cannot clear the label; on a non-widget
|
||||
// (`0x87a160`); on a non-FontString (`0x87a124`). The same label is a no-op; otherwise the old
|
||||
// one is destroyed and the new one moves to the button at ARTWORK (`0x77fd10`) and is adopted.
|
||||
m.set(
|
||||
"SetFontString",
|
||||
lua.create_function(|lua, args: MultiValue| {
|
||||
@@ -727,11 +516,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"Usage: {who}:SetFontString(fontstring)"
|
||||
)));
|
||||
};
|
||||
// The two remaining legs are DIFFERENT questions and the reference asks them in this
|
||||
// order: first "is this a framescript object at all" (`0x780b27`), then "is it a
|
||||
// FontString" (`0x780b90`'s `IsA` against the FontString token). A Frame or a Texture
|
||||
// passes the first and fails the second — so resolving straight to a region handle
|
||||
// would collapse both onto the first message and mislabel every widget argument.
|
||||
// Two checks in the reference's order, a widget at all (`0x780b27`), then a FontString
|
||||
// (`0x780b90`), so a Frame or a Texture gets the second message.
|
||||
let rh = {
|
||||
let not_an_object = || {
|
||||
mlua::Error::runtime(format!(
|
||||
@@ -767,8 +553,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
with_button(lua, &this, |bs| bs.text = Some(rh))?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if let Some(dead) = old {
|
||||
// DESTROYED, not orphaned — the shared region-lifetime law, whose doc carries the
|
||||
// reason (`0x778d3c`'s scalar deleting destructor).
|
||||
// Destroyed, not orphaned (`0x778d3c`).
|
||||
super::region::free_region(&mut model, dead);
|
||||
}
|
||||
model.arena.set_region_owner(rh, Some(owner));
|
||||
@@ -776,41 +561,20 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
r.draw_layer = DrawLayer::Artwork;
|
||||
}
|
||||
adopt_label(&mut model, owner, rh);
|
||||
// A new label, a new owner and a possible new anchor — the layout's read set moved,
|
||||
// and the string's extents are what the button's own `GetTextWidth` reports.
|
||||
// The layout's read set moved, and `GetTextWidth` reads the new label's extents.
|
||||
model.touch_layout();
|
||||
model.touch_measure(rh);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetTextWidth / GetTextHeight — the Button's OWN text-extent readers (`0x782290` /
|
||||
// `0x782390`); both are present on Button's own method table, `GetStringWidth` is not.
|
||||
// Both are thin forwards onto the label FontString's own extent vtable slots
|
||||
// (`0x1c` / `0x20`), which is exactly what this delegation is.
|
||||
//
|
||||
// **Who asks.** `Bagnon_Forever/database/ui.lua:61` sizes its character-switch dropdown from
|
||||
// `button:GetTextWidth() + 40` over every saved character — with the method nil the whole
|
||||
// dropdown died on the first row, so the director could not switch characters in Bagnon at all.
|
||||
// The idiom generalises: it is how a 1.12 kit fits a button to its label, and the reference's
|
||||
// own `MoneyFrame.lua` l.202 (`SetWidth(GetTextWidth() + iconWidth)`) is the same shape.
|
||||
//
|
||||
// **Which measurement**, since a FontString carries two. The label's `GetStringWidth` — the
|
||||
// NATURAL, unwrapped extent — never the laid-out `GetWidth`. A button label does not wrap, so
|
||||
// the two agree in the ordinary case; where they differ, serving the laid-out width would hand
|
||||
// every "size the button to its text" caller its own previous output as its next input, which
|
||||
// is decision 0997's measured feedback loop (the macro window's tab that changed width every
|
||||
// frame). The unwrapped extent is the one that settles.
|
||||
//
|
||||
// `0` before the host has measured the string (a frame's latency, as everywhere else), and `0`
|
||||
// for a Button with no label at all — the reference dereferences its FontString pointer `+0x338`
|
||||
// here where `SetFont`/`GetFont` deliberately do not, and what a null one does is NOT byte-read,
|
||||
// so this answers the harmless number rather than raising on a guess.
|
||||
// GetTextWidth/GetTextHeight (`0x782290`/`0x782390`) forward to the label's extents (vtable
|
||||
// `0x1c`/`0x20`). The width is the natural, unwrapped one: the laid-out width would feed a
|
||||
// size-to-text caller (`MoneyFrame.lua:202`) its own output. 0 before measurement and with no
|
||||
// label, where the reference dereferences `+0x338` and what it does on null is untraced.
|
||||
for (name, region_getter) in [
|
||||
("GetTextWidth", "GetStringWidth"),
|
||||
// `GetHeight`, not a `GetStringHeight` — 1.12 has no such method (byte-verified absent
|
||||
// in every encoding), and the reference's own `Button:GetTextHeight 0x782390` is this
|
||||
// same call on the embedded FontString rather than a separate API.
|
||||
// 1.12 has no `GetStringHeight`; `0x782390` reads the label's height.
|
||||
("GetTextHeight", "GetHeight"),
|
||||
] {
|
||||
m.set(
|
||||
@@ -825,17 +589,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
}
|
||||
|
||||
// The per-state label fonts (the 1.12 API trio; XML `<NormalFont>/<HighlightFont>/
|
||||
// <DisabledFont>` route here through the loader). Stored as font-object NAMES — extract
|
||||
// re-points the ButtonText to the current state's object each frame, so Enable/Disable and
|
||||
// hover swap the label's paint with no Lua involvement (the client's own behavior:
|
||||
// UIPanelButtonTemplate's gold/white/gray label states).
|
||||
//
|
||||
// Each takes the font OBJECT, its name, or nil, like `FontString:SetFontObject` — across the
|
||||
// trio the corpus splits 5 object-form to 4 string-form, so accepting one shape only would
|
||||
// break about half the callers, and nil clears the state back to the default. Because the
|
||||
// state is stored as a NAME and re-resolved at every `extract`, a later mutation of that font
|
||||
// object reaches these labels for free.
|
||||
// The per-state label fonts, and `<NormalFont>` and kin through the loader. Each takes a font
|
||||
// object, its name or nil, stored by name: extract re-resolves the current state's font every
|
||||
// frame, so a state change or a later edit to the object reaches the label.
|
||||
m.set(
|
||||
"SetTextFontObject",
|
||||
lua.create_function(|lua, (this, font): (Table, Value)| {
|
||||
@@ -862,30 +618,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetFont(file, height [, flags]) / GetFont() — the Button's own font, `0x780880`/`0x79f3b0`.
|
||||
// `_LazyPig/LazyPigMenu.lua:214` calls it straight on a `CreateFrame("Button", …)`, and it is
|
||||
// that addon's only blocker.
|
||||
//
|
||||
// Three contract details, each a plausible implementation's silent divergence:
|
||||
//
|
||||
// · **It returns ZERO values.** The shared impl `0x79f210` pushes `1`/nil (a real font-load
|
||||
// probe on a Font object — `!OmniCC` uses it), but `Button:SetFont` DISCARDS it
|
||||
// (`xor eax,eax`), so a failed font load is undetectable from a Button. Returning `true`
|
||||
// here would look harmless and would hand an addon a probe the client does not have.
|
||||
// · **It never touches the label.** `SetFont`/`GetFont` do not dereference the FontString
|
||||
// pointer `+0x338` at all, so styling a bare `CreateFrame("Button")` with no `<ButtonText>`
|
||||
// is no error, no crash and **no lazy creation** — which is why the style is stored on the
|
||||
// button ([`ButtonState::font`]) and `extract` applies it to whatever label exists. A later
|
||||
// `SetText` (which *does* lazily create one, `0x778dc0`) then picks it up, so either call
|
||||
// order works, exactly as the reference's does.
|
||||
// · **It retunes all three embedded fonts** (normal `+0x33c`, disabled `+0x434`, highlight
|
||||
// `+0x3b8`) and `GetFont` reads back only the normal one — see [`ButtonFont`] for why one
|
||||
// record is the faithful shape for three identical writes.
|
||||
//
|
||||
// The argument gate is the shared impl's: arg2 `lua_isstring` and arg3 `lua_isnumber`, else
|
||||
// `luaL_error("Usage: %s:SetFont(\"font\", fontHeight [, flags])")` (`0x87c69c`). Both
|
||||
// predicates are the *coercing* ones in 5.0 — `lua_isstring` takes a number, `lua_isnumber`
|
||||
// takes a numeric string — so the leniency is transcribed rather than tightened.
|
||||
// SetFont(file, height [, flags]) (`0x780880`) sets all three embedded fonts and returns
|
||||
// nothing, discarding the shared `0x79f210`'s load result. It never touches the label
|
||||
// (`+0x338`), so creates none: extract applies the stored font to whatever label exists. Its
|
||||
// checks are 5.0's coercing `lua_isstring`/`lua_isnumber`, else a usage error (`0x87c69c`).
|
||||
m.set(
|
||||
"SetFont",
|
||||
lua.create_function(
|
||||
@@ -915,15 +651,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
flags,
|
||||
})
|
||||
})
|
||||
// …and nothing is returned: `with_button` yields `()`.
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
// GetFont() → file, height, flags — **3 values**, read off the NORMAL embedded font. Unset
|
||||
// locally, that font still resolves through what the normal state inherits
|
||||
// (`<NormalFont>`/`SetTextFontObject`), which is how a `GameMenuButtonTemplate` button answers
|
||||
// GameFontNormal's face before anything calls `SetFont`. Nil path / nil height when neither
|
||||
// exists, still 3 values.
|
||||
// GetFont() (`0x7809a0` → `0x79f3b0`) → file, height, flags off the normal font, else the
|
||||
// object it inherits (`<NormalFont>`, `SetTextFontObject`); always three values.
|
||||
m.set(
|
||||
"GetFont",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -952,10 +684,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// The per-state label COLORS (Button:SetTextColor + the Highlight/Disabled pair): the state's
|
||||
// color, when set, repaints the ButtonText over the state font object's paint at extract —
|
||||
// the dropdown kit's rows use all three (info.textR/G/B rows, isTitle's NORMAL-yellow and
|
||||
// notClickable's HIGHLIGHT-white recolors of a disabled row).
|
||||
// The per-state label colours: a set one paints over the state font's colour at extract.
|
||||
m.set(
|
||||
"SetTextColor",
|
||||
lua.create_function(
|
||||
@@ -972,25 +701,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
// GetTextColor() → r, g, b, a — **FOUR values** (`0x781100`, table `0x879d00`, argc 1, arity 4,
|
||||
// kinds `(number,number,number,number)`), the same shape as the FontString/Font-object twins
|
||||
// (`0x79d840` table `0x87c1d8`, `0x79f680`) this codebase already answers. Three is the
|
||||
// plausible wrong answer, and the shapes table calls the name `name-not-unique` precisely
|
||||
// because it is registered on seven tables; this is the Button one.
|
||||
//
|
||||
// **Read like `GetFont` right above, not like `SetTextColor` right above that.** The reference
|
||||
// reads the colour back off the button's NORMAL embedded `CSimpleFont`, and an unset local
|
||||
// colour there still resolves through what the normal state INHERITS
|
||||
// (`<NormalFont>`/`SetTextFontObject`) — which is how a `GameMenuButtonTemplate` button answers
|
||||
// `GameFontNormal`'s colour before anything calls `SetTextColor`. Falling back to white would
|
||||
// be wrong in exactly the case the corpus cares about: reading a stock button's colour to
|
||||
// restore it after a temporary recolour. White is the last resort, matching every other colour
|
||||
// getter here (`GetVertexColor`, `FontString:GetTextColor`) — the untinted default a region
|
||||
// with no colour anywhere draws at.
|
||||
//
|
||||
// Completeness rather than a live break: the census found no corpus site with a Button
|
||||
// receiver (every measured `GetTextColor` call is on a FontString or a font object). It is here
|
||||
// because the reference registers it and the widget shape gate can then cover it.
|
||||
// GetTextColor() (`0x781100`) → r, g, b, a, four values, off the normal font: its own colour,
|
||||
// else the inherited object's (`<NormalFont>`, `SetTextFontObject`), else white.
|
||||
m.set(
|
||||
"GetTextColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -1039,8 +751,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// LockHighlight/UnlockHighlight — pin the HighlightTexture on regardless of hover (ref
|
||||
// CButton::LockHighlight; the dropdown kit's checked rows stay lit).
|
||||
// LockHighlight (`0x782840`) and UnlockHighlight: the highlight shows regardless of hover.
|
||||
m.set(
|
||||
"LockHighlight",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -1062,28 +773,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"Disable",
|
||||
lua.create_function(|lua, this: Table| set_enabled(lua, &this, false))?,
|
||||
)?;
|
||||
// IsEnabled() → the NUMBER 1 or the NUMBER 0 — never a boolean, and never nil.
|
||||
//
|
||||
// The Button method table at
|
||||
// `0x879d10` gives `IsEnabled 0x7800b0`, which reads the three-valued STATE at `[obj+0x328]`
|
||||
// (0 DISABLED / 1 NORMAL / 2 PUSHED), does `setne cl` for "not disabled", and pushes that as a
|
||||
// NUMBER. So the reference's own `IsEnabled() == 0` and `== 1` tests (FriendsFrame.lua l.404,
|
||||
// StaticPopup.lua l.713) are live code.
|
||||
//
|
||||
// **This answered a Lua boolean until now, and the difference is not cosmetic, because 0 is
|
||||
// TRUTHY in Lua.** A caller writing the reference's `== 0` got false forever; a caller writing
|
||||
// `IsEnabled() > 0` got "attempt to compare number with boolean" — which is where pfUI's
|
||||
// `api/ui-widgets.lua:812` icon-fade died, taking the widget module with it. The divergence was
|
||||
// known and worked around by hand in three of our own files (FriendsFrame.xml's
|
||||
// `guildButtonDisabled` read BOTH spellings and said so); those workarounds go with this
|
||||
// change, which is 1719's bar — a prior call revisited because a new measurement arrived.
|
||||
//
|
||||
// Slider used to carry its own `IsEnabled` (slider.rs), left alone here because "whether the
|
||||
// slider widget shares the contract is unverified". It does not share it — it does not HAVE
|
||||
// one: 1.12 registers `Enable`/`Disable`/`IsEnabled` on this table alone, and the Slider's
|
||||
// LoadXML `0x789580` has no `enabled` attribute either, so ours was a superset. The trio came
|
||||
// off the Slider rather than being brought into line with this contract; see the note at its
|
||||
// old site in `slider.rs`.
|
||||
// IsEnabled() (`0x7800b0`) → the number 1 or 0 from the state at `+0x328`, never a boolean:
|
||||
// 0 is truthy in Lua, and the stock UI tests `== 0` (`FriendsFrame.lua:404`) and `== 1`
|
||||
// (`StaticPopup.lua:713`).
|
||||
m.set(
|
||||
"IsEnabled",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -1091,10 +783,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// RegisterForClicks(...): replaces the button's registered-click set outright (the live API's
|
||||
// contract — not additive) with its varargs, verbatim strings (`"LeftButtonUp"`,
|
||||
// `"RightButtonDown"`, …). [`UiScript::mouse_button`] consults the set via [`wants_click`] to
|
||||
// decide whether a press or a release reaches `OnClick`.
|
||||
// RegisterForClicks(...) (`0x782490`) replaces the registered set rather than adding to it
|
||||
// (`0x779730` stores the mask), matching the names in any case (`0x64a4c0`); [`wants_click`]
|
||||
// reads it.
|
||||
m.set(
|
||||
"RegisterForClicks",
|
||||
lua.create_function(|lua, (this, args): (Table, MultiValue)| {
|
||||
@@ -1108,21 +799,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// **`SetButtonState(state[, lock]) 0x780270` — TWO arguments**, and the second is not
|
||||
// cosmetic. It reads three stack
|
||||
// indices and returns none; the state maps case-insensitively over exactly
|
||||
// `{"DISABLED", "NORMAL", "PUSHED"}` (`0x780390`, `SStrCmpI`) and an unrecognised string
|
||||
// raises `Usage: %s:SetButtonState("state", lock)`; the flag is `GetBoolOrDefault` with
|
||||
// **default 0** (`0x78032c push 0`), whose number arm truncates through `_ftol`, so the
|
||||
// literal `1` that `MainMenuBarMicroButtons.lua` passes arms it.
|
||||
//
|
||||
// **The lock is the whole of decision 2134.** While it is set the button ignores the mouse
|
||||
// press/release transitions; while it is clear, the next release un-pushes a scripted push —
|
||||
// which is what Tablet-2.0's rows depend on, since the library pushes them and never calls
|
||||
// `SetButtonState("NORMAL")` anywhere.
|
||||
//
|
||||
// DISABLED really is accepted here — the mapper takes it — even though the 1.12 FrameXML
|
||||
// never passes it.
|
||||
// SetButtonState(state [, lock]) (`0x780270`): DISABLED, NORMAL or PUSHED in any case
|
||||
// (`0x780390`), else a usage error; `lock` is `GetBoolOrDefault` with default 0 (`0x78032c`),
|
||||
// so the `1` of `MainMenuBarMicroButtons.lua:22` sets it. While locked, presses and releases
|
||||
// leave the state alone; unlocked, the next release un-pushes a scripted push.
|
||||
m.set(
|
||||
"SetButtonState",
|
||||
lua.create_function(|lua, (this, state, lock): (Table, String, MultiValue)| {
|
||||
@@ -1145,12 +825,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"GetButtonState",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
// ONE variable, read back (`0x780180` reads `[obj+0x328]`). This used to OR in the
|
||||
// live mouse-held press, because the state was derived and the press lived outside
|
||||
// the widget; now the press *wrote* the state at its edge, so a mouse-held button
|
||||
// answers "PUSHED" for the same reason the reference does — and cannot disagree with
|
||||
// the art the way a re-derived answer could (decision 2134). The chat scroll buttons'
|
||||
// hold-repeat (ref `MessageFrameScrollButton_OnUpdate`) still reads what it needs.
|
||||
// The latched state (`0x780180` reads `+0x328`): a held button answers PUSHED because
|
||||
// its press wrote it, which the chat scroll repeat reads (`ChatFrame.lua:1588`).
|
||||
with_button(lua, &this, |bs| match bs.button_state() {
|
||||
ButtonVisualState::Disabled => "DISABLED",
|
||||
ButtonVisualState::Pushed => "PUSHED",
|
||||
@@ -1159,7 +835,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// Click([button]) — the programmatic click: same path as a physical one (toggle-then-OnClick).
|
||||
// Click([button]) (`0x7826c0`): the physical click's path, toggle and `OnClick` alike.
|
||||
m.set(
|
||||
"Click",
|
||||
lua.create_function(|lua, (this, button): (Table, Option<String>)| {
|
||||
@@ -1169,8 +845,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
model.frame_id(h)
|
||||
};
|
||||
let btn = button.unwrap_or_else(|| "LeftButton".to_string());
|
||||
// A programmatic Click() always emulates a completed (released) click.
|
||||
// A programmatic Click() — `scripted = true`, which only a LootButton reads.
|
||||
// A released click, flagged as scripted, which only a LootButton reads.
|
||||
click_button(lua, id, &btn, false, true);
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -1178,21 +853,19 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
lua.set_named_registry_value(REG_BUTTON_METHODS, m)?;
|
||||
|
||||
// CheckButton's own table (consulted before Button's — the class chain).
|
||||
// CheckButton's own table, consulted before Button's.
|
||||
let c = lua.create_table()?;
|
||||
texture_pair(lua, &c, "Checked", Slot::Checked)?;
|
||||
texture_pair(lua, &c, "DisabledChecked", Slot::DisabledChecked)?;
|
||||
c.set(
|
||||
"SetChecked",
|
||||
lua.create_function(|lua, (this, v): (Table, Value)| {
|
||||
// Numeric coercion, NOT Lua truthiness (decision 0227; `SetChecked 0x799bf0` →
|
||||
// `0x6f1c10`): a number goes through `lua_tonumber` then a truncate-to-int (`fistp`,
|
||||
// round-toward-zero, `0x40a2b0`) and the C++ setter tests `!= 0`. So `SetChecked(0)`
|
||||
// UNchecks (0 is Lua-truthy — only a numeric read gets this right) and `SetChecked(1)`
|
||||
// checks; the type-dispatched helper also honors the keyword strings the reference
|
||||
// passes ("true"/"false" — SpellBookFrame.lua l.296-303), while a non-keyword string
|
||||
// falls to tonumber (→ 0 → unchecked). nil/other → false; booleans honored as-is for
|
||||
// this codebase's own Era-style callers.
|
||||
// `SetChecked` (`0x799bf0`) reads its argument through `0x6f1c10` with default 1
|
||||
// (`0x799c77`), not Lua truthiness: a number truncates toward zero (`0x40a2b0`), so
|
||||
// `SetChecked(0)` unchecks, and the stock UI passes `"true"`/`"false"`
|
||||
// (`SpellBookFrame.lua:296-303`). Here a missing argument unchecks and a string checks
|
||||
// only as `"true"` or a nonzero number, where the reference checks for a missing one
|
||||
// and reads a string by its first byte, so `"yes"` checks there.
|
||||
let checked = match v {
|
||||
Value::Boolean(b) => b,
|
||||
Value::Integer(i) => i != 0,
|
||||
@@ -1208,13 +881,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
c.set(
|
||||
// GetChecked() -> the NUMBER 1 or nil, never a boolean (1830). Its neighbour
|
||||
// `IsEnabled` two entries up was corrected for the same reason under 1719, whose comment
|
||||
// records that a boolean here killed pfUI's widget module outright; this is that call
|
||||
// finished. The reference proves the shape in its own source rather than only at the
|
||||
// bytes: stock `UIOptionsFrame.xml:310` saves a checkbox as
|
||||
// `tostring(this:GetChecked())` and stock `BuffFrame.lua:71` reads it back as `== "1"`,
|
||||
// a round-trip that only closes on the number.
|
||||
// GetChecked() → the number 1 or nil, never a boolean: `UIOptionsFrame.xml:310` saves
|
||||
// `tostring(this:GetChecked())` and `BuffFrame.lua:71` compares it with `"1"`.
|
||||
"GetChecked",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let checked = with_button(lua, &this, |bs| bs.checked)?;
|
||||
@@ -1226,12 +894,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether frame `h` should fire `OnClick` for the input transition `name`
|
||||
/// (`"<Button>ButtonUp"`/`"<Button>ButtonDown"`, e.g. `"RightButtonDown"`) — the
|
||||
/// [`UiScript::mouse_button`](super::UiScript::mouse_button) gate. A Button/CheckButton consults
|
||||
/// its `RegisterForClicks` set (case-insensitively — the live API is forgiving of case); any
|
||||
/// other kind with an `OnClick` fires exactly as it always has, before `RegisterForClicks`
|
||||
/// existed: on release only (`name` ending `"Up"`), for any button, never on press.
|
||||
/// Whether `h` fires `OnClick` for the transition `name` (e.g. `"RightButtonDown"`): a Button
|
||||
/// checks its `RegisterForClicks` set, ignoring case; any other kind fires on release only.
|
||||
pub(super) fn wants_click(model: &Model, h: FrameHandle, name: &str) -> bool {
|
||||
match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Button(bs)) => bs
|
||||
@@ -1242,28 +906,10 @@ pub(super) fn wants_click(model: &Model, h: FrameHandle, name: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether frame `h` is registered for `button` (`"LeftButton"`, `"RightButton"`, …) in **either**
|
||||
/// variant — the press-visual gate, which is a strictly weaker test than [`wants_click`]'s.
|
||||
///
|
||||
/// `CButton::OnMouseDown 0x779210` runs the two as separate tests in one pass, and the difference
|
||||
/// between them is the whole of this:
|
||||
///
|
||||
/// ```text
|
||||
/// 0x779238 eax = event.buttonMask
|
||||
/// 0x779243 ecx = eax << 8 | eax
|
||||
/// 0x77924b test [this+0x330], ecx ; registered EITHER WAY -> keep going, else nothing at all
|
||||
/// 0x779256 hit-test the point ; else nothing
|
||||
/// 0x77926b test event.buttonMask, [this+0x330] ; the DOWN bits alone -> fire OnClick
|
||||
/// 0x7792ad push 2 ; SetState(PUSHED) ; unconditional past the gates above
|
||||
/// ```
|
||||
///
|
||||
/// So `[this+0x330]` is one dword holding the `"…ButtonDown"` registrations in byte 0 and the
|
||||
/// `"…ButtonUp"` registrations in byte 1, and the mask `m | m << 8` asks "this button, either
|
||||
/// variant". **The pushed art is not conditional on the click firing, or on the handler doing
|
||||
/// anything** — it is conditional only on the button being registered for that mouse button at
|
||||
/// all, which is why a right-click on an action, spellbook or pet slot lights up even when the
|
||||
/// right-click does nothing (they all register `RightButtonUp`), and why a right-click on a plain
|
||||
/// `LeftButtonUp` button does not.
|
||||
/// Whether `h` is registered for `button` (`"LeftButton"`, …) as Up or Down, the press-art gate.
|
||||
/// `OnMouseDown` (`0x779210`) tests `m | m << 8` against `[this+0x330]` (Down in byte 0, Up in
|
||||
/// byte 1, `0x77924b`) before the Down-only `OnClick` test (`0x77926b`), so any registration for
|
||||
/// that button pushes the art, click or not.
|
||||
pub(super) fn wants_press_visual(model: &Model, h: FrameHandle, button: &str) -> bool {
|
||||
match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Button(bs)) => bs.registered_clicks.iter().any(|s| {
|
||||
@@ -1275,18 +921,12 @@ pub(super) fn wants_press_visual(model: &Model, h: FrameHandle, button: &str) ->
|
||||
}
|
||||
}
|
||||
|
||||
/// The click behavior shared by the input path and `Click()`: gated on `enabled`; a CheckButton
|
||||
/// **toggles before OnClick fires** (the documented widget contract — a handler reading
|
||||
/// `self:GetChecked()` sees the new state); then `OnClick(self, button, down)` — `down` is `true`
|
||||
/// only for a press that fired via a `"<Button>ButtonDown"` registration, `false` for every
|
||||
/// release-fired or programmatic click (the Era signature: `down` mirrors which transition fired).
|
||||
///
|
||||
/// `scripted` says the click came from Lua `:Click()` rather than from hardware — the reference's
|
||||
/// second `OnClick` argument (`0` from `0x779280`/`0x7793a4`, `1` from the `Button:Click` binding
|
||||
/// `0x7826c0`). Only [`crate::widget::FrameKind::LootButton`] reads it, and it reads it as a hard
|
||||
/// gate: `0x4c182b` returns before the base call, so **a scripted click on a loot row does
|
||||
/// nothing at all — not even run the row's own Lua `OnClick`**. Every other kind ignores it, which
|
||||
/// is the reference's shape too (the base `0x779540` never looks at the flag).
|
||||
/// The click shared by the input path and `Click()`: a disabled Button fires nothing, a CheckButton
|
||||
/// toggles before `OnClick` runs (`0x785550`), then `OnClick` gets the button name and `down`, true
|
||||
/// only for a click a `…ButtonDown` registration fired. The second argument is not 1.12's, whose
|
||||
/// `OnClick` gets the name alone (`0x779540`). `scripted` marks a Lua `Click()` (`0x7826c0` passes
|
||||
/// 1, the mouse `0x779280`/`0x7793a4` pass 0): a LootButton then does nothing at all, not even its
|
||||
/// `OnClick` (`0x4c182b`); every other kind ignores it.
|
||||
pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripted: bool) {
|
||||
let mut take_loot = None;
|
||||
let fire = {
|
||||
@@ -1302,12 +942,10 @@ pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripte
|
||||
if scripted {
|
||||
return;
|
||||
}
|
||||
// Read the take BEFORE the handler runs, but queue it after: the reference reads
|
||||
// `[esi+0x4dc]` after the base call, and a handler that re-slots its own row mid-click
|
||||
// is not a case the shipped UI produces — reading first keeps the borrow simple and
|
||||
// the two cannot disagree. The modifier gate is `0x4c183a`/`0x4c1848`/`0x4c1856`:
|
||||
// shift, ctrl OR alt suppresses, and it is the whole reason the shipped
|
||||
// `LootFrameItem_OnClick` never calls a take itself.
|
||||
// Deviation: the slot is read before the handler, where the reference reads it after
|
||||
// (`[esi+0x4dc]`), because reading first keeps the borrow simple and no stock handler
|
||||
// re-slots its row. Shift, ctrl or alt suppresses the take (`0x4c183a`/`0x4c1848`/
|
||||
// `0x4c1856`).
|
||||
let (shift, ctrl, alt) = model.modifiers;
|
||||
if !shift && !ctrl && !alt {
|
||||
take_loot = model.arena.frame(h).and_then(|f| match &f.kind_state {
|
||||
@@ -1321,7 +959,7 @@ pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripte
|
||||
};
|
||||
let is_check = frame.kind == FrameKind::CheckButton;
|
||||
match &mut frame.kind_state {
|
||||
// A Button/CheckButton click is gated on its enabled flag…
|
||||
// A Button fires only while enabled;
|
||||
KindState::Button(bs) => {
|
||||
if bs.enabled() {
|
||||
if is_check {
|
||||
@@ -1332,7 +970,7 @@ pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripte
|
||||
false
|
||||
}
|
||||
}
|
||||
// …every other kind with an OnClick just fires (plain frames can carry one too).
|
||||
// any other kind with an `OnClick` just fires.
|
||||
_ => true,
|
||||
}
|
||||
};
|
||||
@@ -1349,21 +987,16 @@ pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripte
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
}
|
||||
// **A nameplate's click is the engine's too** (decision 2148): the reference's plate overrides
|
||||
// the button click slot (`0x7cb910`) *and* chains the base, so its unit is selected whether the
|
||||
// click came from the pointer or from Lua's own `Click()` — this funnel is both. Recorded after
|
||||
// the handler, like the loot take below and for the same reason: an addon hook that errors
|
||||
// cannot silently eat the selection. A press (`down`) records nothing — the plate registers for
|
||||
// `LeftButtonUp | RightButtonUp` only (`RegisterForClicks(0x500)` at `0x7cb637`).
|
||||
// A nameplate click selects its unit, from the pointer or `Click()` alike (the plate's
|
||||
// override `0x7cb910` chains the base), after the handler so an erroring hook cannot eat it. A
|
||||
// press selects nothing: the plate registers Up clicks only (`0x7cb637`).
|
||||
if !down {
|
||||
super::nameplate::note_click(lua, id, button);
|
||||
}
|
||||
// …and then the take, unconditionally on the handler's outcome. `0x4c1867` is not guarded by
|
||||
// anything the Lua side did: the base call at `0x4c1833` returns void and its result is never
|
||||
// tested. A row whose `OnClick` errored still loots, which is the reference's behaviour and
|
||||
// the reason a broken addon hook cannot silently eat your loot.
|
||||
// The take runs whatever the handler did (`0x4c1867`): a row whose `OnClick` errored still
|
||||
// loots.
|
||||
if let Some(slot) = take_loot {
|
||||
// The 0-based store, back to the 1-based display row the take queue speaks.
|
||||
// The slot is 0-based; the take queue speaks the 1-based row.
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.loot_picks
|
||||
|
||||
Reference in New Issue
Block a user