Files
benilla-octo/crates/benilla-ui/src/script/surface.rs
T
SamandClaude Fable 5.1 fed2c96ec8 the mailbox takes typing again, and events carry their real arguments
- the mail window's send tab takes typing again: the subject, body and money boxes were taking no input while the recipient box swallowed every key, and a sent letter clears its subject and item icon instead of putting them straight back (2145)
- seven events fire the way the reference does: UNIT_DYNAMIC_FLAGS fires at all, swapping two identical stacks between bank slots fires PLAYERBANKSLOTS_CHANGED twice, the pet sheet's training points repaint, and ITEM_LOCK_CHANGED loses its invented arguments (2140)
- nine widget methods 1.12 addons probe for are in: ShaguTweaks stops recolouring textures the reference leaves alone, pfUI and _Nameplates stop raising on a tooltip's GetAnchorType, and 63 AceEvent addons get RegisterAllEvents (2142)
- six post-1.12 methods leave the widget surface (SetSize, SetShown, HasFocus, SetFormattedText and two tooltip verbs), so an addon reaching for a later client's API finds what a real 1.12 client gives it (2142, 2146)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 20:52:50 +07:00

192 lines
7.7 KiB
Rust

//! **benilla's widget METHOD surface, asked of a running VM** — the shared measurement behind
//! the `dump_widget_methods` example and the widget-surface gate
//! (`script::tests::widget_surface`, decision 2142).
//!
//! It exists for the same reason `dump_globals` does (decisions 1188, 1189): **it is a run, not a
//! grep**. `_G` is not the whole surface an addon can tell apart — most of what an addon touches
//! is reached through a *widget*, and an addon's test for "does this client have X" is
//! `if frame.SetBackdrop then`, i.e. what the `__index` chain answers on a live instance. A regex
//! over `m.set("Name", …)` cannot answer that: it cannot see which registry table a kind actually
//! chains to, it cannot see the Texture/FontString leaf split ([`super::region`]), and it reads a
//! name registered under a `format!` family as one method.
//!
//! So every row is one live object being asked `type(obj.Name) == "function"`, exactly as an addon
//! asks it. The only thing **not** asked of the VM is the *candidate name list* — a metatable whose
//! `__index` is a Rust dispatcher is not enumerable from Lua, so the candidates are the union of
//! the keys of the VM's own method tables ([`METHOD_TABLES`]), read out of the Lua registry at
//! runtime rather than out of the source. A key that no longer resolves to a table is a hard error
//! rather than a silent loss of candidates: a quietly-missing candidate source would under-report
//! the surface, which is the exact failure this measurement exists to prevent.
use std::collections::BTreeSet;
use mlua::{Table, Value};
use super::UiScript;
/// The named-registry method tables the VM installs — the **candidate name source**, read from the
/// live registry.
///
/// This is not the class chain (the chain is `object::kind_method_registries`' business and this
/// instrument deliberately does not model it — it asks the instance instead). It is only the
/// universe of names worth probing: every name benilla registers on any widget appears as a key in
/// one of these, so the union is a superset of our surface and the per-instance probe decides
/// membership.
const METHOD_TABLES: &[&str] = &[
"__benilla_frame_methods",
"__benilla_region_methods",
"__benilla_texture_methods",
"__benilla_fontstring_methods",
"__benilla_title_methods",
"__benilla_font_methods",
"__benilla_button_methods",
"__benilla_checkbutton_methods",
"__benilla_lootbutton_methods",
"__benilla_editbox_methods",
"__benilla_statusbar_methods",
"__benilla_slider_methods",
"__benilla_scrollframe_methods",
"__benilla_simplehtml_methods",
"__benilla_colorselect_methods",
"__benilla_model_methods",
"__benilla_playermodel_methods",
"__benilla_dressupmodel_methods",
"__benilla_tabardmodel_methods",
"__benilla_plain_messageframe_methods",
"__benilla_scrollingmessageframe_methods",
"__benilla_minimap_methods",
"__benilla_tooltip_methods",
];
/// Every widget/region/font class benilla can hand an addon, and one Lua expression that makes one.
///
/// **Ordered, and the order is load-bearing**: the region and font rows are made *by* the plain
/// frame the first row creates, so `Frame` comes first. Each instance is published as the global
/// `DW_<Class>`; the frame kinds also publish their own `CreateFrame` name, which is harmless.
///
/// `TaxiRouteFrame` is absent on purpose — it is a registered `CreateFrame` type that *is* a
/// `Frame` and nothing else (decision 1828), so it would be the `Frame` row twice.
const CLASSES: &[(&str, &str)] = &[
("Frame", r#"CreateFrame("Frame", "DWFrameN", UIParent)"#),
(
"WorldFrame",
r#"CreateFrame("WorldFrame", "DWWorldN", UIParent)"#,
),
("Button", r#"CreateFrame("Button", "DWButtonN", UIParent)"#),
(
"LootButton",
r#"CreateFrame("LootButton", "DWLootN", UIParent)"#,
),
(
"CheckButton",
r#"CreateFrame("CheckButton", "DWCheckN", UIParent)"#,
),
("EditBox", r#"CreateFrame("EditBox", "DWEditN", UIParent)"#),
(
"StatusBar",
r#"CreateFrame("StatusBar", "DWStatusN", UIParent)"#,
),
("Slider", r#"CreateFrame("Slider", "DWSliderN", UIParent)"#),
(
"ScrollFrame",
r#"CreateFrame("ScrollFrame", "DWScrollN", UIParent)"#,
),
("Model", r#"CreateFrame("Model", "DWModelN", UIParent)"#),
(
"PlayerModel",
r#"CreateFrame("PlayerModel", "DWPlayerModelN", UIParent)"#,
),
(
"DressUpModel",
r#"CreateFrame("DressUpModel", "DWDressN", UIParent)"#,
),
(
"TabardModel",
r#"CreateFrame("TabardModel", "DWTabardN", UIParent)"#,
),
(
"MessageFrame",
r#"CreateFrame("MessageFrame", "DWMessageN", UIParent)"#,
),
(
"ScrollingMessageFrame",
r#"CreateFrame("ScrollingMessageFrame", "DWScrollMsgN", UIParent)"#,
),
(
"ColorSelect",
r#"CreateFrame("ColorSelect", "DWColorN", UIParent)"#,
),
(
"SimpleHTML",
r#"CreateFrame("SimpleHTML", "DWHtmlN", UIParent)"#,
),
(
"MovieFrame",
r#"CreateFrame("MovieFrame", "DWMovieN", UIParent)"#,
),
(
"GameTooltip",
r#"CreateFrame("GameTooltip", "DWTooltipN", UIParent)"#,
),
(
"Minimap",
r#"CreateFrame("Minimap", "DWMinimapN", UIParent)"#,
),
("Texture", r#"DW_Frame:CreateTexture("DWTexN")"#),
("FontString", r#"DW_Frame:CreateFontString("DWFSN")"#),
("TitleRegion", r#"DW_Frame:CreateTitleRegion()"#),
("Font", r#"CreateFont("DWFontN")"#),
];
/// Every `(class, method)` pair this VM answers — one live instance per class, probed with the
/// membership test an addon writes.
///
/// **A class that cannot be instantiated is REPORTED, never skipped**: its row is
/// `(class, "!NOT-INSTANTIABLE")`. A census that silently drops a class reads as "that class has
/// no methods", which is the wrong answer to a different question.
pub fn widget_method_census(script: &UiScript) -> mlua::Result<Vec<(String, String)>> {
let mut candidates: BTreeSet<String> = BTreeSet::new();
for key in METHOD_TABLES {
let table: Table = script.lua().named_registry_value(key).map_err(|e| {
mlua::Error::runtime(format!(
"candidate source '{key}' is not a table in this VM ({e}) — a renamed or retired \
method table would silently shrink this census; fix the list, do not ignore it"
))
})?;
for pair in table.pairs::<Value, Value>() {
let (k, _) = pair?;
if let Value::String(s) = k {
candidates.insert(s.to_str()?.to_string());
}
}
}
// Hand the names to the VM once; the per-class probe is then one chunk, not one per name.
let names = script.lua().create_table()?;
for (i, n) in candidates.iter().enumerate() {
names.set(i + 1, n.as_str())?;
}
script.lua().globals().set("DW_NAMES", names)?;
let mut rows: Vec<(String, String)> = Vec::new();
for (class, expr) in CLASSES {
if let Err(e) = script.run(&format!("DW_{class} = {expr}")) {
eprintln!("{class}: could not be instantiated: {e}");
rows.push(((*class).to_string(), "!NOT-INSTANTIABLE".to_string()));
continue;
}
let found: Vec<String> = script.eval(&format!(
"local o = DW_{class} \
local out = {{}} \
for i = 1, table.getn(DW_NAMES) do \
local n = DW_NAMES[i] \
if type(o[n]) == 'function' then table.insert(out, n) end \
end \
return out"
))?;
for name in found {
rows.push(((*class).to_string(), name));
}
}
rows.sort();
Ok(rows)
}