Files
benilla-octo/crates/benilla-ui/src/script/object/frame_state.rs
T
SamandClaude Fable 5 6eae5e8507 widget identity, a funded purse and a walkable quest log
- GetObjectType and IsObjectType land from the binary's own carve, with the client's exact inheritance answers
- the survey seats money and a quest log — sixteen addons call GetMoney, five walk the quest log, and none of those paths had ever run
- `date("*t")` returns its table form; the missing shape was an infinite loop for any addon walking days backwards
- unit tokens fold case like the client's resolver, and a FontString template carries the font object it inherits through the whole chain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:59:47 +07:00

697 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Frame method-table cluster: Show/Hide/visibility, identity/hierarchy (`GetName`/`GetParent`/
//! `SetParent`), strata/level/scale/alpha, the Backdrop mechanism (`SetBackdrop`/`SetBackdropColor`/
//! `SetBackdropBorderColor`), and mouse-enable. Split out of [`super`] purely for size — the shared
//! id/handle plumbing, `CreateFrame`, and the method-table wiring stay there; this module's
//! [`install`] just populates its share of the one shared method table.
use mlua::{Lua, Table, Value};
use crate::order::Strata;
use crate::script::{event, Backdrop, Insets, Model};
use crate::widget::FrameKind;
use super::{decode_id, frame_handle_of, frame_wrapper, strata_from_str};
/// Populate `m`'s visibility/hierarchy/strata/backdrop/mouse methods (see the module doc).
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
// Show / Hide / visibility
m.set(
"Show",
lua.create_function(|lua, this: Table| set_shown(lua, &this, true))?,
)?;
m.set(
"Hide",
lua.create_function(|lua, this: Table| set_shown(lua, &this, false))?,
)?;
// SetShown(bool) — the live API's branchless Show/Hide (a consensus call across the 0068
// target addons; Lua truthiness, so SetShown(nil) hides).
m.set(
"SetShown",
lua.create_function(|lua, (this, shown): (Table, Value)| {
let show = !matches!(shown, Value::Nil | Value::Boolean(false));
set_shown(lua, &this, show)
})?,
)?;
m.set(
"IsShown",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.frame(h).map(|f| f.shown).unwrap_or(false))
})?,
)?;
m.set(
"IsVisible",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model
.arena
.frame(h)
.map(|f| f.effective_visible)
.unwrap_or(false))
})?,
)?;
// Identity / hierarchy
m.set(
"GetName",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let name = {
let model = lua.app_data_ref::<Model>().expect("model");
model.arena.frame(h).and_then(|f| f.name.clone())
};
match name {
Some(n) => Ok(Value::String(lua.create_string(&n)?)),
None => Ok(Value::Nil),
}
})?,
)?;
// GetID/SetID — the app-meaning-free numeric label (XML `id=`, the client's `+0xb4`): a
// dropdown row's list position, a tab index. Default 0 (see `Frame::wow_id`).
m.set(
"GetID",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.frame(h).map(|f| f.wow_id).unwrap_or(0))
})?,
)?;
m.set(
"SetID",
lua.create_function(|lua, (this, id): (Table, i64)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
if let Some(f) = model.arena.frame_mut(h) {
f.wow_id = id;
}
Ok(())
})?,
)?;
// ── GetObjectType / IsObjectType, frame side (wow-re `widget-type-identity.md` §6) ──────────
//
// The region side landed first because a FontString found it; `_Nameplates` is why this half
// exists too — `_Nameplates.lua:164` tests `Nameplate:GetObjectType() ~= "Button"` and `:191`
// tests `== "StatusBar"`, on FRAMES, in the same file whose `Region:GetObjectType()` calls the
// region verbs answer.
//
// The chains are a hardcoded straight-line list per class in the binary, not a runtime parent
// walk, so they are a table here too. Three of them are things a reasonable person invents
// wrongly:
//
// · **`ScrollingMessageFrame` derives from `Frame`, NOT from `MessageFrame`.** The name says
// otherwise and the roster is explicit (`0x787940`).
// · **`SimpleHTML` is spelled with a capitalised HTML.** Our enum variant is `SimpleHtml`, so
// anything derived from the variant name — `format!("{:?}")` and friends — would hand
// addons `"SimpleHtml"`, and `GetObjectType` is compared with `==` (`IsObjectType`'s
// case-folding would hide it; the getter's would not).
// · **`Cooldown` answers `"Model"`.** 1.12.1 has no `Cooldown` type — the census finds 23
// type-name globals and none is that. The reference builds its cooldown as a `Model` playing
// `UI-Cooldown-Indicator.mdx` (`CooldownFrameTemplate`), and OUR `FrameKind::Cooldown` is a
// deliberate Era-shaped divergence that models the mechanism first-class (0137 phase 4). So
// the faithful answer is what the reference's own cooldown widget IS. Answering `"Cooldown"`
// would announce an Era type to a Lua ecosystem that branches on presence — precisely the
// superset 1189 had to take back out.
fn type_chain(kind: FrameKind) -> &'static [&'static str] {
match kind {
FrameKind::Frame => &["Frame", "Region"],
FrameKind::Button => &["Button", "Frame", "Region"],
FrameKind::CheckButton => &["CheckButton", "Button", "Frame", "Region"],
FrameKind::EditBox => &["EditBox", "Frame", "Region"],
FrameKind::StatusBar => &["StatusBar", "Frame", "Region"],
FrameKind::Slider => &["Slider", "Frame", "Region"],
FrameKind::ScrollFrame => &["ScrollFrame", "Frame", "Region"],
FrameKind::Model => &["Model", "Frame", "Region"],
FrameKind::MessageFrame => &["MessageFrame", "Frame", "Region"],
FrameKind::ScrollingMessageFrame => &["ScrollingMessageFrame", "Frame", "Region"],
FrameKind::ColorSelect => &["ColorSelect", "Frame", "Region"],
FrameKind::SimpleHtml => &["SimpleHTML", "Frame", "Region"],
FrameKind::MovieFrame => &["MovieFrame", "Frame", "Region"],
FrameKind::GameTooltip => &["GameTooltip", "Frame", "Region"],
FrameKind::Minimap => &["Minimap", "Frame", "Region"],
FrameKind::Cooldown => &["Model", "Frame", "Region"],
}
}
fn chain_of(lua: &Lua, this: &Table) -> mlua::Result<&'static [&'static str]> {
let h = frame_handle_of(lua, this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model
.arena
.frame(h)
.map_or(&["Frame", "Region"][..], |f| type_chain(f.kind)))
}
m.set(
"GetObjectType",
lua.create_function(|lua, this: Table| {
Ok(Value::String(lua.create_string(chain_of(lua, &this)?[0])?))
})?,
)?;
// Same four traps as the region twin (see `script::region`): case-insensitive whole-string,
// number 1 on a hit and nil on a miss, exactly one value either way, and a non-string
// non-number argument raises the reference's own `Usage:` text.
m.set(
"IsObjectType",
lua.create_function(|lua, (this, want): (Table, Value)| {
let chain = chain_of(lua, &this)?;
let want = match &want {
Value::String(s) => s.to_str()?.to_string(),
Value::Number(n) => n.to_string(),
Value::Integer(i) => i.to_string(),
_ => {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
let who = model
.arena
.frame(h)
.and_then(|f| f.name.clone())
.unwrap_or_else(|| "<unnamed>".to_string());
return Err(mlua::Error::runtime(format!(
"Usage: {who}:IsObjectType(\"TYPE\")"
)));
}
};
Ok(if chain.iter().any(|t| want.eq_ignore_ascii_case(t)) {
Value::Number(1.0)
} else {
Value::Nil
})
})?,
)?;
m.set(
"GetParent",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let parent_id = {
let mut model = lua.app_data_mut::<Model>().expect("model");
model
.arena
.frame(h)
.and_then(|f| f.parent)
.map(|p| model.frame_id(p))
};
match parent_id {
Some(pid) => Ok(Value::Table(frame_wrapper(lua, pid)?)),
None => Ok(Value::Nil),
}
})?,
)?;
m.set(
"SetParent",
lua.create_function(|lua, (this, parent): (Table, Value)| {
let h = frame_handle_of(lua, &this)?;
let new_parent = {
let model = lua.app_data_ref::<Model>().expect("model");
match &parent {
Value::Table(t) => decode_id(t)
.ok()
.and_then(|id| model.id_to_frame.get(&id).copied()),
Value::String(s) => {
s.to_str().ok().and_then(|n| model.arena.lookup(n.as_ref()))
}
_ => None,
}
};
let changed = {
let mut model = lua.app_data_mut::<Model>().expect("model");
let before = model.arena.frame(h).and_then(|f| f.parent);
let changed = model.arena.set_parent(h, new_parent);
// A real reparent can move the subtree's effective scale — a layout-gate input
// (the arena rejects same-parent/cycle calls, so `before` moving is the test).
if model.arena.frame(h).and_then(|f| f.parent) != before {
model.touch_layout();
}
changed
};
event::fire_visibility_changes(lua, changed);
Ok(())
})?,
)?;
// Strata / level / scale / alpha
m.set(
"SetFrameStrata",
lua.create_function(|lua, (this, strata): (Table, String)| {
let h = frame_handle_of(lua, &this)?;
let s = strata_from_str(&strata)
.ok_or_else(|| mlua::Error::runtime(format!("unknown frameStrata '{strata}'")))?;
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_frame_strata(h, s);
Ok(())
})?,
)?;
m.set(
"GetFrameStrata",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
let s = model.arena.frame(h).map(|f| f.strata).unwrap_or_default();
Ok(strata_name(s).to_string())
})?,
)?;
m.set(
"SetFrameLevel",
lua.create_function(|lua, (this, level): (Table, i64)| {
let h = frame_handle_of(lua, &this)?;
let lvl = level.clamp(0, i64::from(u16::MAX)) as u16;
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_frame_level(h, lvl, true);
Ok(())
})?,
)?;
m.set(
"GetFrameLevel",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(i64::from(
model.arena.frame(h).map(|f| f.level).unwrap_or(0),
))
})?,
)?;
m.set(
"SetScale",
lua.create_function(|lua, (this, scale): (Table, f32)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
// Effective-scale changes ride the propagation's own eps gate; the own-scale compare
// is the cheap superset (same own scale => no effective change is possible).
let changed = model.arena.frame(h).is_some_and(|f| f.scale != scale);
model.arena.set_scale(h, scale);
if changed {
model.touch_layout();
}
Ok(())
})?,
)?;
m.set(
"GetScale",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.frame(h).map(|f| f.scale).unwrap_or(1.0))
})?,
)?;
m.set(
"SetAlpha",
lua.create_function(|lua, (this, alpha): (Table, f32)| {
let h = frame_handle_of(lua, &this)?;
// The 1.12 API clamps to 0..1 — ref Lua leans on it (fade code that passes a 0..255
// alpha stays fully opaque until the value falls below 1/255).
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_alpha(h, alpha.clamp(0.0, 1.0));
Ok(())
})?,
)?;
m.set(
"GetAlpha",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.frame(h).map(|f| f.alpha).unwrap_or(1.0))
})?,
)?;
// Backdrop (backdrop-mechanism.md): SetBackdrop(table|nil) installs (or, with nil, tears down)
// the frame's tiled bg + 8-piece border plate. The two color setters tint the bg / all 8 border
// pieces (never the reverse — spec §4). The Lua-table SetBackdrop defaults both colors to WHITE
// (the ctor), so a caller must SetBackdropColor after to tint (the tooltip's OnLoad does).
m.set(
"SetBackdrop",
lua.create_function(|lua, (this, arg): (Table, Value)| {
let h = frame_handle_of(lua, &this)?;
let bd = match arg {
Value::Nil => None,
Value::Table(t) => Some(backdrop_from_table(&t)?),
other => {
return Err(mlua::Error::runtime(format!(
"SetBackdrop: expected a table or nil, got {}",
other.type_name()
)))
}
};
let mut model = lua.app_data_mut::<Model>().expect("model");
match bd {
Some(b) => {
model.backdrops.insert(h, b);
}
None => {
model.backdrops.remove(&h);
}
}
Ok(())
})?,
)?;
// `GetBackdrop()` (`0x777370`; wow-re `ui/scratch/widget-api-batch-benilla.md` Q5) — five things
// a plausible implementation gets wrong, so each is spelled out with its reason:
//
// 1. It is a **reconstruction from the struct**, never the caller's table. The reference stores
// no Lua reference anywhere: `SetBackdrop` reads six keys into a fresh 0x68-byte struct at
// `frame+0x1ac` and drops the table, so `0x777426`–`0x7776b9` re-push every key from those
// fields. Handing back a stored clone would leak keys the reader never accepted and would
// keep stale values a later `SetBackdropColor` changed.
// 2. **No backdrop ⇒ ZERO Lua values, not `nil`** — the early bail is `xor eax,eax; ret`, which
// for a *return* path really is "no values" (contrast `binding_abi`'s note: the same two
// bytes after a `luaL_error` are unreachable boilerplate). Observable through `select('#')`,
// and it is the shape our `GetTitleRegion` will *not* have when it lands — that one pushes
// nil, i.e. one value. The client cannot distinguish "never set" from `SetBackdrop(nil)`.
// 3. **A partial `SetBackdrop` omits nothing on the way out.** Every `SetBackdrop` allocates a
// fresh struct (`0x777801`, ctor `0x77e5f0`), so a key the caller left out is a *ctor
// default* here, not an absent key and not the previous backdrop's value: `bgFile`/`edgeFile`
// `""`, `tileSize` 0, `edgeSize` **32**. Our `backdrop_from_table` already builds on
// `Backdrop::default()`, so this falls out — but only because `None` maps to `""` below
// rather than to nil.
// 4. **`tile` is the NUMBER `1`, or the key is ABSENT — never `true`/`false`.** The push is
// `0x3ff00000` (the double 1.0) on true and `lua_pushnil` on false, and `lua_settable` with a
// nil value creates no key (and *erases* one from a recycled table, which is why the nil is
// written rather than skipped). An addon reading `if backdrop.tile then` sees the same truth
// either way; one that round-trips the table into `SetBackdrop` is why the number matters,
// since `tile` there goes through a coercer that takes numbers.
// 5. The undocumented **in-place form** (`0x77740e`): `lua_type(L,2) == LUA_TTABLE` skips
// `lua_newtable` and fills arg 2, reusing an existing `insets` subtable rather than replacing
// it. Implemented — it is four lines, and an addon caching one table across frames would
// otherwise silently get a new one each call. A non-table arg 2 is ignored, not an error.
//
// No `bgColor`/`edgeColor`/`alpha` key exists: the two colors live in the struct but the reader
// never pushes them (`GetBackdropColor`/`GetBackdropBorderColor` are their only accessors).
m.set(
"GetBackdrop",
lua.create_function(|lua, (this, target): (Table, Value)| {
let h = frame_handle_of(lua, &this)?;
// Copied out before a single Lua write: filling a *caller-supplied* table can run a
// `__newindex` metamethod, which can re-enter us and would panic on the app-data borrow.
let bd = {
let model = lua.app_data_ref::<Model>().expect("model");
model.backdrops.get(&h).cloned()
};
let Some(bd) = bd else {
return Ok(mlua::MultiValue::new()); // trap 2 — zero values, not nil
};
let t = match target {
Value::Table(t) => t,
_ => lua.create_table()?,
};
t.set("bgFile", bd.bg_file.as_deref().unwrap_or(""))?;
t.set("edgeFile", bd.edge_file.as_deref().unwrap_or(""))?;
t.set(
"tile",
if bd.tile {
Value::Number(1.0) // trap 4
} else {
Value::Nil // trap 4 — erases the key from a recycled table
},
)?;
t.set("tileSize", f64::from(bd.tile_size))?;
t.set("edgeSize", f64::from(bd.edge_size))?;
let insets = match t.get::<Value>("insets") {
Ok(Value::Table(existing)) => existing, // trap 5 — reuse, don't replace
_ => {
let fresh = lua.create_table()?;
t.set("insets", &fresh)?;
fresh
}
};
insets.set("left", f64::from(bd.insets.left))?;
insets.set("right", f64::from(bd.insets.right))?;
insets.set("top", f64::from(bd.insets.top))?;
insets.set("bottom", f64::from(bd.insets.bottom))?;
Ok(mlua::MultiValue::from_vec(vec![Value::Table(t)]))
})?,
)?;
m.set(
"SetBackdropColor",
lua.create_function(
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
if let Some(bd) = model.backdrops.get_mut(&h) {
bd.bg_color = [r, g, b, a.unwrap_or(1.0)];
}
Ok(())
},
)?,
)?;
m.set(
"SetBackdropBorderColor",
lua.create_function(
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
if let Some(bd) = model.backdrops.get_mut(&h) {
bd.border_color = [r, g, b, a.unwrap_or(1.0)];
}
Ok(())
},
)?,
)?;
m.set(
"GetBackdropColor",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
match model.backdrops.get(&h) {
Some(bd) => Ok((
bd.bg_color[0],
bd.bg_color[1],
bd.bg_color[2],
bd.bg_color[3],
)),
None => Ok((1.0, 1.0, 1.0, 1.0)),
}
})?,
)?;
m.set(
"GetBackdropBorderColor",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
match model.backdrops.get(&h) {
Some(bd) => Ok((
bd.border_color[0],
bd.border_color[1],
bd.border_color[2],
bd.border_color[3],
)),
None => Ok((1.0, 1.0, 1.0, 1.0)),
}
})?,
)?;
// Mouse interaction (EnableMouse gates hit-testing; keyboard focus is out of scope)
m.set(
"EnableMouse",
lua.create_function(|lua, (this, enable): (Table, bool)| {
let h = frame_handle_of(lua, &this)?;
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_mouse_enabled(h, enable);
Ok(())
})?,
)?;
m.set(
"IsMouseEnabled",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.is_mouse_enabled(h))
})?,
)?;
// EnableKeyboard(flag) / IsKeyboardEnabled() — `0x776f90` / `0x776ff0`, real Frame entries in
// wow-re's frame-API carve, beside the mouse pair above.
//
// 8 corpus addons call this and were RAISING on it (`ColorPickerPlus:258`,
// `Dewdrop-2.0.lua:2021-2022` — which is in ~65 addons — `AckisRecipeList/ARLFrame.lua:1650`).
// The flag round-trips; **key delivery is not gated on it**, exactly as the wheel's flag
// shipped in 1198, because the machinery it would gate does not exist here yet.
//
// Why that is the right half rather than a silent stub, which this codebase has been bitten by
// three times (1203/1205/1211): the majority of the corpus calls pass **false**, and for those
// our behaviour is already the reference's — we deliver no keys to arbitrary frames either. The
// `true` callers want delivery we do not do, but they did not get it before this landed; they
// got a raise that killed the enclosing function. And per
// `frame-key-script-delivery.md` §3.2 being enabled genuinely is separable from having a
// handler, so the flag is a real piece of the model, not a placeholder for one.
m.set(
"EnableKeyboard",
lua.create_function(|lua, (this, enable): (Table, bool)| {
let h = frame_handle_of(lua, &this)?;
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_keyboard_enabled(h, enable);
Ok(())
})?,
)?;
m.set(
"IsKeyboardEnabled",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.is_keyboard_enabled(h))
})?,
)?;
// `EnableMouseWheel(flag)` / `IsMouseWheelEnabled()` — the wheel's own gate, a separate flag
// from `EnableMouse` in the reference and separate here (decision 1198).
//
// The flag is real and round-trips. **The dispatch is NOT gated on it yet, deliberately.**
// Our wheel dispatch keys off "does this frame carry an `OnMouseWheel` handler", walking up to
// the nearest ancestor that does — more permissive than the reference, which also requires the
// frame to be wheel-enabled so a scroll region can hand the wheel to the window behind it
// without tearing its handler out.
//
// Gating it today would break our own UI: 44 `OnMouseWheel` sites across 14 shipped files and
// **not one of them declares `enableMouseWheel`**, because the loader has never read that
// attribute. The condition to flip it is concrete rather than someday — teach the loader the
// attribute, declare it on those 44 sites, then gate. Until then this is a disclosed superset
// (1189's argument, pointed the other way), and the two corpus addons that stopped on the
// missing *method* are unblocked either way.
m.set(
"EnableMouseWheel",
lua.create_function(|lua, (this, enable): (Table, bool)| {
let h = frame_handle_of(lua, &this)?;
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_mouse_wheel_enabled(h, enable);
Ok(())
})?,
)?;
m.set(
"IsMouseWheelEnabled",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.is_mouse_wheel_enabled(h))
})?,
)?;
// Clamp-to-screen (`0x776c00`/`0x776cb0`, geometry flags bit4 — layout.md): the layout resolve
// keeps the frame's assembled rect inside the window, size preserved. GameTooltip frames
// default true by construction (widget::Frame::clamped_to_screen — decision 0352).
m.set(
"SetClampedToScreen",
lua.create_function(|lua, (this, clamp): (Table, bool)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
let changed = model.arena.is_clamped_to_screen(h) != clamp;
model.arena.set_clamped_to_screen(h, clamp);
if changed {
model.touch_layout();
}
Ok(())
})?,
)?;
m.set(
"IsClampedToScreen",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.arena.is_clamped_to_screen(h))
})?,
)?;
// Hit-rect insets — the MOUSE rect only (widget::Frame::hit_rect_insets): the hit test shrinks
// the resolved rect by these four before testing the cursor, and nothing else reads them, so a
// frame's geometry/draw/anchor answers are unchanged. The ref sets them wherever a button's
// frame is larger than its art (the micro buttons' 18 px empty header).
m.set(
"SetHitRectInsets",
lua.create_function(
|lua, (this, left, right, top, bottom): (Table, f32, f32, f32, f32)| {
let h = frame_handle_of(lua, &this)?;
lua.app_data_mut::<Model>()
.expect("model")
.arena
.set_hit_rect_insets(h, [left, right, top, bottom]);
Ok(())
},
)?,
)?;
m.set(
"GetHitRectInsets",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
let i = model.arena.hit_rect_insets(h);
Ok((i[0], i[1], i[2], i[3]))
})?,
)?;
Ok(())
}
/// Parse a Lua `SetBackdrop` table into a [`Backdrop`] (backdrop-mechanism.md §1). The keys read —
/// exactly the compiled reader's set — are `bgFile`/`edgeFile` (strings), `tile` (boolean, default
/// false), `tileSize`/`edgeSize` (numbers; a missing number leaves the ctor default: tileSize 0,
/// edgeSize 32), and `insets{left,right,top,bottom}` (numbers, each default 0). A non-string file or
/// non-number size is treated as absent (the client's per-key type gate). Colors stay the ctor
/// white — SetBackdrop never reads a color key; the color setters do that.
fn backdrop_from_table(t: &Table) -> mlua::Result<Backdrop> {
let mut bd = Backdrop::default();
if let Ok(Value::String(s)) = t.get::<Value>("bgFile") {
bd.bg_file = Some(s.to_str()?.to_string());
}
if let Ok(Value::String(s)) = t.get::<Value>("edgeFile") {
bd.edge_file = Some(s.to_str()?.to_string());
}
// `tile` — Lua truthiness (nil/false ⇒ false; a missing key reads as Nil).
bd.tile = !matches!(
t.get::<Value>("tile").unwrap_or(Value::Nil),
Value::Nil | Value::Boolean(false)
);
if let Ok(Value::Number(n)) = t.get::<Value>("tileSize") {
bd.tile_size = n as f32;
} else if let Ok(Value::Integer(n)) = t.get::<Value>("tileSize") {
bd.tile_size = n as f32;
}
if let Ok(Value::Number(n)) = t.get::<Value>("edgeSize") {
bd.edge_size = n as f32;
} else if let Ok(Value::Integer(n)) = t.get::<Value>("edgeSize") {
bd.edge_size = n as f32;
}
if let Ok(Value::Table(ins)) = t.get::<Value>("insets") {
let num = |k: &str| -> f32 {
match ins.get::<Value>(k) {
Ok(Value::Number(n)) => n as f32,
Ok(Value::Integer(n)) => n as f32,
_ => 0.0,
}
};
bd.insets = Insets {
left: num("left"),
right: num("right"),
top: num("top"),
bottom: num("bottom"),
};
}
Ok(bd)
}
fn strata_name(s: Strata) -> &'static str {
match s {
Strata::World => "WORLD",
Strata::Background => "BACKGROUND",
Strata::Low => "LOW",
Strata::Medium => "MEDIUM",
Strata::High => "HIGH",
Strata::Dialog => "DIALOG",
Strata::Fullscreen => "FULLSCREEN",
Strata::FullscreenDialog => "FULLSCREEN_DIALOG",
Strata::Tooltip => "TOOLTIP",
Strata::Blizzard => "BLIZZARD",
}
}
fn set_shown(lua: &Lua, this: &Table, shown: bool) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
let changed = {
let mut model = lua.app_data_mut::<Model>().expect("model");
model.arena.set_shown(h, shown)
};
event::fire_visibility_changes(lua, changed);
Ok(())
}