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>
This commit is contained in:
Sam
2026-09-11 20:52:50 +07:00
co-authored by Claude Fable 5.1
parent 3f3cf21afa
commit fed2c96ec8
100 changed files with 3837 additions and 869 deletions
@@ -15,7 +15,7 @@ fn setpoint_explicit_nil_relative_to_keeps_offsets() {
r#"
local f = CreateFrame("Frame", "Nil")
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 40, -40)
f:SetSize(300, 200)
f:SetWidth(300); f:SetHeight(200)
"#,
)
.unwrap();
@@ -40,7 +40,7 @@ fn setpoint_resolve_size_and_rect() {
r#"
local f = CreateFrame("Frame", "Sized")
f:SetPoint("TOPLEFT", 10, -5) -- relativeTo = screen (default), relativePoint = TOPLEFT
f:SetSize(200, 50)
f:SetWidth(200); f:SetHeight(50)
"#,
)
.unwrap();
@@ -68,7 +68,7 @@ fn setpoint_resolve_size_and_rect() {
#[test]
fn getwidth_falls_back_to_explicit_size_before_resolve() {
let s = script();
// No SetPoint ⇒ unresolvable; GetWidth returns the explicit SetSize value.
// No SetPoint ⇒ unresolvable; GetWidth returns the explicit SetWidth value.
let w: f32 = s
.eval(r#"local f = CreateFrame("Frame"); f:SetWidth(123); return f:GetWidth()"#)
.unwrap();
@@ -13,7 +13,7 @@ fn backdrop_installs_and_extracts_pieces_with_colors() {
r#"
local f = CreateFrame("Frame", "Plate")
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 100, -100)
f:SetSize(200, 100)
f:SetWidth(200); f:SetHeight(100)
f:SetBackdrop({
bgFile = "bg", edgeFile = "edge", tile = true,
tileSize = 16, edgeSize = 16,
@@ -129,7 +129,7 @@ fn get_backdrop_round_trips_through_set_backdrop() {
r#"
local f = CreateFrame("Frame", "BC2Plate")
f:SetPoint("CENTER")
f:SetSize(54, 54)
f:SetWidth(54); f:SetHeight(54)
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge", tile = true,
tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 } })
@@ -166,7 +166,7 @@ fn set_backdrop_nil_tears_down() {
r#"
local f = CreateFrame("Frame", "Plate2")
f:SetPoint("CENTER")
f:SetSize(100, 100)
f:SetWidth(100); f:SetHeight(100)
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge" })
f:SetBackdrop(nil)
"#,
+105 -22
View File
@@ -34,7 +34,7 @@ fn button_state_textures_switch_with_interaction() {
s.run(
r#"
local b = CreateFrame("Button", "StateBtn")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
b:SetNormalTexture("Interface\\N.blp")
b:SetPushedTexture("Interface\\P.blp")
b:SetDisabledTexture("Interface\\D.blp")
@@ -110,7 +110,7 @@ fn a_state_with_no_texture_leaves_the_shown_one_standing() {
s.run(
r#"
local b = CreateFrame("Button", "StickyBtn")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
b:SetNormalTexture("Interface\\N.blp")
"#,
)
@@ -162,7 +162,7 @@ fn a_state_with_no_texture_leaves_the_shown_one_standing() {
s.run(
r#"
local b = CreateFrame("Button", "BornDeadBtn")
b:SetPoint("BOTTOMLEFT", 200, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", 200, 0); b:SetWidth(100); b:SetHeight(100)
b:Disable()
b:SetNormalTexture("Interface\\N.blp")
"#,
@@ -191,7 +191,7 @@ fn disabling_a_button_takes_its_whole_highlight_layer() {
s.run(
r#"
local b = CreateFrame("Button", "LayerBtn")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
b:SetNormalTexture("Interface\\N.blp")
b:SetHighlightTexture("Interface\\H.blp")
local own = b:CreateTexture(nil, "HIGHLIGHT")
@@ -271,7 +271,7 @@ fn any_registered_mouse_button_shows_the_pushed_texture() {
r#"
local function slot(name, x)
local b = CreateFrame("Button", name)
b:SetPoint("BOTTOMLEFT", x, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", x, 0); b:SetWidth(100); b:SetHeight(100)
b:SetNormalTexture("Interface\\" .. name .. "N.blp")
b:SetPushedTexture("Interface\\" .. name .. "P.blp")
return b
@@ -331,7 +331,7 @@ fn set_button_state_drives_the_pushed_visual() {
s.run(
r#"
local b = CreateFrame("Button", "PushBtn")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
b:SetNormalTexture("Interface\\N.blp")
b:SetPushedTexture("Interface\\P.blp")
"#,
@@ -375,7 +375,7 @@ fn disabled_button_swallows_clicks_checkbutton_toggles_before_onclick() {
r#"
clicks, seen_checked = 0, nil
local cb = CreateFrame("CheckButton", "Toggler")
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100)
cb:SetScript("OnClick", function(self, button, down)
clicks = clicks + 1
seen_checked = self:GetChecked()
@@ -422,7 +422,7 @@ fn default_registration_is_left_click_only_right_click_reaches_nothing() {
r#"
clicks = 0
local btn = CreateFrame("Button", "Vendor")
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100)
btn:SetScript("OnClick", function(self, button, down) clicks = clicks + 1 end)
"#,
)
@@ -445,7 +445,7 @@ fn register_for_clicks_grows_right_click_and_carries_the_button_name() {
r#"
clicks, click_btn, arg1_btn = 0, nil, nil
local btn = CreateFrame("Button", "Vendor")
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetWidth(100); btn:SetHeight(100)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:SetScript("OnClick", function(self, button, down)
clicks = clicks + 1
@@ -478,7 +478,7 @@ fn down_registration_fires_on_press_and_toggles_checked_once() {
r#"
clicks, seen_down = 0, nil
local cb = CreateFrame("CheckButton", "QuickSell")
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetWidth(100); cb:SetHeight(100)
cb:RegisterForClicks("LeftButtonDown")
cb:SetScript("OnClick", function(self, button, down)
clicks = clicks + 1
@@ -509,9 +509,9 @@ fn highlight_is_additive_and_state_textures_fill_then_anchor() {
s.run(
r#"
local b = CreateFrame("Button", "AddBtn")
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetSize(36, 36)
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetWidth(36); b:SetHeight(36)
b:SetNormalTexture("Interface\\Ring.blp")
b:GetNormalTexture():SetSize(64, 64)
local nt = b:GetNormalTexture(); nt:SetWidth(64); nt:SetHeight(64)
b:SetHighlightTexture("Interface\\Hi.blp")
"#,
)
@@ -533,7 +533,7 @@ fn highlight_is_additive_and_state_textures_fill_then_anchor() {
));
// A fresh state texture gets the creation-path implicit SetAllPoints (decision 1310 — the
// reference's string setters anchor a freshly built texture to the button outright), whose
// two corners pin all four edges: the later SetSize(64) is structurally unread and the ring
// two corners pin all four edges: the later 64px size is structurally unread and the ring
// FILLS the 36px button.
let r = find("Interface\\Ring.blp").rect.unwrap();
assert_eq!(
@@ -608,7 +608,7 @@ fn button_label_repaints_by_state_font_object() {
s.run(
r#"
b = CreateFrame("Button", "FontBtn")
b:SetPoint("CENTER", 0, 0); b:SetSize(100, 20)
b:SetPoint("CENTER", 0, 0); b:SetWidth(100); b:SetHeight(20)
b:SetText("Label")
b:SetTextFontObject("GoldFont")
b:SetDisabledFontObject("GrayFont")
@@ -696,7 +696,7 @@ fn a_locked_or_hovered_button_wears_its_highlight_font_over_its_normal_color() {
s.run(
r#"
b = CreateFrame("Button", "RowBtn")
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetSize(100, 20)
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetWidth(100); b:SetHeight(20)
b:SetText("Rough Copper Vest")
b:SetTextFontObject("RowNormal")
b:SetHighlightFontObject("RowHighlight")
@@ -1103,7 +1103,7 @@ fn a_button_labels_own_setfont_survives_the_state_font_repoint() {
s.run(
r#"
b = CreateFrame("Button", "SkinnedBtn")
b:SetPoint("CENTER", 0, 0); b:SetSize(100, 20)
b:SetPoint("CENTER", 0, 0); b:SetWidth(100); b:SetHeight(20)
b:SetText("Label")
b:SetTextFontObject("TemplateFont")
b:GetFontString():SetFont("Interface\\Addons\\Skin\\Fonts\\porky.ttf", 18, "OUTLINE")
@@ -1189,7 +1189,7 @@ fn a_state_texture_slot_takes_an_object_and_a_nil() {
s.run(
r#"
b = CreateFrame("Button", "SlotBtn")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
b:SetNormalTexture("Interface\\N.blp")
-- Bongos' own idiom: build the highlight yourself and hand the object over.
hl = b:CreateTexture()
@@ -1260,7 +1260,7 @@ fn an_unlocked_scripted_push_is_released_by_the_next_mouse_release() {
s.run(
r#"
row = CreateFrame("Button", "TabletRow")
row:SetPoint("BOTTOMLEFT", 0, 0); row:SetSize(100, 100)
row:SetPoint("BOTTOMLEFT", 0, 0); row:SetWidth(100); row:SetHeight(100)
row:SetNormalTexture("Interface\\RowN.blp")
row:SetPushedTexture("Interface\\RowP.blp")
row:SetButtonState("PUSHED") -- Tablet-2.0's call, verbatim: no lock argument
@@ -1301,10 +1301,10 @@ fn a_locked_state_ignores_the_mouse_and_enable_disable_clears_the_lock() {
s.run(
r#"
micro = CreateFrame("Button", "MicroButton")
micro:SetPoint("BOTTOMLEFT", 0, 0); micro:SetSize(100, 100)
micro:SetPoint("BOTTOMLEFT", 0, 0); micro:SetWidth(100); micro:SetHeight(100)
micro:SetButtonState("PUSHED", 1) -- MainMenuBarMicroButtons.lua, verbatim
pin = CreateFrame("Button", "PinnedNormal")
pin:SetPoint("BOTTOMLEFT", 200, 0); pin:SetSize(100, 100)
pin:SetPoint("BOTTOMLEFT", 200, 0); pin:SetWidth(100); pin:SetHeight(100)
pin:SetButtonState("NORMAL", 1)
"#,
)
@@ -1367,7 +1367,7 @@ fn the_hover_is_not_an_input_to_the_press_state() {
s.run(
r#"
b = CreateFrame("Button", "HeldButton")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(100); b:SetHeight(100)
b:SetNormalTexture("Interface\\HeldN.blp")
b:SetPushedTexture("Interface\\HeldP.blp")
"#,
@@ -1416,7 +1416,7 @@ fn hiding_a_held_button_un_presses_it() {
s.run(
r#"
h = CreateFrame("Button", "HidButton")
h:SetPoint("BOTTOMLEFT", 0, 0); h:SetSize(100, 100)
h:SetPoint("BOTTOMLEFT", 0, 0); h:SetWidth(100); h:SetHeight(100)
"#,
)
.unwrap();
@@ -1438,3 +1438,86 @@ fn hiding_a_held_button_un_presses_it() {
s.mouse_button(50.0, 50.0, "LeftButton", false);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `Button:GetTextColor()` — **FOUR** values, r/g/b/a (`0x781100`, table `0x879d00`, argc 1,
/// arity 4, kinds `(number,number,number,number)`). Three is the plausible wrong answer, and the
/// shapes table flags the name `name-not-unique` because seven tables register it — this asserts
/// the BUTTON one, on a Button, and its two inheritance legs.
///
/// Completeness rather than a live break: no corpus site has a Button receiver today (every
/// measured `GetTextColor` is on a FontString or a font object). It is here because the reference
/// registers it, so the widget shape gate can cover it from now on.
#[test]
fn button_get_text_color_answers_four_values_through_the_state_font() {
let s = script();
s.register_font_object(
"BtnGold",
FontObject {
color: Some([1.0, 0.82, 0.0, 1.0]),
height: Some(12.0),
..Default::default()
},
);
s.run(
r#"
Plain = CreateFrame("Button", "PlainBtn")
Themed = CreateFrame("Button", "ThemedBtn")
Themed:SetTextFontObject("BtnGold")
"#,
)
.unwrap();
assert_eq!(
s.eval::<i64>("return select('#', PlainBtn:GetTextColor())")
.unwrap(),
4,
"arity 4 — not 3, the plausible wrong answer"
);
let kinds: String = s
.eval(
r#"local r,g,b,a = PlainBtn:GetTextColor()
return type(r)..","..type(g)..","..type(b)..","..type(a)"#,
)
.unwrap();
assert_eq!(kinds, "number,number,number,number");
// A button with nothing set anywhere is the untinted white every other colour getter here
// (`GetVertexColor`, `FontString:GetTextColor`) answers.
let plain: Vec<f32> = (1..=4)
.map(|i| {
s.eval::<f32>(&format!("return select({i}, PlainBtn:GetTextColor())"))
.unwrap()
})
.collect();
assert_eq!(plain, vec![1.0, 1.0, 1.0, 1.0]);
// With no local colour it reads THROUGH what the normal state inherits — the same leg
// `Button:GetFont` takes, and the one a stock `GameMenuButtonTemplate` button relies on.
let themed: Vec<f32> = (1..=4)
.map(|i| {
s.eval::<f32>(&format!("return select({i}, ThemedBtn:GetTextColor())"))
.unwrap()
})
.collect();
assert_eq!(themed, vec![1.0, 0.82, 0.0, 1.0]);
// A local SetTextColor wins, alpha included, and round-trips.
s.run("ThemedBtn:SetTextColor(0.1, 0.2, 0.3, 0.4)").unwrap();
let set: Vec<f32> = (1..=4)
.map(|i| {
s.eval::<f32>(&format!("return select({i}, ThemedBtn:GetTextColor())"))
.unwrap()
})
.collect();
assert_eq!(set, vec![0.1, 0.2, 0.3, 0.4]);
// A CheckButton reaches it through Button's table, as it does the rest of the trio.
assert_eq!(
s.eval::<i64>(
r#"local c = CreateFrame("CheckButton", "ChkColorBtn")
return select('#', c:GetTextColor())"#
)
.unwrap(),
4
);
}
@@ -14,7 +14,7 @@ fn end_to_end_two_frame_tree_extracts_in_zkey_order() {
-- same strata/level) with a texture and a fontstring. ~30 lines of ordinary FrameXML Lua.
local parent = CreateFrame("Frame", "Root")
parent:SetPoint("TOPLEFT", 0, 0) -- anchored to the screen root
parent:SetSize(400, 300)
parent:SetWidth(400); parent:SetHeight(300)
-- SetAllPoints on each region: a templateless Lua region gets NO implicit anchor
-- (decision 1310 — rect-less, never drawn), so real addon code anchors it, and so do we.
local pbg = parent:CreateTexture(nil, "BACKGROUND")
@@ -23,7 +23,7 @@ fn end_to_end_two_frame_tree_extracts_in_zkey_order() {
local child = CreateFrame("Frame", "Leaf", parent)
child:SetPoint("TOPLEFT", parent, "TOPLEFT", 10, -10)
child:SetSize(100, 50)
child:SetWidth(100); child:SetHeight(50)
local cbg = child:CreateTexture(nil, "ARTWORK")
cbg:SetTexture("Interface\\Child.blp")
cbg:SetAllPoints()
@@ -270,3 +270,112 @@ fn mid_dispatch_removal_of_the_next_stops_and_append_is_visited() {
"a tail-append during dispatch is still visited this dispatch"
);
}
/// `Frame:RegisterAllEvents()` — the frame's `OnEvent` receives every event dispatched
/// (`0x774c20`, table `0x878ec0`, argc 1, arity 0), and `UnregisterAllEvents` clears that state
/// along with the per-event ones.
///
/// **The clearing half is the load-bearing one.** AceEvent-2.0 — shipped by 63 vanilla addons and
/// 6 of the top 20 — turns all-events on once (`AceEvent.frame:RegisterAllEvents()`), deliberately
/// stops calling `frame:UnregisterEvent(event)` while it is on, and gets back to per-event
/// registration by calling `frame:UnregisterAllEvents()` and then re-registering each event it
/// still wants. A `RegisterAllEvents` that survived that call would leave every Ace2 addon on the
/// whole event stream for the session.
#[test]
fn register_all_events_takes_every_event_and_unregister_all_clears_it() {
let mut s = script();
s.run(
r#"
seen = {}
All = CreateFrame("Frame", "AllEv")
All:SetScript("OnEvent", function() table.insert(seen, event) end)
"#,
)
.unwrap();
// Arity 0 — the verb answers nothing.
assert_eq!(
s.eval::<i64>("return select('#', All:RegisterAllEvents())")
.unwrap(),
0
);
// Anything dispatched now reaches it, including an event no name list could have enumerated.
for ev in [
"PLAYER_LOGIN",
"UNIT_HEALTH",
"SOME_SERVER_EVENT_NOBODY_LISTED",
] {
s.fire_event(ev, vec![]);
}
assert_eq!(
s.eval::<i64>("return table.getn(seen)").unwrap(),
3,
"every event dispatched, whatever its name"
);
assert_eq!(
s.eval::<String>("return seen[3]").unwrap(),
"SOME_SERVER_EVENT_NOBODY_LISTED"
);
// Registering twice is a no-op, and a frame holding BOTH an all-events registration and a
// RegisterEvent for the same event is one listener, not two — the same rule `RegisterEvent`'s
// own `if not already in the list` holds one level down.
s.run(r#"seen = {}; All:RegisterAllEvents(); All:RegisterEvent("UNIT_HEALTH")"#)
.unwrap();
s.fire_event("UNIT_HEALTH", vec![]);
assert_eq!(
s.eval::<i64>("return table.getn(seen)").unwrap(),
1,
"fired once, not twice"
);
// UnregisterAllEvents clears BOTH: the explicit UNIT_HEALTH registration and the all-events one.
s.run("seen = {}; All:UnregisterAllEvents()").unwrap();
for ev in ["UNIT_HEALTH", "PLAYER_LOGIN"] {
s.fire_event(ev, vec![]);
}
assert_eq!(
s.eval::<i64>("return table.getn(seen)").unwrap(),
0,
"the all-events registration does not outlive UnregisterAllEvents"
);
// …and the AceEvent path back: re-register the individual events it still wants.
s.run(r#"All:RegisterEvent("UNIT_HEALTH")"#).unwrap();
for ev in ["UNIT_HEALTH", "PLAYER_LOGIN"] {
s.fire_event(ev, vec![]);
}
assert_eq!(
s.eval::<i64>("return table.getn(seen)").unwrap(),
1,
"back to exactly one event"
);
assert!(s.take_errors().is_empty());
}
/// An all-events frame dispatches AFTER the event's own listeners — where it would sit if the
/// registration were expanded into every per-event list, since it joined later than they did.
/// Cross-frame order is a law consumers depend on (the two ZoneText frames writing one FontString);
/// it does not stop being one because a listener asked for everything.
#[test]
fn an_all_events_listener_runs_after_the_events_own() {
let mut s = script();
s.run(
r#"
order = {}
Named = CreateFrame("Frame", "NamedEv")
Named:SetScript("OnEvent", function() table.insert(order, "named") end)
Named:RegisterEvent("PLAYER_LOGIN")
Everything = CreateFrame("Frame", "EveryEv")
Everything:SetScript("OnEvent", function() table.insert(order, "all") end)
Everything:RegisterAllEvents()
"#,
)
.unwrap();
s.fire_event("PLAYER_LOGIN", vec![]);
assert_eq!(s.eval::<String>("return order[1]").unwrap(), "named");
assert_eq!(s.eval::<String>("return order[2]").unwrap(), "all");
assert_eq!(s.eval::<i64>("return table.getn(order)").unwrap(), 2);
}
@@ -1,9 +1,15 @@
//! Small frame-API wins: GetLocale, SetAllPoints, SetFormattedText, SetShown, and GetPoint readback.
//! Small frame-API wins: GetLocale, SetAllPoints, and GetPoint readback.
use super::common::script;
/// Two era names, asserted together because neither earns a file of its own.
///
/// This test carried two more legs until decision 2142, `SetFormattedText` and `SetShown`, and
/// both went with the verbs: neither is in a 1.12 method table. What the first of them actually
/// proved — that `%N$s` reorders rather than consumes — is `string.format`'s own behaviour and is
/// pinned where it lives, in [`crate::strings`].
#[test]
fn consensus_api_small_wins() {
fn get_locale_and_set_all_points() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
// GetLocale — benilla is enUS-data-only.
@@ -12,13 +18,11 @@ fn consensus_api_small_wins() {
s.run(
r#"
p = CreateFrame("Frame", "SAP_Parent")
p:SetPoint("BOTTOMLEFT", 100, 100); p:SetSize(200, 50)
p:SetPoint("BOTTOMLEFT", 100, 100); p:SetWidth(200); p:SetHeight(50)
c = CreateFrame("Frame", "SAP_Child", p)
c:SetAllPoints() -- default: the parent
f = CreateFrame("Frame", "SAP_Free")
f:SetAllPoints("SAP_Parent") -- by name
fs = p:CreateFontString(nil, "OVERLAY")
fs:SetFormattedText("%2$s %1$s (%3$d)", "world", "hello", 7)
"#,
)
.unwrap();
@@ -34,20 +38,6 @@ fn consensus_api_small_wins() {
)
.unwrap();
assert!(ok, "SetAllPoints matched the target rect");
// SetFormattedText routes through the positional-aware format.
assert_eq!(
s.eval::<String>("return fs:GetText()").unwrap(),
"hello world (7)"
);
// SetShown: truthy shows, nil/false hides.
s.run("p:SetShown(false)").unwrap();
assert!(!s.eval::<bool>("return p:IsShown()").unwrap());
s.run("p:SetShown(1)").unwrap();
assert!(s.eval::<bool>("return p:IsShown()").unwrap());
s.run("p:SetShown(nil)").unwrap();
assert!(!s.eval::<bool>("return p:IsShown()").unwrap());
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
+27 -27
View File
@@ -12,10 +12,10 @@ fn enable_mouse_gates_hit_testing() {
-- Two full-screen frames. `b` is created later (drawn on top) but mouse-disabled, so it is
-- transparent to hits; the enabled frame behind it (`a`) must capture.
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
a:SetScript("OnEnter", function(self) who = self:GetName() end)
local b = CreateFrame("Frame", "B")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(800, 600); b:EnableMouse(false)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(false)
b:SetScript("OnEnter", function(self) who = self:GetName() end)
-- 1/nil, not a boolean (1830): this is the exact comparison shape that inverts, so the
-- test asserts it rather than leaning on truthiness.
@@ -51,10 +51,10 @@ fn hit_order_is_strata_then_level_then_the_earlier_linked_frame() {
r#"
who = nil
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
a:SetScript("OnEnter", function(self) who = self:GetName() end)
local b = CreateFrame("Frame", "B")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(800, 600); b:EnableMouse(true)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(800); b:SetHeight(600); b:EnableMouse(true)
b:SetScript("OnEnter", function(self) who = self:GetName() end)
"#,
)
@@ -96,7 +96,7 @@ fn mouse_move_fires_enter_then_leave_across_a_boundary_with_correct_self() {
enters, leaves = 0, 0
enter_self_ok, leave_self_ok = false, false
local a = CreateFrame("Frame", "A") -- left half only
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
a:SetScript("OnEnter", function(self) enters = enters + 1; enter_self_ok = (self == a) end)
a:SetScript("OnLeave", function(self) leaves = leaves + 1; leave_self_ok = (self == a) end)
"#,
@@ -127,10 +127,10 @@ fn onclick_fires_on_press_release_same_frame_not_when_release_lands_elsewhere()
r#"
clicks_a, clicks_b, click_btn = 0, 0, nil
local a = CreateFrame("Frame", "A") -- left half
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
a:SetScript("OnClick", function(self, button, down) clicks_a = clicks_a + 1; click_btn = button end)
local b = CreateFrame("Frame", "B") -- right half
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetSize(400, 600); b:EnableMouse(true)
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true)
b:SetScript("OnClick", function(self, button, down) clicks_b = clicks_b + 1 end)
"#,
)
@@ -168,14 +168,14 @@ fn hidden_or_effective_hidden_frame_never_captures() {
entered = false
-- own shown = false
local own = CreateFrame("Frame", "OwnHidden")
own:SetPoint("BOTTOMLEFT", 0, 0); own:SetSize(800, 600); own:EnableMouse(true)
own:SetPoint("BOTTOMLEFT", 0, 0); own:SetWidth(800); own:SetHeight(600); own:EnableMouse(true)
own:SetScript("OnEnter", function() entered = true end)
own:Hide()
-- effective-hidden: child is shown but its parent is hidden
local parent = CreateFrame("Frame", "Par")
parent:SetPoint("BOTTOMLEFT", 0, 0); parent:SetSize(800, 600)
parent:SetPoint("BOTTOMLEFT", 0, 0); parent:SetWidth(800); parent:SetHeight(600)
local child = CreateFrame("Frame", "Ch", parent)
child:SetPoint("BOTTOMLEFT", 0, 0); child:SetSize(800, 600); child:EnableMouse(true)
child:SetPoint("BOTTOMLEFT", 0, 0); child:SetWidth(800); child:SetHeight(600); child:EnableMouse(true)
child:SetScript("OnEnter", function() entered = true end)
parent:Hide()
"#,
@@ -203,7 +203,7 @@ fn mouse_wheel_passes_delta_to_the_captured_frame() {
r#"
wheel = nil
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(800); a:SetHeight(600); a:EnableMouse(true)
-- The wheel is its own index and its own flag: SetScript never auto-enables (that law is
-- XML-load-time only), so a runtime-created frame needs this explicitly, exactly as it
-- needs EnableMouse. Mouse-enablement alone puts nothing in the wheel index.
@@ -231,7 +231,7 @@ fn hit_rect_insets_shrink_the_mouse_rect_only() {
r#"
entered = false
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(100, 100); a:EnableMouse(true)
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(100); a:SetHeight(100); a:EnableMouse(true)
a:SetScript("OnEnter", function() entered = true end)
a:SetHitRectInsets(5, 5, 18, 5)
local l, r, t, b = a:GetHitRectInsets()
@@ -375,7 +375,7 @@ fn a_second_fast_click_fires_on_double_click_instead_of_the_second_on_click() {
r#"
clicks, doubles, dblbtn = 0, 0, nil
local b = CreateFrame("Button", "DblB")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
b:SetScript("OnClick", function(self, button) clicks = clicks + 1 end)
b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end)
"#,
@@ -432,7 +432,7 @@ fn a_frame_with_no_double_click_handler_keeps_every_rapid_on_click() {
r#"
clicks = 0
local b = CreateFrame("Button", "NoDbl")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
b:SetScript("OnClick", function() clicks = clicks + 1 end)
"#,
)
@@ -465,9 +465,9 @@ fn the_double_click_boundary_is_time_and_frame_and_clicks_pair_up() {
f:SetScript("OnDoubleClick", function() doubles = doubles + 1 end)
end
local a = CreateFrame("Button", "DblA")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(100, 100); a:EnableMouse(true); watch(a)
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(100); a:SetHeight(100); a:EnableMouse(true); watch(a)
local b = CreateFrame("Button", "DblBB")
b:SetPoint("BOTTOMLEFT", 300, 0); b:SetSize(100, 100); b:EnableMouse(true); watch(b)
b:SetPoint("BOTTOMLEFT", 300, 0); b:SetWidth(100); b:SetHeight(100); b:EnableMouse(true); watch(b)
"#,
)
.unwrap();
@@ -522,7 +522,7 @@ fn a_multi_registered_button_pairs_a_left_click_with_a_right_one() {
r#"
doubles, dblbtn = 0, nil
local b = CreateFrame("Button", "MixDbl")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
b:SetScript("OnDoubleClick", function(self, button) doubles = doubles + 1 dblbtn = button end)
"#,
@@ -555,7 +555,7 @@ fn a_half_finished_double_click_survives_the_cursor_leaving_the_window() {
r#"
doubles = 0
local b = CreateFrame("Button", "LeaveDbl")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(200, 200); b:EnableMouse(true)
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetWidth(200); b:SetHeight(200); b:EnableMouse(true)
b:SetScript("OnDoubleClick", function() doubles = doubles + 1 end)
"#,
)
@@ -585,7 +585,7 @@ fn set_script_on_double_click_is_accepted_because_something_fires_it() {
r#"
ran = false
Btn = CreateFrame("Button", "DblAccepted")
Btn:SetPoint("BOTTOMLEFT", 0, 0); Btn:SetSize(100, 100); Btn:EnableMouse(true)
Btn:SetPoint("BOTTOMLEFT", 0, 0); Btn:SetWidth(100); Btn:SetHeight(100); Btn:EnableMouse(true)
Btn:SetScript("OnDoubleClick", function() ran = true end)
"#,
)
@@ -625,8 +625,8 @@ fn the_unfired_script_kinds_still_raise_rather_than_silently_accepting() {
"OnAttributeChanged",
// Real 1.12 slots we do not fire, and zero corpus call sites. (`OnUpdateModel` and
// `OnAnimFinished` left this list with decision 2007: the tick's model pass fires
// them — `script::tests::model_clock`.)
"OnHorizontalScroll",
// them — `script::tests::model_clock`. `OnHorizontalScroll` left it with the ScrollFrame's
// horizontal offset pair, which fires it — `script::tests::scrollframe`.)
"OnHyperlinkEnter",
"OnMessageScrollChanged",
"OnInputLanguageChanged",
@@ -653,9 +653,9 @@ fn hiding_the_hovered_frame_fires_its_onleave_before_onhide() {
r#"
log = {}
local win = CreateFrame("Frame", "Win")
win:SetPoint("BOTTOMLEFT", 0, 0); win:SetSize(800, 600)
win:SetPoint("BOTTOMLEFT", 0, 0); win:SetWidth(800); win:SetHeight(600)
local slot = CreateFrame("Button", "Slot", win)
slot:SetPoint("BOTTOMLEFT", 100, 100); slot:SetSize(100, 100); slot:EnableMouse(true)
slot:SetPoint("BOTTOMLEFT", 100, 100); slot:SetWidth(100); slot:SetHeight(100); slot:EnableMouse(true)
slot:SetScript("OnLeave", function() table.insert(log, "leave") end)
slot:SetScript("OnHide", function() table.insert(log, "hide") end)
"#,
@@ -682,7 +682,7 @@ fn hiding_an_unhovered_frame_fires_no_onleave() {
r#"
leaves = 0
local a = CreateFrame("Button", "Away")
a:SetPoint("BOTTOMLEFT", 500, 500); a:SetSize(50, 50); a:EnableMouse(true)
a:SetPoint("BOTTOMLEFT", 500, 500); a:SetWidth(50); a:SetHeight(50); a:EnableMouse(true)
a:SetScript("OnLeave", function() leaves = leaves + 1 end)
"#,
)
@@ -706,10 +706,10 @@ fn the_repick_re_hovers_without_a_mouse_move() {
r#"
entered = {}
local under = CreateFrame("Button", "Under")
under:SetPoint("BOTTOMLEFT", 100, 100); under:SetSize(100, 100); under:EnableMouse(true)
under:SetPoint("BOTTOMLEFT", 100, 100); under:SetWidth(100); under:SetHeight(100); under:EnableMouse(true)
under:SetScript("OnEnter", function() table.insert(entered, "under") end)
local over = CreateFrame("Button", "Over")
over:SetPoint("BOTTOMLEFT", 100, 100); over:SetSize(100, 100); over:EnableMouse(true)
over:SetPoint("BOTTOMLEFT", 100, 100); over:SetWidth(100); over:SetHeight(100); over:EnableMouse(true)
over:SetScript("OnEnter", function() table.insert(entered, "over") end)
-- Say "on top" with the LEVEL, not with declaration order: at equal levels the sweep takes
-- the earlier-linked frame (`Under`), which is the tie law, not what this test is about.
@@ -752,7 +752,7 @@ fn two_frames() -> crate::script::UiScript {
log = {}
local function mk(name, x)
local f = CreateFrame("Frame", name)
f:SetPoint("BOTTOMLEFT", x, 200); f:SetSize(200, 200); f:EnableMouse(true)
f:SetPoint("BOTTOMLEFT", x, 200); f:SetWidth(200); f:SetHeight(200); f:EnableMouse(true)
f:SetScript("OnMouseDown", function(self) table.insert(log, self:GetName()..":down") end)
f:SetScript("OnMouseUp", function(self) table.insert(log, self:GetName()..":up") end)
return f
@@ -184,3 +184,90 @@ fn a_raising_handler_still_consumes_and_is_recorded() {
s.errors()
);
}
/// **A `CSimpleEditBox` in the walk is asked about FOCUS, never about a script slot** (§1's vtable
/// table + `0x77a900`). Its ctor registers it in both key buckets, and vtable `0x81c910` replaces
/// `+0x5c`/`+0x60` with overrides that *never chain to the base* — "there is no `call 0x76b760`
/// anywhere in it, so the generic `OnChar` slot `+0x180` is unreachable on an editbox". An
/// unfocused box therefore takes the `0x77a956 xor eax,eax` leg: decline, walk continues.
///
/// Modelling the box as a plain frame here is not a subtle divergence. Stock
/// `SendMailNameEditBox` carries an XML `<OnChar>` (`SendMailFrame_SendeeAutocomplete`), which
/// auto-enables it, and it registers first in the mail window — so it consumed every keystroke and
/// the send tab's other four boxes took no input at all (decision 2145).
#[test]
fn an_unfocused_editbox_declines_rather_than_eating_its_neighbours_keys() {
let mut s = script();
s.run(
r#"
decoyChars = 0
decoy = CreateFrame("EditBox", "KbDecoyBox")
decoy:SetAutoFocus(false)
decoy:EnableKeyboard(true)
decoy:SetScript("OnChar", function() decoyChars = decoyChars + 1 end)
target = CreateFrame("EditBox", "KbTargetBox")
target:SetAutoFocus(false)
target:SetFocus()
"#,
)
.unwrap();
assert!(s.char_input("k"), "the focused box consumes");
assert_eq!(
s.eval::<String>("return KbTargetBox:GetText()").unwrap(),
"k",
"the keystroke reaches the box that holds the focus"
);
assert_eq!(
s.eval::<i64>("return decoyChars").unwrap(),
0,
"the unfocused box's own OnChar is unreachable from the walk — it declines"
);
// Give the decoy the focus and its handler is reachable again — through the INSERT, which is
// where an editbox's `OnChar` is fired from (`0x77c200`), not through the walk's base gate.
s.run("KbDecoyBox:SetFocus()").unwrap();
assert!(s.char_input("j"), "…and now it is the one that consumes");
assert_eq!(s.eval::<i64>("return decoyChars").unwrap(), 1);
assert_eq!(
s.eval::<String>("return KbDecoyBox:GetText()").unwrap(),
"j"
);
assert_eq!(
s.eval::<String>("return KbTargetBox:GetText()").unwrap(),
"k",
"and the box that lost focus keeps what it had"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The same clause on the key-down channel (`+0x60` → `0x77b160`, focus guard at `0x77b1c7`), which
/// is the [`super::super::keyboard::frame_key_input`] walk: an unfocused box carrying only an
/// `OnKeyUp` must not swallow a BACKSPACE the focused box is waiting for. The plain-frame
/// asymmetry above is real *for plain frames*; on a box the base gate is not reached at all.
#[test]
fn an_unfocused_editbox_does_not_swallow_a_focused_boxs_editing_key() {
let mut s = script();
s.run(
r#"
decoy = CreateFrame("EditBox", "KbDecoyBox2")
decoy:SetAutoFocus(false)
decoy:EnableKeyboard(true)
decoy:SetScript("OnKeyUp", function() end)
target = CreateFrame("EditBox", "KbTargetBox2")
target:SetAutoFocus(false)
target:SetText("ab")
target:SetFocus()
"#,
)
.unwrap();
s.editbox_action(crate::script::EditAction::Delete {
unit: crate::script::EditUnit::Char,
back: true,
});
assert_eq!(
s.eval::<String>("return KbTargetBox2:GetText()").unwrap(),
"a",
"the backspace reached the focused box"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
@@ -187,7 +187,7 @@ fn the_hover_re_enter_loop_neither_re_measures_nor_re_solves() {
s.run(
r#"
local owner = CreateFrame("Button", "Slot")
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
tt = CreateFrame("GameTooltip", "TT")
-- One bag-slot OnEnter: the clear (SetOwner) + the content rebuild, verbatim in shape.
function reenter()
+16 -15
View File
@@ -14,13 +14,13 @@ fn measured_fontstring_height_feeds_frame_anchors() {
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(384); w:SetHeight(512)
local g = w:CreateFontString("Greeting", "ARTWORK")
g:SetText("a long greeting that wraps")
g:SetWidth(270)
g:SetPoint("TOPLEFT", 33, -91)
local row = CreateFrame("Button", "Row1")
row:SetSize(300, 16)
row:SetWidth(300); row:SetHeight(16)
row:SetPoint("TOPLEFT", "Greeting", "BOTTOMLEFT", -10, -20)
"#,
)
@@ -74,7 +74,7 @@ fn a_changed_text_reads_zero_until_its_own_measure_lands() {
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(384); w:SetHeight(512)
local h = w:CreateFontString("Header", "ARTWORK")
h:SetText("Say: ")
h:SetPoint("LEFT", 13, 0)
@@ -129,14 +129,14 @@ fn zero_width_fontstring_autosizes_to_its_line() {
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(384); w:SetHeight(512)
local label = w:CreateFontString("FromLabel", "ARTWORK")
label:SetText("From:")
label:SetSize(0, 16)
label:SetWidth(0); label:SetHeight(16)
label:SetPoint("TOPRIGHT", "Win", "TOPLEFT", 114, -45)
local value = w:CreateFontString("FromValue", "ARTWORK")
value:SetText("Thrall")
value:SetSize(110, 0)
value:SetWidth(110); value:SetHeight(0)
value:SetPoint("LEFT", "FromLabel", "RIGHT", 5, 0)
"#,
)
@@ -181,7 +181,7 @@ fn frame_scale_rides_the_quads_and_the_measure_key() {
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(400, 300)
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(400); w:SetHeight(300)
w:SetScale(0.8)
local t = w:CreateFontString("ScaledLabel", "ARTWORK")
t:SetText("Options")
@@ -234,7 +234,7 @@ fn invalidate_text_measures_reopens_the_round_trip() {
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(400, 300)
w:SetPoint("TOPLEFT", 0, -100); w:SetWidth(400); w:SetHeight(300)
local t = w:CreateFontString("Label", "ARTWORK")
t:SetText("Keybindings")
t:SetPoint("TOPLEFT", 10, -10)
@@ -382,7 +382,7 @@ fn a_width_read_in_the_tick_that_set_the_text_is_not_zero() {
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
-- the corpus idiom: set, then measure, in one statement sequence
@@ -408,7 +408,7 @@ fn with_no_engine_installed_the_round_trip_is_still_the_only_answer() {
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetText("Onewarrior")
@@ -435,7 +435,7 @@ fn resolve_closes_the_round_trip_when_an_engine_is_installed() {
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetWidth(35) -- a declared width ⇒ the two extents differ
@@ -469,7 +469,7 @@ fn a_synchronous_measure_satisfies_the_batch_request_too() {
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetText("hello")
@@ -505,7 +505,7 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetText("one")
@@ -514,7 +514,8 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
.unwrap();
let writes: &[(&str, &str)] = &[
("SetText", "Label:SetText('two')"),
("SetFormattedText", "Label:SetFormattedText('n=%d', 3)"),
// `SetFormattedText` was a second row here; it is gone (2142), and its era spelling
// `SetText(format(...))` is the row above's path exactly.
(
"SetFont",
r#"Label:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")"#,
@@ -550,7 +551,7 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
s.run(
r#"
local b = CreateFrame("Button", "Btn")
b:SetPoint("TOPLEFT", 0, -50); b:SetSize(80, 22)
b:SetPoint("TOPLEFT", 0, -50); b:SetWidth(80); b:SetHeight(22)
"#,
)
.unwrap();
@@ -49,6 +49,7 @@ mod tooltip_spell;
mod tooltip_unit;
mod toplevel;
mod visibility;
mod widget_surface;
mod worldframe;
mod worldmap;
@@ -17,7 +17,7 @@ fn movable_panel() -> UiScript {
starts, stops = 0, 0
Panel = CreateFrame("Frame", "MovePanel")
Panel:SetPoint("BOTTOMLEFT", 100, 100)
Panel:SetSize(200, 80)
Panel:SetWidth(200); Panel:SetHeight(80)
Panel:EnableMouse(true)
Panel:SetMovable(true)
Panel:RegisterForDrag("LeftButton")
@@ -113,7 +113,7 @@ fn start_moving_on_a_frame_that_is_not_movable_raises_and_moves_nothing() {
r#"
Fixed = CreateFrame("Frame", "FixedPanel")
Fixed:SetPoint("BOTTOMLEFT", 100, 100)
Fixed:SetSize(200, 80)
Fixed:SetWidth(200); Fixed:SetHeight(80)
ok, err = pcall(function() Fixed:StartMoving() end)
"#,
)
@@ -153,7 +153,7 @@ fn stop_moving_or_sizing_is_harmless_with_nothing_moving_and_stops_only_its_own_
r#"
Other = CreateFrame("Frame", "OtherPanel")
Other:SetPoint("BOTTOMLEFT", 400, 400)
Other:SetSize(50, 50)
Other:SetWidth(50); Other:SetHeight(50)
MovePanel:StopMovingOrSizing() -- nothing is moving
MovePanel:StopMovingOrSizing() -- twice
OtherPanel:StopMovingOrSizing()
@@ -191,7 +191,7 @@ fn a_frame_stretched_between_two_anchors_moves_rigidly() {
s.run(
r#"
Back = CreateFrame("Frame", "StretchBack")
Back:SetPoint("BOTTOMLEFT", 0, 0); Back:SetSize(800, 600)
Back:SetPoint("BOTTOMLEFT", 0, 0); Back:SetWidth(800); Back:SetHeight(600)
Stretch = CreateFrame("Frame", "StretchPanel", Back)
Stretch:SetPoint("BOTTOMLEFT", Back, "BOTTOMLEFT", 100, 100)
Stretch:SetPoint("TOPRIGHT", Back, "BOTTOMLEFT", 300, 200)
@@ -229,7 +229,7 @@ fn a_scaled_frame_tracks_the_cursor_one_to_one_on_screen() {
r#"
Scaled = CreateFrame("Frame", "ScaledPanel")
Scaled:SetPoint("BOTTOMLEFT", 100, 100)
Scaled:SetSize(100, 100)
Scaled:SetWidth(100); Scaled:SetHeight(100)
Scaled:SetScale(2)
Scaled:SetMovable(true)
Scaled:StartMoving()
@@ -266,7 +266,7 @@ fn the_three_flags_default_off_round_trip_and_user_placed_is_guarded() {
s.run(
r#"
F = CreateFrame("Frame", "FlagPanel")
F:SetPoint("BOTTOMLEFT", 10, 10); F:SetSize(50, 50)
F:SetPoint("BOTTOMLEFT", 10, 10); F:SetWidth(50); F:SetHeight(50)
"#,
)
.unwrap();
@@ -425,7 +425,7 @@ fn a_title_region_is_a_plain_region_and_creating_it_twice_is_destructive() {
r#"
TFrame = CreateFrame("Frame", "TFrame")
TFrame:SetPoint("BOTTOMLEFT", 100, 100)
TFrame:SetSize(200, 80)
TFrame:SetWidth(200); TFrame:SetHeight(80)
"#,
)
.unwrap();
@@ -549,7 +549,7 @@ fn a_title_region_drag_swallows_the_press_and_ends_on_release() {
downs = 0
TP = CreateFrame("Frame", "TP")
TP:SetPoint("BOTTOMLEFT", 100, 100)
TP:SetSize(200, 80)
TP:SetWidth(200); TP:SetHeight(80)
TP:EnableMouse(true)
TP:SetScript("OnMouseDown", function() downs = downs + 1 end)
TP:CreateTitleRegion():SetAllPoints(TP)
@@ -29,10 +29,10 @@ fn setpoint_relative_to_resolves_an_alias_global() {
-- The reference UI's own frames.
Bar = CreateFrame("Frame", "Bar")
Bar:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 50)
Bar:SetSize(200, 40)
Bar:SetWidth(200); Bar:SetHeight(40)
for i = 1, 5 do
local b = CreateFrame("Button", "StockButton" .. i, Bar)
b:SetSize(30, 30)
b:SetWidth(30); b:SetHeight(30)
end
-- The addon's aliases: plain globals, no frame's name (Bartender2's Alias.lua).
@@ -84,14 +84,14 @@ fn setpoint_relative_to_resolves_a_region_alias_global() {
r#"
Panel = CreateFrame("Frame", "Panel")
Panel:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
Panel:SetSize(400, 300)
Panel:SetWidth(400); Panel:SetHeight(300)
local tex = Panel:CreateTexture("PanelSwatch", "ARTWORK")
tex:SetPoint("BOTTOMLEFT", "Panel", "BOTTOMLEFT", 10, 20)
tex:SetSize(50, 50)
tex:SetWidth(50); tex:SetHeight(50)
Swatch = PanelSwatch -- the alias
Tag = CreateFrame("Frame", "Tag", Panel)
Tag:SetSize(10, 10)
Tag:SetWidth(10); Tag:SetHeight(10)
Tag:SetPoint("BOTTOMLEFT", "Swatch", "BOTTOMRIGHT", 4, 0)
"#,
)
@@ -117,10 +117,10 @@ fn setpoint_relative_to_ignores_a_non_widget_global() {
r#"
Host = CreateFrame("Frame", "Host")
Host:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 10, 10)
Host:SetSize(100, 100)
Host:SetWidth(100); Host:SetHeight(100)
NotAWidget = 5
Child = CreateFrame("Frame", "Child", Host)
Child:SetSize(20, 20)
Child:SetWidth(20); Child:SetHeight(20)
Child:SetPoint("BOTTOMLEFT", "NotAWidget", "BOTTOMRIGHT", 0, 0)
"#,
)
@@ -150,17 +150,17 @@ fn setpoint_relative_to_expands_parent_against_the_first_named_ancestor() {
r#"
Root = CreateFrame("Frame", "Root")
Root:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
Root:SetSize(300, 200)
Root:SetWidth(300); Root:SetHeight(200)
Peg = CreateFrame("Frame", "RootPeg", Root)
Peg:SetPoint("BOTTOMLEFT", "Root", "BOTTOMLEFT", 20, 30)
Peg:SetSize(10, 10)
Peg:SetWidth(10); Peg:SetHeight(10)
-- An ANONYMOUS link in between: the walk skips it and lands on Root.
Anon = CreateFrame("Frame", nil, Root)
Anon:SetAllPoints(Root)
Kid = CreateFrame("Frame", "Kid", Anon)
Kid:SetSize(5, 5)
Kid:SetWidth(5); Kid:SetHeight(5)
Kid:SetPoint("BOTTOMLEFT", "$parentPeg", "BOTTOMRIGHT", 0, 0)
"#,
)
@@ -227,7 +227,7 @@ fn setallpoints_resolves_an_alias_global() {
r#"
Plate = CreateFrame("Frame", "Plate")
Plate:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 30, 40)
Plate:SetSize(120, 60)
Plate:SetWidth(120); Plate:SetHeight(60)
PlateAlias = Plate
Overlay = CreateFrame("Frame", "Overlay")
Overlay:SetAllPoints("PlateAlias")
@@ -48,8 +48,15 @@ fn reference() -> Vec<(String, String)> {
fn allowed_beyond_1_12() -> HashSet<&'static str> {
[
// ── our Lua runtime is 5.1 where 1.12's is 5.0 ────────────────────────────────────────
// `select` is load-bearing and stays on purpose — our transcribed FrameXML uses it in 16
// files as the 5.1 spelling of 5.0's implicit `arg` table.
// `select`'s old reason here — "our transcribed FrameXML uses it in 16 files as the 5.1
// spelling of 5.0's implicit `arg` table" — **was true and is not any more**: 1751's
// stock-first migration retired those files, and `assets/ui`'s seven survivors contain
// zero `select(` call sites (measured 2026-09-09, decision 2142). What keeps it is now a
// different fact, and a weaker one: it is mlua's, not ours to remove without replacing the
// 5.1 varargs it comes with. The corpus reads it as a version probe —
// `pfUI/libs/libpredict.lua:141` and `ShaguTweaks/libs/libpredict.lua:82` both branch on
// `elseif select and UnitCastingInfo then`, and only our NOT having `UnitCastingInfo`
// keeps them off the TBC cast path.
//
// `_G` is the last one inherited rather than chosen: 1.12's base library does not export
// it (an addon reaches the globals with `getfenv(0)`, which is what AceLibrary does), but
@@ -64,9 +71,38 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
"_G",
"select",
// ── WoW API past 1.12 — 1188 phase 5's list, and the reason that phase exists ─────────
// Every one of these predates 1189 and is used by our own transcribed FrameXML today.
// Resolving each means either replacing it with its 1.12 equivalent (`UnitPower` →
// `UnitMana`, which 1.12 has and we do not) or recording why it stays.
// **The reason written here was "every one of these is used by our own transcribed
// FrameXML today", and as of 2026-09-09 that is true of exactly ONE of the sixteen**
// (`SubmitChatInput`, in the dev-only `ScriptLogFrame.xml`) — a corpus census measured it
// and this file's own claim was checked against `assets/ui`'s seven remaining files, name
// by name, before this comment was rewritten (decision 2142). 1751's migration retired the
// windows that used the rest; the list outlived its justification, which is exactly how
// 2118 found the modifier keys still answering booleans years after 0068's reason for it
// had gone.
//
// **What they are NOT is load-bearing for the addon corpus, and 2143 corrects 2142 for
// saying so.** The claim written here was that the vanilla ecosystem assumes several of
// these exist — `pfUI/modules/loot.lua` calling `wipe` 25 times and defining it nowhere,
// so ours "makes that module run at all". That is wrong twice over, and the error is
// instructive: **a "1.12 addon" in that corpus is usually a MULTI-CLIENT addon**. pfUI
// ships one codebase for three clients and gates per module —
// `pfUI:RegisterModule("loot", "vanilla:tbc", …)`, matched by
// `strfind(version, pfUI.expansion)` at `pfUI.lua:250` — so a TBC-era habit sits in code
// the vanilla client reaches. The `wipe` calls are in `RequestRolls`,
// `BuildSpecialRecipientsMenu` and `ClearRolls`: the master-looter's roll helper, not the
// loot window, and that path throws on a real 1.12 client too. And `strsplit` was worse —
// pfUI has its own `pfUI.api.strsplit` (`api/api.lua:16`) and ShaguTweaks its own
// `ShaguTweaks.strsplit`; the bare calls are inside the vendored `libs/libpredict.lua`,
// which has TBC branches of its own.
//
// So our superset was HIDING those latent nil-calls, never enabling anything. The one
// real interaction left is the other direction: `AtlasLoot/Core/AtlasLoot.lua:2119`/`:2148`
// define GLOBAL `strsplit`/`strtrim` unconditionally, with incompatible pattern-based
// two-return semantics, and clobber ours for everything loaded after them — so ours is not
// authoritative at runtime either.
//
// Resolving each means replacing it with its 1.12 equivalent (`UnitPower` → `UnitMana`,
// which 1.12 has and we do not), deleting it, or recording why it stays.
//
// Three left with 1751's gossip work: `GetNumGossipQuests`, `GetGossipQuestInfo` and
// `SelectGossipQuest` were benilla's own single-list shape over the gossip packet's quest
@@ -332,12 +368,12 @@ fn region_set_parent_relinks_the_draw_owner_and_leaves_anchors_alone() {
s.run(
r#"
A = CreateFrame("Frame", "SPOwnerA")
A:SetPoint("BOTTOMLEFT", 0, 0) A:SetSize(100, 50)
A:SetPoint("BOTTOMLEFT", 0, 0) A:SetWidth(100); A:SetHeight(50)
B = CreateFrame("Frame", "SPOwnerB")
B:SetPoint("BOTTOMLEFT", 300, 0) B:SetSize(100, 50)
B:SetPoint("BOTTOMLEFT", 300, 0) B:SetWidth(100); B:SetHeight(50)
Spark = A:CreateTexture("SPSpark", "ARTWORK")
Spark:SetTexture("Interface\\SPGlow")
Spark:SetSize(24, 24)
Spark:SetWidth(24); Spark:SetHeight(24)
Spark:SetPoint("TOPLEFT", 4, -4)
"#,
)
@@ -1161,7 +1197,7 @@ fn every_region_map_method_is_callable_on_a_texture_and_a_fontstring() {
s.run(
r#"
RMOwner = CreateFrame("Frame", "RMOwner")
RMOwner:SetPoint("BOTTOMLEFT", 0, 0) RMOwner:SetSize(100, 50)
RMOwner:SetPoint("BOTTOMLEFT", 0, 0) RMOwner:SetWidth(100); RMOwner:SetHeight(50)
RMTex = RMOwner:CreateTexture("RMTex", "ARTWORK")
RMStr = RMOwner:CreateFontString("RMStr", "ARTWORK")
"#,
@@ -1197,9 +1233,9 @@ fn the_region_map_readers_answer_the_way_the_edges_do() {
s.run(
r#"
RRFrame = CreateFrame("Frame", "RRFrame")
RRFrame:SetPoint("BOTTOMLEFT", 100, 200) RRFrame:SetSize(200, 100)
RRFrame:SetPoint("BOTTOMLEFT", 100, 200) RRFrame:SetWidth(200); RRFrame:SetHeight(100)
RRPlate = RRFrame:CreateTexture("RRPlate", "ARTWORK")
RRPlate:SetPoint("TOPLEFT", 10, -10) RRPlate:SetSize(50, 20)
RRPlate:SetPoint("TOPLEFT", 10, -10) RRPlate:SetWidth(50); RRPlate:SetHeight(20)
-- the sibling-region anchor the real XML uses everywhere
RRLabel = RRFrame:CreateFontString("RRLabel", "OVERLAY")
RRLabel:SetPoint("LEFT", RRPlate, "RIGHT", 4, 0)
+179 -31
View File
@@ -40,10 +40,10 @@ fn region_texture_anchored_topleft_resolves_exact_rect() {
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [bottom 0, left 0, top 50, right 100]
f:SetSize(100, 50)
f:SetWidth(100); f:SetHeight(50)
local t = f:CreateTexture("Tex", "ARTWORK")
t:SetTexture("Interface\\Icon")
t:SetSize(24, 24)
t:SetWidth(24); t:SetHeight(24)
t:SetPoint("TOPLEFT", 4, -4)
"#,
)
@@ -69,7 +69,7 @@ fn region_fontstring_span_floors_at_one_unit_until_measured() {
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
f:SetSize(100, 50)
f:SetWidth(100); f:SetHeight(50)
local fs = f:CreateFontString("FS", "ARTWORK")
fs:SetText("Name")
fs:SetJustifyH("LEFT")
@@ -116,7 +116,7 @@ fn a_zero_width_solid_spans_eight_units_and_its_artless_twin_gets_no_rect() {
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
f:SetSize(100, 50)
f:SetWidth(100); f:SetHeight(50)
Fill = f:CreateTexture("Fill", "BORDER")
Fill:SetTexture(1, 1, 1, 1) -- the <Color> form: an 8x8 solid
Fill:SetWidth(0); Fill:SetHeight(13)
@@ -153,10 +153,10 @@ fn templateless_lua_region_without_anchors_never_draws() {
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
f:SetSize(100, 50)
f:SetWidth(100); f:SetHeight(50)
local t = f:CreateTexture("Tex", "ARTWORK")
t:SetTexture("Interface\\Ring")
t:SetSize(24, 24) -- no anchors, and no template ⇒ no rect, no draw
t:SetWidth(24); t:SetHeight(24) -- no anchors, and no template ⇒ no rect, no draw
assert(t:GetLeft() == nil, "a rect-less region reads nil edges")
"#,
)
@@ -178,10 +178,10 @@ fn region_set_all_points_fills_owner() {
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0) -- owner [0, 0, 50, 100]
f:SetSize(100, 50)
f:SetWidth(100); f:SetHeight(50)
local t = f:CreateTexture("Tex", "ARTWORK")
t:SetTexture("Interface\\Fill")
t:SetSize(24, 24) -- size present, but setAllPoints wins
t:SetWidth(24); t:SetHeight(24) -- size present, but setAllPoints wins
t:SetAllPoints()
"#,
)
@@ -205,14 +205,14 @@ fn region_anchors_to_sibling_region_by_name() {
s.run(
r#"
local row = CreateFrame("Button", "Row")
row:SetPoint("BOTTOMLEFT", 100, 100); row:SetSize(153, 44)
row:SetPoint("BOTTOMLEFT", 100, 100); row:SetWidth(153); row:SetHeight(44)
-- Plate first: its target doesn't exist yet at SetPoint time in XML order terms; the
-- name lookup happens at SetPoint (both exist by then in real loads), the RECT ordering
-- is the fixpoint's job.
local plate = row:CreateTexture("RowPlate", "BACKGROUND")
plate:SetSize(128, 78)
plate:SetWidth(128); plate:SetHeight(78)
local slot = row:CreateTexture("RowSlot", "BACKGROUND")
slot:SetSize(64, 64)
slot:SetWidth(64); slot:SetHeight(64)
slot:SetPoint("TOPLEFT", "Row", "TOPLEFT", -13, 13)
plate:SetPoint("LEFT", "RowSlot", "RIGHT", -9, -18)
"#,
@@ -266,9 +266,9 @@ fn set_portrait_texture_binds_unit_token_then_settexture_clears_it() {
r#"
local f = CreateFrame("Frame", "PFrame")
f:SetPoint("TOPLEFT", 0, 0)
f:SetSize(100, 100)
f:SetWidth(100); f:SetHeight(100)
local p = f:CreateTexture("PFramePortrait", "BACKGROUND")
p:SetSize(64, 64)
p:SetWidth(64); p:SetHeight(64)
p:SetPoint("TOPLEFT", 0, 0)
SetPortraitTexture(p, "player")
"#,
@@ -319,7 +319,7 @@ fn benilla_set_booth_texture_binds_square() {
r#"
local f = CreateFrame("Frame", "DollFrame")
f:SetPoint("TOPLEFT", 0, 0)
f:SetSize(200, 300)
f:SetWidth(200); f:SetHeight(300)
local m = f:CreateTexture("DollFrameModel", "ARTWORK")
m:SetAllPoints()
BenillaSetBoothTexture(m, "paperdoll")
@@ -359,11 +359,11 @@ fn long_region_chain_resolves_and_a_frame_binds_to_its_tail() {
r#"
local f = CreateFrame("Frame", "Book")
f:SetPoint("TOPLEFT", 0, 0)
f:SetSize(384, 512)
f:SetWidth(384); f:SetHeight(512)
for i = 1, 12 do
local t = f:CreateTexture("Line" .. i, "ARTWORK")
t:SetTexture("Interface\\Line" .. i)
t:SetSize(300, 10)
t:SetWidth(300); t:SetHeight(10)
if i == 1 then
t:SetPoint("TOPLEFT", "Book", "TOPLEFT", 5, -5)
else
@@ -371,7 +371,7 @@ fn long_region_chain_resolves_and_a_frame_binds_to_its_tail() {
end
end
local b = CreateFrame("Button", "TailButton", f)
b:SetSize(147, 41)
b:SetWidth(147); b:SetHeight(41)
b:SetPoint("TOPLEFT", "Line12", "BOTTOMLEFT", 0, -6)
"#,
)
@@ -414,13 +414,13 @@ fn child_frame_layers_regions_render_after_the_fixpoint() {
r#"
local parent = CreateFrame("Frame", "Win")
parent:SetPoint("TOPLEFT", 10, -10)
parent:SetSize(400, 300)
parent:SetWidth(400); parent:SetHeight(300)
local child = CreateFrame("Frame", "SubPanel", parent)
child:SetPoint("TOPLEFT", "Win", "TOPLEFT", 20, -20)
child:SetSize(200, 100)
child:SetWidth(200); child:SetHeight(100)
local fs = child:CreateFontString("SubText", "ARTWORK")
fs:SetPoint("TOPLEFT", "SubPanel", "TOPLEFT", 5, -5)
fs:SetSize(150, 12)
fs:SetWidth(150); fs:SetHeight(12)
fs:SetText("hello from the child")
local tex = child:CreateTexture("SubTex", "BACKGROUND")
tex:SetTexture("Interface\\SubFill")
@@ -462,7 +462,7 @@ fn region_alpha_is_its_own_and_multiplies_the_owner_frames() {
r#"
f = CreateFrame("Frame", "AlphaOwner")
f:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
f:SetSize(10, 10)
f:SetWidth(10); f:SetHeight(10)
tex = f:CreateTexture("AlphaTex", "ARTWORK")
tex:SetTexture("Interface\\Foo")
assert(tex:GetAlpha() == 1, "an untouched region is opaque")
@@ -895,11 +895,22 @@ fn the_two_region_leaves_answer_their_own_maps() {
"GetTexture",
"SetTexCoord",
"SetBlendMode",
"GetBlendMode",
"SetTexCoordModifiesRect",
"GetTexCoordModifiesRect",
"GetVertexColor",
] {
assert!(has(&s, "Tex", m), "a Texture answers {m}");
assert!(!has(&s, "Str", m), "a FontString must NOT answer {m}");
}
// `SetRotation` is 1.12's PLAYERMODEL verb (`0x84f1fc`/`0x505f00`), not a region one — it is in
// neither leaf map, and ours used to put it on Texture.
for leaf in ["Tex", "Str"] {
assert!(
!has(&s, leaf, "SetRotation"),
"SetRotation is not a region method"
);
}
// FontString-only.
for m in [
"SetText",
@@ -1012,7 +1023,7 @@ fn the_size_getters_take_the_author_first_then_the_natural_width_and_wrapped_hei
s.run(
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetSize(100, 50)
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
Sized = f:CreateFontString("Sized", "ARTWORK")
Sized:SetPoint("TOPLEFT"); Sized:SetWidth(300); Sized:SetText("Name")
Auto = f:CreateFontString("Auto", "ARTWORK")
@@ -1054,7 +1065,7 @@ fn an_empty_string_reads_back_one_unit_and_a_pending_measure_reads_back_zero() {
s.run(
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetSize(100, 50)
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
Pending = f:CreateFontString("Pending", "ARTWORK")
Pending:SetPoint("TOPLEFT"); Pending:SetText("Name")
Empty = f:CreateFontString("Empty", "ARTWORK")
@@ -1080,7 +1091,7 @@ fn a_textures_getters_report_its_texel_span_on_an_unsized_axis() {
s.run(
r#"
local f = CreateFrame("Frame", "Owner")
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetSize(100, 50)
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
Art = f:CreateTexture("Art", "ARTWORK")
Art:SetTexture("Interface\\Crest"); Art:SetPoint("TOPLEFT")
Half = f:CreateTexture("Half", "ARTWORK")
@@ -1214,9 +1225,9 @@ fn set_portrait_texture_folds_the_token_to_lowercase() {
r#"
local f = CreateFrame("Frame", "NFrame")
f:SetPoint("TOPLEFT", 0, 0)
f:SetSize(100, 100)
f:SetWidth(100); f:SetHeight(100)
local p = f:CreateTexture("NFramePortrait", "BACKGROUND")
p:SetSize(64, 64)
p:SetWidth(64); p:SetHeight(64)
p:SetPoint("TOPLEFT", 0, 0)
SetPortraitTexture(p, "NPC")
"#,
@@ -1278,11 +1289,7 @@ fn an_empty_fontstring_reads_back_nil_and_an_edit_box_does_not() {
// Never written, and every shape of an empty write, all nil.
assert_eq!(text_of(&s, "fresh"), None);
for write in [
r#"fresh:SetText("")"#,
r#"fresh:SetText(nil)"#,
r#"fresh:SetFormattedText("%s", "")"#,
] {
for write in [r#"fresh:SetText("")"#, r#"fresh:SetText(nil)"#] {
s.run(write).unwrap();
assert_eq!(text_of(&s, "fresh"), None, "after `{write}`");
}
@@ -1386,3 +1393,144 @@ fn an_unresolvable_set_texture_keeps_the_art_the_region_had() {
s.run("t:SetTexture(nil)").unwrap();
assert!(s.eval::<bool>("return t:GetTexture() == nil").unwrap());
}
/// `Texture:GetBlendMode()` — ONE string, the reference's own spelling, round-tripping whatever
/// `SetBlendMode` was given (`0x79a890`/`0x79a950`, table `0x87c128`, argc 1, arity 1, kinds
/// `(string?)`).
///
/// The untouched default is asserted alongside the round trip because it is the load-bearing half:
/// `ShaguTweaks/mods/dark-ui-elements.lua:169` guards a recolour with
/// `region.GetBlendMode and region:GetBlendMode() == "ADD"`, so the answer an ordinary texture gives
/// decides whether the reference's additive art is left alone. `"BLEND"` is the CSimpleTexture
/// ctor's `[+0xd0] = 2` (`0x76fc64`).
#[test]
fn get_blend_mode_answers_one_string_and_defaults_to_the_ctors_blend() {
let s = crate::script::UiScript::new().unwrap();
s.run(
r#"
BlendOwner = CreateFrame("Frame", "BlendOwner")
BTex = BlendOwner:CreateTexture("BTex", "ARTWORK")
"#,
)
.unwrap();
// Arity 1 and kind string, measured the way the shape gate measures it.
assert_eq!(
s.eval::<i64>("return select('#', BTex:GetBlendMode())")
.unwrap(),
1,
"arity 1"
);
assert_eq!(
s.eval::<String>("return type(BTex:GetBlendMode())")
.unwrap(),
"string",
"kind string"
);
assert_eq!(
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
"BLEND",
"an untouched texture is the ctor's mode 2"
);
// Every one of the enum's five round-trips, including the three the renderer flattens.
for mode in ["DISABLE", "ALPHAKEY", "BLEND", "ADD", "MOD"] {
s.run(&format!(r#"BTex:SetBlendMode("{mode}")"#)).unwrap();
assert_eq!(
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
mode,
"round trip through the setter"
);
}
// Case-insensitive in, canonical out — the setter coerces, the getter answers the table's name.
s.run(r#"BTex:SetBlendMode("add")"#).unwrap();
assert_eq!(
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
"ADD"
);
// A name outside the enum leaves the mode alone rather than inventing one.
s.run(r#"BTex:SetBlendMode("NOT_A_MODE")"#).unwrap();
assert_eq!(
s.eval::<String>("return BTex:GetBlendMode()").unwrap(),
"ADD",
"an unknown name changes nothing"
);
// A FontString is not a Texture: neither half of the pair is on its map.
assert!(s
.eval::<bool>(
r#"local fs = BlendOwner:CreateFontString("BStr", "ARTWORK")
return fs.GetBlendMode == nil and fs.SetBlendMode == nil"#
)
.unwrap());
}
/// `Texture:GetTexCoordModifiesRect()` — the 1.12 predicate return, `1`/`nil` and never a Lua
/// boolean (`0x79c120`, table `0x87c128`, argc 1, arity 1, kinds `(nil) | (number)`; decision 2118).
/// `pfUI/modules/thirdparty-tbc.lua:319` calls it bare on a Texture.
///
/// The flag's effect on the region's rect is deliberately NOT wired (see
/// `RegionData::tex_coord_modifies_rect`), so this asserts the state and the shape — and asserts
/// the un-wiring too, by checking that setting the flag moves no rect: a future change that wires
/// the geometry will fail here and have to say so.
#[test]
fn tex_coord_modifies_rect_is_one_slash_nil_and_moves_no_rect_yet() {
let mut s = crate::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run(
r#"
TCMOwner = CreateFrame("Frame", "TCMOwner")
TCMOwner:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
TCMOwner:SetWidth(200); TCMOwner:SetHeight(200)
TCMTex = TCMOwner:CreateTexture("TCMTex", "ARTWORK")
TCMTex:SetTexture("Interface\\TcmArt")
TCMTex:SetPoint("BOTTOMLEFT", TCMOwner, "BOTTOMLEFT", 0, 0)
TCMTex:SetWidth(64); TCMTex:SetHeight(32)
"#,
)
.unwrap();
s.resolve();
let before = region_tex_rect(&s, "TcmArt");
assert_eq!(
s.eval::<i64>("return select('#', TCMTex:GetTexCoordModifiesRect())")
.unwrap(),
1,
"arity 1"
);
assert!(
s.eval::<bool>("return TCMTex:GetTexCoordModifiesRect() == nil")
.unwrap(),
"unset is nil, not false"
);
s.run("TCMTex:SetTexCoordModifiesRect(1)").unwrap();
assert!(
s.eval::<bool>("return TCMTex:GetTexCoordModifiesRect() == 1")
.unwrap(),
"set is the NUMBER 1, not true"
);
assert_eq!(
s.eval::<String>("return type(TCMTex:GetTexCoordModifiesRect())")
.unwrap(),
"number",
"kind number, never boolean"
);
// The flag is state only: a SetTexCoord under it still leaves the rect where the anchors and
// the size put it. When the geometry leg is built, this assertion is the one that must change.
s.run("TCMTex:SetTexCoord(0, 0.25, 0, 0.5)").unwrap();
s.resolve();
assert_eq!(
region_tex_rect(&s, "TcmArt"),
before,
"the flag does not (yet) let SetTexCoord re-derive the region's rect"
);
s.run("TCMTex:SetTexCoordModifiesRect(nil)").unwrap();
assert!(
s.eval::<bool>("return TCMTex:GetTexCoordModifiesRect() == nil")
.unwrap(),
"cleared back to nil"
);
}
+201 -27
View File
@@ -36,6 +36,11 @@ fn scrollframe_methods_exist_only_on_scrollframes() {
return (type(sf.SetScrollChild) == "function") and (plain.SetScrollChild == nil)
and (type(sf.SetVerticalScroll) == "function") and (plain.SetVerticalScroll == nil)
and (type(sf.GetVerticalScrollRange) == "function")
-- the horizontal trio lives in the same class table, not on every Frame
and (type(sf.SetHorizontalScroll) == "function") and (plain.SetHorizontalScroll == nil)
and (type(sf.GetHorizontalScroll) == "function") and (plain.GetHorizontalScroll == nil)
and (type(sf.GetHorizontalScrollRange) == "function")
and (plain.GetHorizontalScrollRange == nil)
and (type(sf.Show) == "function") -- base methods still reachable through the fallback
"#,
)
@@ -81,13 +86,13 @@ fn scroll_child_top_tracks_vertical_scroll_unclamped_then_restores_on_clear() {
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100) -- screen top 600 -> frame top 500
frame:SetSize(300, 200) -- frame bottom 300
frame:SetWidth(300); frame:SetHeight(200) -- frame bottom 300
local child = CreateFrame("Frame", "Child")
-- Authored anchor: relative to the screen (child has no parent) — overridden while scrolled,
-- and must be exactly what re-applies once the scroll child is cleared.
child:SetPoint("TOPLEFT", 20, -20)
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
local marker = child:CreateTexture(nil, "ARTWORK")
marker:SetTexture("marker:child")
marker:SetAllPoints() -- templateless Lua regions carry no implicit anchor (decision 1310)
@@ -170,10 +175,10 @@ fn set_vertical_scroll_fires_on_vertical_scroll_only_on_a_change() {
Fires = {}
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100)
frame:SetSize(300, 200)
frame:SetWidth(300); frame:SetHeight(200)
frame:SetScript("OnVerticalScroll", function() table.insert(Fires, arg1) end)
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
frame:SetScrollChild(child)
frame:SetVerticalScroll(0) -- already 0: nothing
frame:SetVerticalScroll(40) -- a change
@@ -199,9 +204,9 @@ fn vertical_scroll_range_is_live_and_zero_when_unresolved_or_childless_or_shorte
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100)
frame:SetSize(300, 200)
frame:SetWidth(300); frame:SetHeight(200)
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
frame:SetScrollChild(child)
"#,
)
@@ -221,7 +226,7 @@ fn vertical_scroll_range_is_live_and_zero_when_unresolved_or_childless_or_shorte
);
// A child shorter than the frame: 0, not negative.
s.run("Child:SetSize(300, 150)").unwrap();
s.run("Child:SetWidth(300); Child:SetHeight(150)").unwrap();
s.resolve();
assert_eq!(
s.eval::<f32>("return SF:GetVerticalScrollRange()").unwrap(),
@@ -251,10 +256,10 @@ fn vertical_scroll_range_is_local_units_on_a_scaled_frame() {
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100)
frame:SetSize(300, 200)
frame:SetWidth(300); frame:SetHeight(200)
frame:SetScale(0.5)
local child = CreateFrame("Frame", "Child", frame)
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
frame:SetScrollChild(child)
"#,
)
@@ -299,9 +304,9 @@ fn vertical_scroll_fires_as_given_and_update_rect_fires_range_changed() {
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100)
frame:SetSize(300, 200)
frame:SetWidth(300); frame:SetHeight(200)
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
frame:SetScrollChild(child)
seen_v = nil
@@ -345,9 +350,9 @@ fn update_scroll_child_rect_sees_a_same_tick_resize_with_no_intervening_resolve(
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100)
frame:SetSize(300, 200)
frame:SetWidth(300); frame:SetHeight(200)
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 200) -- starts exactly frame-height: range 0
child:SetWidth(300); child:SetHeight(200) -- starts exactly frame-height: range 0
frame:SetScrollChild(child)
"#,
)
@@ -358,7 +363,7 @@ fn update_scroll_child_rect_sees_a_same_tick_resize_with_no_intervening_resolve(
r#"
seen_hi = nil
SF:SetScript("OnScrollRangeChanged", function(self, lo, hi) seen_hi = hi end)
Child:SetSize(300, 600) -- grows well past the frame — all in this ONE tick
Child:SetWidth(300); Child:SetHeight(600) -- grows well past the frame — all in this ONE tick
SF:UpdateScrollChildRect()
"#,
)
@@ -389,10 +394,10 @@ fn extract_clips_the_scroll_childs_whole_subtree_and_leaves_a_sibling_unclipped(
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100) -- top 500
frame:SetSize(300, 200) -- bottom 300, right 300
frame:SetWidth(300); frame:SetHeight(200) -- bottom 300, right 300
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
frame:SetScrollChild(child)
local cm = child:CreateTexture(nil, "ARTWORK")
cm:SetTexture("marker:child")
@@ -400,14 +405,14 @@ fn extract_clips_the_scroll_childs_whole_subtree_and_leaves_a_sibling_unclipped(
-- a grandchild FRAME (a real arena descendant of the scroll child) with its own region
local grand = CreateFrame("Frame", "Grand", child)
grand:SetPoint("TOPLEFT", child, "TOPLEFT", 0, 0)
grand:SetSize(50, 50)
grand:SetWidth(50); grand:SetHeight(50)
local gm = grand:CreateTexture(nil, "ARTWORK")
gm:SetTexture("marker:grand")
-- a sibling entirely outside the ScrollFrame
local sib = CreateFrame("Frame", "Sib")
sib:SetPoint("TOPLEFT", 0, 0)
sib:SetSize(50, 50)
sib:SetWidth(50); sib:SetHeight(50)
local sm = sib:CreateTexture(nil, "ARTWORK")
sm:SetTexture("marker:sib")
"#,
@@ -441,11 +446,11 @@ fn nested_scrollframes_intersect_their_clip_rects() {
r#"
local outer = CreateFrame("ScrollFrame", "Outer")
outer:SetPoint("TOPLEFT", 0, -50) -- top 550
outer:SetSize(400, 300) -- bottom 250, right 400
outer:SetWidth(400); outer:SetHeight(300) -- bottom 250, right 400
local om = outer:CreateTexture(nil, "ARTWORK"); om:SetTexture("marker:outer")
local c1 = CreateFrame("Frame", "C1")
c1:SetSize(400, 1000)
c1:SetWidth(400); c1:SetHeight(1000)
outer:SetScrollChild(c1)
local c1m = c1:CreateTexture(nil, "ARTWORK"); c1m:SetTexture("marker:c1")
@@ -454,11 +459,11 @@ fn nested_scrollframes_intersect_their_clip_rects() {
-- Outer's right edge — an intersection distinct from either rect alone.
local inner = CreateFrame("ScrollFrame", "Inner", c1)
inner:SetPoint("TOPLEFT", "C1", "TOPLEFT", 350, -30)
inner:SetSize(200, 150)
inner:SetWidth(200); inner:SetHeight(150)
local im = inner:CreateTexture(nil, "ARTWORK"); im:SetTexture("marker:inner")
local c2 = CreateFrame("Frame", "C2", inner)
c2:SetSize(200, 500)
c2:SetWidth(200); c2:SetHeight(500)
inner:SetScrollChild(c2)
local leaf = c2:CreateTexture(nil, "ARTWORK"); leaf:SetTexture("marker:leaf")
"#,
@@ -514,16 +519,16 @@ fn hit_test_denies_a_button_clipped_out_and_admits_it_once_scrolled_into_view()
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100) -- top 500
frame:SetSize(300, 200) -- bottom 300
frame:SetWidth(300); frame:SetHeight(200) -- bottom 300
frame:EnableMouse(false) -- isolate the button's own clip-gated hit (§5's subject)
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
frame:SetScrollChild(child)
local btn = CreateFrame("Button", "Btn", child)
btn:SetPoint("TOPLEFT", "Child", "TOPLEFT", 0, -400)
btn:SetSize(50, 20)
btn:SetWidth(50); btn:SetHeight(20)
"#,
)
.unwrap();
@@ -568,9 +573,9 @@ fn update_scroll_child_rect_measures_the_childs_text_before_it_reads_the_range()
s.run(
r#"
local sf = CreateFrame("ScrollFrame", "SF", UIParent)
sf:SetPoint("TOPLEFT", 0, 0); sf:SetSize(100, 50)
sf:SetPoint("TOPLEFT", 0, 0); sf:SetWidth(100); sf:SetHeight(50)
local child = CreateFrame("Frame", "SFChild", sf)
child:SetSize(100, 50); sf:SetScrollChild(child)
child:SetWidth(100); child:SetHeight(50); sf:SetScrollChild(child)
local fs = child:CreateFontString("SFText", "ARTWORK")
fs:SetPoint("TOPLEFT", 0, 0); fs:SetWidth(70)
sf:SetScript("OnScrollRangeChanged", function() RANGE = arg2 end)
@@ -586,3 +591,172 @@ fn update_scroll_child_rect_measures_the_childs_text_before_it_reads_the_range()
"56px of measured text under a 50px frame: the range is known at the call, not a frame later"
);
}
/// The horizontal axis is the vertical one's byte-clone (`0x786d30` against `0x786db0`), so this is
/// the vertical trio's test on the other axis: the offset is stored VERBATIM and re-anchors the
/// child, `OnHorizontalScroll` fires only on a real change, and the range is
/// `max(0, subtreeWidth - frameWidth)`.
///
/// **The sign is the point.** `0x787100` hands both offsets to `SetPoint(TOPLEFT, self, TOPLEFT,
/// +hScroll, +vScroll)` unnegated, and x grows right — so a positive horizontal pushes the child
/// RIGHT where a positive vertical lifts it. `aux-addon/tabs/search/filter.lua:315-322` corroborates
/// it from the caller's side (it bounds x into `[frameWidth - contentWidth - 10, 0]` and y into
/// `[0, contentHeight - frameHeight]`), which is why it is asserted here rather than assumed.
#[test]
fn scroll_child_left_tracks_horizontal_scroll_unclamped_and_fires_on_change() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local frame = CreateFrame("ScrollFrame", "HSF")
frame:SetPoint("TOPLEFT", 0, -100) -- frame left 0, top 500
frame:SetWidth(300); frame:SetHeight(200) -- frame right 300
local child = CreateFrame("Frame", "HChild")
child:SetPoint("TOPLEFT", 20, -20)
child:SetWidth(500); child:SetHeight(200)
local marker = child:CreateTexture(nil, "ARTWORK")
marker:SetTexture("marker:hchild")
marker:SetAllPoints()
frame:SetScrollChild(child)
hfired = {}
frame:SetScript("OnHorizontalScroll", function() table.insert(hfired, arg1) end)
"#,
)
.unwrap();
s.resolve();
assert_eq!(
s.eval::<f32>("return HSF:GetHorizontalScroll()").unwrap(),
0.0,
"a fresh ScrollFrame is at 0 on both axes"
);
assert_eq!(
marker_rect(&s.extract(), "marker:hchild").map(|r| r.left),
Some(0.0),
"horizontal=0 -> child left = frame left"
);
// Positive pushes the child RIGHT (the reference's unnegated xOff) …
s.run("HSF:SetHorizontalScroll(40)").unwrap();
s.resolve();
assert_eq!(
marker_rect(&s.extract(), "marker:hchild").map(|r| r.left),
Some(40.0),
"a positive horizontal offset moves the child right"
);
// … and it is the NEGATIVE direction that scrolls right, which is the half aux-addon relies on.
s.run("HSF:SetHorizontalScroll(-60)").unwrap();
s.resolve();
assert_eq!(
marker_rect(&s.extract(), "marker:hchild").map(|r| r.left),
Some(-60.0),
"a negative horizontal offset scrolls right"
);
// No clamp — the range is 500-300 = 200 and 450 is stored and applied whole (decision 2017's
// law, which is `0x786d30`'s as much as `0x786db0`'s: neither reads the range).
assert_eq!(
s.eval::<f32>("return HSF:GetHorizontalScrollRange()")
.unwrap(),
200.0,
"range = subtree width - frame width"
);
s.run("HSF:SetHorizontalScroll(450)").unwrap();
s.resolve();
assert_eq!(
s.eval::<f32>("return HSF:GetHorizontalScroll()").unwrap(),
450.0,
"stored verbatim, past the range"
);
// Arity/kind, the way the shape gate measures them.
assert_eq!(
s.eval::<i64>("return select('#', HSF:GetHorizontalScroll())")
.unwrap(),
1
);
assert_eq!(
s.eval::<i64>("return select('#', HSF:GetHorizontalScrollRange())")
.unwrap(),
1
);
assert_eq!(
s.eval::<String>("return type(HSF:GetHorizontalScroll())")
.unwrap(),
"number"
);
assert_eq!(
s.eval::<String>("return type(HSF:GetHorizontalScrollRange())")
.unwrap(),
"number"
);
assert_eq!(
s.eval::<i64>("return select('#', HSF:SetHorizontalScroll(450))")
.unwrap(),
0,
"the setter answers nothing"
);
// The change-gate: four setter calls moved the value (40, -60, 450, and 450 again is not one).
let fired: Vec<f32> = (1..=s.eval::<i64>("return table.getn(hfired)").unwrap())
.map(|i| s.eval::<f32>(&format!("return hfired[{i}]")).unwrap())
.collect();
assert_eq!(
fired,
vec![40.0, -60.0, 450.0],
"OnHorizontalScroll fires once per ACTUAL change, with the new offset"
);
// The two axes are independent state, not one offset behind two names.
s.run("HSF:SetVerticalScroll(25)").unwrap();
assert_eq!(
s.eval::<f32>("return HSF:GetHorizontalScroll()").unwrap(),
450.0
);
assert_eq!(
s.eval::<f32>("return HSF:GetVerticalScroll()").unwrap(),
25.0
);
}
/// `UpdateScrollChildRect` notifies `OnScrollRangeChanged(self, xRange, yRange)` — **horizontal
/// first**. Byte-derived, not guessed: the vertical result leaves `0x786e30` on top of the x87
/// stack, is `fstp`'d first and so pushed deepest, landing as `arg2` (wow-re
/// `scrollframe-offset-and-range-law.md` §3.6 — two of that round's own workers published it the
/// other way round, which is why it is asserted here). `arg1` was a hardcoded `0.0` for as long as
/// there was no horizontal range to put in it.
#[test]
fn update_scroll_child_rect_reports_both_ranges_horizontal_first() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local frame = CreateFrame("ScrollFrame", "BSF")
frame:SetPoint("TOPLEFT", 0, -100)
frame:SetWidth(300); frame:SetHeight(200)
local child = CreateFrame("Frame", "BChild")
child:SetPoint("TOPLEFT", 0, 0)
child:SetWidth(500); child:SetHeight(600)
frame:SetScrollChild(child)
seen = nil
frame:SetScript("OnScrollRangeChanged", function() seen = { arg1, arg2 } end)
"#,
)
.unwrap();
s.resolve();
s.run("BSF:UpdateScrollChildRect()").unwrap();
assert_eq!(
s.eval::<f32>("return seen[1]").unwrap(),
200.0,
"arg1 is the HORIZONTAL range (500 - 300)"
);
assert_eq!(
s.eval::<f32>("return seen[2]").unwrap(),
400.0,
"arg2 is the VERTICAL range (600 - 200)"
);
assert!(s.take_errors().is_empty());
}
@@ -122,7 +122,7 @@ fn page(s: &UiScript) {
f:SetTextColor(0.18, 0.12, 0.06)
Page = CreateFrame("SimpleHTML", "Page")
Page:SetPoint("TOPLEFT", 0, 0)
Page:SetSize(270, 304)
Page:SetWidth(270); Page:SetHeight(304)
Page:SetFontObject("P", ItemTextFontNormal)
"#,
)
@@ -20,7 +20,7 @@ fn a_resize_fires_on_size_changed_and_a_move_does_not() {
fires, w, h = 0, nil, nil
Panel = CreateFrame("Frame", "SizedPanel")
Panel:SetPoint("BOTTOMLEFT", 100, 100)
Panel:SetSize(200, 50)
Panel:SetWidth(200); Panel:SetHeight(50)
Panel:SetScript("OnSizeChanged", function(self, aw, ah)
fires = fires + 1 w = aw h = ah
end)
@@ -70,7 +70,7 @@ fn an_anchor_driven_resize_fires_it_too() {
fires, w = 0, nil
Parent = CreateFrame("Frame", "SizeParent")
Parent:SetPoint("BOTTOMLEFT", 0, 0)
Parent:SetSize(300, 200)
Parent:SetWidth(300); Parent:SetHeight(200)
Child = CreateFrame("Frame", "SizeChild", Parent)
Child:SetPoint("BOTTOMLEFT", Parent, "BOTTOMLEFT", 0, 0)
Child:SetPoint("TOPRIGHT", Parent, "TOPRIGHT", 0, 0)
@@ -107,7 +107,7 @@ fn a_handler_that_resizes_its_own_frame_settles_instead_of_spinning() {
fires = 0
Sq = CreateFrame("Frame", "SquarePanel")
Sq:SetPoint("BOTTOMLEFT", 0, 0)
Sq:SetSize(200, 50)
Sq:SetWidth(200); Sq:SetHeight(50)
-- The idiom: "keep me square". It writes back into the very input that fired it.
Sq:SetScript("OnSizeChanged", function(self, aw, ah)
fires = fires + 1
@@ -148,7 +148,7 @@ fn set_script_on_size_changed_is_accepted_because_the_resolve_pass_fires_it() {
ran = false
Accepted = CreateFrame("Frame", "SizeAccepted")
Accepted:SetPoint("BOTTOMLEFT", 0, 0)
Accepted:SetSize(10, 10)
Accepted:SetWidth(10); Accepted:SetHeight(10)
Accepted:SetScript("OnSizeChanged", function() ran = true end)
"#,
)
@@ -180,7 +180,7 @@ fn the_on_size_changed_watch_list_is_exactly_the_frames_carrying_the_script() {
fires = 0
Watched = CreateFrame("Frame", "WatchedPanel")
Watched:SetPoint("BOTTOMLEFT", 0, 0)
Watched:SetSize(10, 10)
Watched:SetWidth(10); Watched:SetHeight(10)
Other = CreateFrame("Frame", "UnwatchedPanel")
Other:SetScript("OnShow", function() end) -- another kind never enrols
local bump = function() fires = fires + 1 end
@@ -199,7 +199,8 @@ fn the_on_size_changed_watch_list_is_exactly_the_frames_carrying_the_script() {
assert_eq!(watched(&s), 1, "one frame carries the script");
s.resolve();
s.run("Watched:SetSize(40, 40)").unwrap();
s.run("Watched:SetWidth(40); Watched:SetHeight(40)")
.unwrap();
s.resolve();
assert_eq!(
s.eval::<i64>("return fires").unwrap(),
+40 -20
View File
@@ -27,10 +27,10 @@ fn slider_thumb_draws_at_value_fraction_along_the_track() {
local sl = CreateFrame("Slider", "SlRender")
-- A vertical scrollbar: 16 wide, 100 tall, bottom-left at (100, 100) -> track y in [100, 200].
sl:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
sl:SetSize(16, 100)
sl:SetWidth(16); sl:SetHeight(100)
sl:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
local t = sl:GetThumbTexture()
t:SetSize(16, 16)
t:SetWidth(16); t:SetHeight(16)
sl:SetMinMaxValues(0, 100)
sl:SetValue(0)
"#,
@@ -73,9 +73,9 @@ fn slider_thumb_drag_maps_cursor_to_value() {
r#"
bar = CreateFrame("Slider", "SlDrag")
bar:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
bar:SetSize(16, 100)
bar:SetWidth(16); bar:SetHeight(100)
bar:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
bar:GetThumbTexture():SetSize(16, 16)
local thumb = bar:GetThumbTexture(); thumb:SetWidth(16); thumb:SetHeight(16)
bar:SetMinMaxValues(0, 100)
bar:SetValue(0)
"#,
@@ -104,16 +104,16 @@ fn slider_thumb_drag_maps_cursor_to_value() {
}
#[test]
fn slider_track_press_seats_the_thumb_and_a_disabled_slider_ignores_it() {
fn slider_track_press_seats_the_thumb_and_a_mouseless_slider_ignores_it() {
let mut s = script();
s.set_screen_size(1024.0, 768.0);
s.run(
r#"
bar = CreateFrame("Slider", "SlTrack")
bar:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
bar:SetSize(16, 100)
bar:SetWidth(16); bar:SetHeight(100)
bar:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
bar:GetThumbTexture():SetSize(16, 16)
local thumb = bar:GetThumbTexture(); thumb:SetWidth(16); thumb:SetHeight(16)
bar:SetMinMaxValues(0, 100)
bar:SetValue(0)
fired = {}
@@ -142,18 +142,25 @@ fn slider_track_press_seats_the_thumb_and_a_disabled_slider_ignores_it() {
assert_eq!(v, 100.0, "the gesture drags on without re-grabbing");
s.mouse_button(108.0, 108.0, "LeftButton", false);
// A disabled slider ignores a press anywhere — thumb and track alike.
s.run(r#"SlTrack:SetValue(0); SlTrack:Disable(); fired = {}"#)
// A slider the mouse cannot reach ignores a press anywhere — thumb and track alike. This was
// `SlTrack:Disable()` until the Button-only `Enable`/`Disable`/`IsEnabled` trio came off the
// Slider table (1.12 registers them on `0x879d00` alone, and the Slider's LoadXML has no
// `enabled` attribute either). `EnableMouse(false)` is how the reference keeps a slider off a
// press, and it gates upstream in the hit test rather than inside `begin_drag`.
s.run(r#"SlTrack:SetValue(0); SlTrack:EnableMouse(false); fired = {}"#)
.unwrap();
for y in [192.0, 150.0] {
s.mouse_button(108.0, y, "LeftButton", true);
s.mouse_move(108.0, 130.0);
let v: f32 = s.eval("return SlTrack:GetValue()").unwrap();
assert_eq!(v, 0.0, "disabled slider does not move (press at y={y})");
assert_eq!(
v, 0.0,
"mouse-disabled slider does not move (press at y={y})"
);
s.mouse_button(108.0, 130.0, "LeftButton", false);
}
let n: usize = s.eval("return table.getn(fired)").unwrap();
assert_eq!(n, 0, "disabled: no OnValueChanged at all");
assert_eq!(n, 0, "mouse disabled: no OnValueChanged at all");
}
#[test]
@@ -235,17 +242,30 @@ fn slider_setvalue_fires_onvaluechanged_only_on_change() {
.unwrap();
}
/// `Enable`/`Disable`/`IsEnabled` are a **Button** trio in 1.12 (table `0x879d00`), and a Slider
/// must not answer them: a duck-typing addon that branches on `if widget.IsEnabled then` reads a
/// superset as the wrong class (1189/1250 §5). This asserts the removal *and* its control — the
/// same three names still present on a Button, so a regression that emptied the Button table would
/// fail here rather than pass by accident.
#[test]
fn slider_enable_disable_roundtrips() {
fn a_slider_does_not_answer_the_buttons_enable_trio() {
let s = script();
s.run(
r#"
local sl = CreateFrame("Slider", "SlEnable")
assert(sl:IsEnabled(), "enabled by ctor")
sl:Disable()
assert(not sl:IsEnabled(), "disabled")
sl:Enable()
assert(sl:IsEnabled(), "re-enabled")
for _, name in ipairs({ "Enable", "Disable", "IsEnabled" }) do
assert(sl[name] == nil, "Slider must not answer " .. name)
end
local b = CreateFrame("Button", "SlEnableControl")
for _, name in ipairs({ "Enable", "Disable", "IsEnabled" }) do
assert(type(b[name]) == "function", "Button still answers " .. name)
end
-- The Button's own predicate is the NUMBER 1 / the NUMBER 0, never a Lua boolean — its
-- false leg is 0 rather than nil, settled per body at `0x7800b0`'s `setne`+`fild`
-- (wow-re `button-enabled-state.md`; decision 2118's `binding_abi::flag` doc).
assert(b:IsEnabled() == 1, "1, not true")
b:Disable()
assert(b:IsEnabled() == 0, "0, not false and not nil")
"#,
)
.unwrap();
@@ -279,9 +299,9 @@ fn slider_scrollbar_wiring_does_not_recurse() {
r#"
local sf = CreateFrame("ScrollFrame", "SlSF")
sf:SetPoint("TOPLEFT", nil, "TOPLEFT", 0, 0)
sf:SetSize(100, 100)
sf:SetWidth(100); sf:SetHeight(100)
local child = CreateFrame("Frame", "SlSFChild", sf)
child:SetSize(100, 300) -- 200px taller than the frame -> scroll range 200
child:SetWidth(100); child:SetHeight(300) -- 200px taller than the frame -> scroll range 200
sf:SetScrollChild(child)
bar = CreateFrame("Slider", "SlSFBar")
@@ -399,7 +419,7 @@ fn a_thumb_with_no_authored_size_takes_its_arts_texel_span_and_still_drags() {
-- Slider and not to the mouse-enabled host sitting under it.
host = CreateFrame("Frame", "DdHost", nil)
host:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
host:SetSize(80, 170)
host:SetWidth(80); host:SetHeight(170)
host:SetFrameStrata("FULLSCREEN_DIALOG")
host:EnableMouse(true)
bar = CreateFrame("Slider", "DdSlider", host)
@@ -72,7 +72,7 @@ fn statusbar_bar_region_scales_by_fraction_on_extract() {
r#"
local b = CreateFrame("StatusBar", "SbFill")
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
b:SetSize(200, 20)
b:SetWidth(200); b:SetHeight(20)
b:SetStatusBarTexture(0.2, 0.7, 0.2)
b:SetMinMaxValues(0, 100)
b:SetValue(25)
@@ -137,7 +137,7 @@ fn statusbar_bar_region_crops_its_texture_rather_than_stretching_it() {
r#"
local b = CreateFrame("StatusBar", "SbCrop")
b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
b:SetSize(200, 20)
b:SetWidth(200); b:SetHeight(20)
b:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
b:SetMinMaxValues(0, 100)
b:SetValue(25)
+2 -2
View File
@@ -147,7 +147,7 @@ fn set_talent_renders_the_interleaved_tooltip() {
);
s.run(
r#"
local a = CreateFrame("Button", "TB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "TB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetTalent(1, 1)
@@ -183,7 +183,7 @@ fn set_talent_locked_reqs_and_the_ask_once_miss() {
// No spell view pushed for Ignite (11119): the render falls back, the ask is recorded.
s.run(
r#"
local a = CreateFrame("Button", "TB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "TB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT2")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetTalent(1, 2)
+101 -12
View File
@@ -31,7 +31,7 @@ fn line_stack_autosize_and_right_flush() {
s.run(
r#"
local owner = CreateFrame("Button", "Slot")
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(owner, "ANCHOR_RIGHT")
tt:AddLine("Tough Jerky", 1, 1, 1)
@@ -101,7 +101,7 @@ fn empty_line_is_a_one_unit_row_and_the_chain_stays_inside_the_plate() {
s.run(
r#"
local owner = CreateFrame("Button", "Slot2")
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
local tt = CreateFrame("GameTooltip", "TTE")
tt:SetOwner(owner, "ANCHOR_RIGHT")
tt:AddLine("Marshal McBride", 0.25, 0.75, 0.25)
@@ -154,15 +154,15 @@ fn owner_clear_and_hide_lifecycle() {
s.run(
r#"
cleared = 0
local a = CreateFrame("Button", "A"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local b = CreateFrame("Button", "B"); b:SetPoint("CENTER", 50, 0); b:SetSize(10, 10)
local a = CreateFrame("Button", "A"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local b = CreateFrame("Button", "B"); b:SetPoint("CENTER", 50, 0); b:SetWidth(10); b:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT2")
tt:SetScript("OnTooltipCleared", function() cleared = cleared + 1 end)
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:AddLine("first hover")
tt:Show()
assert(tt:IsOwned(a) and not tt:IsOwned(b), "owned by a")
assert(tt:GetOwner() == a, "GetOwner returns the owner wrapper")
assert(tt:BenillaGetTooltipOwner() == a, "the owner wrapper reads back")
tt:SetOwner(b, "ANCHOR_LEFT")
assert(cleared >= 1, "SetOwner cleared the old content")
assert(tt:NumLines() == 0, "content cleared on re-own")
@@ -190,7 +190,7 @@ fn settext_shows_and_both_addline_shapes() {
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local a = CreateFrame("Button", "A3"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "A3"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT3")
tt:Hide() -- the XML instance ships hidden="true"; CreateFrame defaults shown
tt:SetOwner(a, "ANCHOR_RIGHT")
@@ -248,9 +248,9 @@ fn unmeasured_lines_hold_declared_size() {
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local a = CreateFrame("Button", "A6"); a:SetPoint("TOPLEFT", 100, -100); a:SetSize(10, 10)
local a = CreateFrame("Button", "A6"); a:SetPoint("TOPLEFT", 100, -100); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT6")
tt:SetSize(120, 32)
tt:SetWidth(120); tt:SetHeight(32)
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:AddLine("Tough Jerky")
tt:AddDoubleLine("One-Hand", "Sword")
@@ -291,7 +291,7 @@ fn minimum_width_floors_autosize() {
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local a = CreateFrame("Button", "A4"); a:SetPoint("TOPLEFT", 100, -100); a:SetSize(10, 10)
local a = CreateFrame("Button", "A4"); a:SetPoint("TOPLEFT", 100, -100); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT4")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:AddLine("tiny")
@@ -316,7 +316,7 @@ fn fadeout_ramps_then_hides() {
s.run(
r#"
cleared = 0
local a = CreateFrame("Button", "A5"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "A5"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT5")
tt:SetScript("OnTooltipCleared", function() cleared = cleared + 1 end)
tt:SetOwner(a, "ANCHOR_RIGHT")
@@ -387,7 +387,7 @@ fn owner_anchored_tooltip_clamps_to_screen() {
s.run(
r#"
local owner = CreateFrame("Button", "ZoneTextBtn")
owner:SetPoint("TOPRIGHT", -100, 0); owner:SetSize(90, 14)
owner:SetPoint("TOPRIGHT", -100, 0); owner:SetWidth(90); owner:SetHeight(14)
local tt = CreateFrame("GameTooltip", "TTC")
assert(tt:IsClampedToScreen(), "a GameTooltip clamps by construction")
assert(not owner:IsClampedToScreen(), "a plain frame does not")
@@ -539,7 +539,7 @@ fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_c
s.run(
r#"
local owner = CreateFrame("Button", "SlotR")
owner:SetPoint("TOPLEFT", 100, -100); owner:SetSize(40, 40)
owner:SetPoint("TOPLEFT", 100, -100); owner:SetWidth(40); owner:SetHeight(40)
local tt = CreateFrame("GameTooltip", "TTR")
tt:SetOwner(owner, "ANCHOR_RIGHT")
tt:AddLine("Marshal McBride")
@@ -589,3 +589,92 @@ fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_c
.unwrap();
assert!(s.take_errors().is_empty());
}
/// `GameTooltip:GetAnchorType()` — ONE string, the reference's own spelling, round-tripping
/// whatever `SetOwner` was given (`0x5313e0`, table `0x854198`, argc 1, arity 1, kinds `(string)`,
/// reading `[+0x318]` back through the name table `0x531530`).
///
/// The round trip is the whole point rather than a nicety: `_Nameplates/_Nameplates.lua:479` is
/// `if GameTooltip:GetAnchorType() ~= Anchor then GameTooltip:SetOwner(Column, Anchor) end`, so an
/// answer that never equals what the setter was handed would re-own the plate every OnUpdate; and
/// `pfUI/modules/tooltip.lua:97` compares against the literal `"ANCHOR_NONE"`.
#[test]
fn tooltip_anchor_type_round_trips_every_reachable_mode() {
let s = script();
s.run(
r#"
AnchorTip = CreateFrame("GameTooltip", "AnchorTip")
AnchorOwner = CreateFrame("Frame", "AnchorOwner")
"#,
)
.unwrap();
// Arity 1, kind string, and NO nil leg — the shapes row is `(string)` with no alternative, so
// a plate nobody has owned still answers a string.
assert_eq!(
s.eval::<i64>("return select('#', AnchorTip:GetAnchorType())")
.unwrap(),
1,
"arity 1"
);
assert_eq!(
s.eval::<String>("return type(AnchorTip:GetAnchorType())")
.unwrap(),
"string",
"kind string, never nil"
);
assert_eq!(
s.eval::<String>("return AnchorTip:GetAnchorType()")
.unwrap(),
"ANCHOR_NONE",
"a plate nothing has owned is anchored to nothing"
);
// The eight modes SetOwner accepts, each answered back verbatim — ANCHOR_PRESERVE included,
// which the reference stores like any other rather than resolving back to what it preserved.
for mode in [
"ANCHOR_RIGHT",
"ANCHOR_LEFT",
"ANCHOR_TOPRIGHT",
"ANCHOR_TOPLEFT",
"ANCHOR_BOTTOMRIGHT",
"ANCHOR_BOTTOMLEFT",
"ANCHOR_NONE",
"ANCHOR_PRESERVE",
] {
s.run(&format!(r#"AnchorTip:SetOwner(AnchorOwner, "{mode}")"#))
.unwrap();
assert_eq!(
s.eval::<String>("return AnchorTip:GetAnchorType()")
.unwrap(),
mode,
"round trip through SetOwner"
);
}
// Lower case in, the reference's canonical spelling out — SetOwner already folds the token.
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "anchor_left")"#)
.unwrap();
assert_eq!(
s.eval::<String>("return AnchorTip:GetAnchorType()")
.unwrap(),
"ANCHOR_LEFT"
);
// An anchor SetOwner cannot honour is recorded as the ANCHOR_RIGHT it is actually PLACED by,
// never as the string it was handed — the getter reports the plate, not the request.
// `ANCHOR_CURSOR` is the live case (nine corpus files ask for it; mode 6 follows the cursor per
// frame and this engine has no such driver), and SetOwner warns rather than pretending.
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "ANCHOR_CURSOR")"#)
.unwrap();
assert_eq!(
s.eval::<String>("return AnchorTip:GetAnchorType()")
.unwrap(),
"ANCHOR_RIGHT"
);
assert!(
s.warnings().iter().any(|w| w.contains("ANCHOR_CURSOR")),
"the unhonoured anchor is warned, not silently taken: {:?}",
s.warnings()
);
}
@@ -93,7 +93,7 @@ fn shift_compare_fires_seats_and_renders_the_compare_shape() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
-- CreateFrame'd frames start SHOWN; the shipped XML instances are hidden="true".
CreateFrame("GameTooltip", "GameTooltip"):Hide()
CreateFrame("GameTooltip", "ShoppingTooltip1"):Hide()
@@ -101,7 +101,7 @@ fn shift_compare_fires_seats_and_renders_the_compare_shape() {
compare_calls = {}
for slot = 11, 12 do
local f = CreateFrame("Button", "Doll" .. slot)
f:SetPoint("CENTER", 100, 0); f:SetSize(8, 8)
f:SetPoint("CENTER", 100, 0); f:SetWidth(8); f:SetHeight(8)
f.invSlotId = slot
f:RegisterEvent("SHOW_COMPARE_TOOLTIP")
f:SetScript("OnEvent", function()
@@ -213,7 +213,7 @@ fn set_inventory_item_renders_full_outside_compare() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
assert(tt:SetInventoryItem("player", 16) == 1, "occupied slot answers 1")
@@ -266,7 +266,7 @@ fn set_hyperlink_renders_items_and_ignores_other_links() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetHyperlink("|cff1eff00|Hitem:7002:0:0:0|h[New Loop]|h|r")
@@ -23,11 +23,11 @@ fn item_line_law_and_red_requirements() {
});
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetItemById(871)
assert(tt:IsShown(), "SetItemById shows")
tt:BenillaSetItemById(871)
assert(tt:IsShown(), "BenillaSetItemById shows")
"#,
)
.unwrap();
@@ -95,10 +95,10 @@ fn red_lines_track_player_state() {
});
s.run(
r#"
local a = CreateFrame("Button", "Slot2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetItemById(871)
tt:BenillaSetItemById(871)
"#,
)
.unwrap();
@@ -122,7 +122,7 @@ fn red_lines_track_player_state() {
/// The §5-verified families folded back 2026-07-10 (tooltip-content-law.md): SIGNABLE green,
/// UNIQUE before STARTS_QUEST, LOCKED red, six-equal resistances collapse to the ALL line (and
/// Holy never prints singly), a known taught spell reds "Already known", the description gold —
/// and NO openable line: `SetItemById` is a template source, and the whole openable/readable/
/// and NO openable line: `BenillaSetItemById` is a template source, and the whole openable/readable/
/// creator tail rides the ref's item-OBJECT gate (`0x52e1c7`/`0x52e2e0` — byte-read 2026-07-20;
/// the instance-tail law itself is in [`instance_tail_creator_and_readable`]).
#[test]
@@ -154,10 +154,10 @@ fn verified_families_signable_locked_resists_known() {
});
s.run(
r#"
local a = CreateFrame("Button", "Slot5"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot5"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetItemById(5518)
tt:BenillaSetItemById(5518)
"#,
)
.unwrap();
@@ -208,7 +208,7 @@ fn verified_families_signable_locked_resists_known() {
s.run(
r#"
TT:SetOwner(Slot5, "ANCHOR_RIGHT")
TT:SetItemById(5519)
TT:BenillaSetItemById(5519)
assert(TT:NumLines() == 1, "a lone Holy resist prints no line, got " .. TT:NumLines())
"#,
)
@@ -243,10 +243,10 @@ fn proficiency_and_reputation_reds() {
s.set_player_req_state(req.clone());
s.run(
r#"
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot9"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetItemById(871)
tt:BenillaSetItemById(871)
"#,
)
.unwrap();
@@ -281,7 +281,7 @@ fn proficiency_and_reputation_reds() {
req2.proficiency.insert(2, 1 << 0);
req2.rep_ranks.insert(72, 4);
s.set_player_req_state(req2.clone());
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(871)"#)
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(871)"#)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
@@ -300,7 +300,7 @@ fn proficiency_and_reputation_reds() {
let mut alt = item;
alt.proficiency_alt = Some(0);
s.set_item_template(871, alt);
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(871)"#)
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(871)"#)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
@@ -328,7 +328,7 @@ fn proficiency_and_reputation_reds() {
let mut req3 = req2.clone();
req3.proficiency.insert(2, 1 << 15);
s.set_player_req_state(req3.clone());
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(872)"#)
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(872)"#)
.unwrap();
let lines = lines_of(&mut s);
// `INVTYPE_WEAPONOFFHAND`, NOT `INVTYPE_SHIELD` — one enUS sentence, two keys, and only the
@@ -345,7 +345,7 @@ fn proficiency_and_reputation_reds() {
);
req3.can_dual_wield = true;
s.set_player_req_state(req3);
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(872)"#)
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(872)"#)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
@@ -365,7 +365,7 @@ fn proficiency_and_reputation_reds() {
..Default::default()
},
);
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(118)"#)
s.run(r#"TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(118)"#)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
@@ -387,7 +387,7 @@ fn proficiency_and_reputation_reds() {
);
s.run(
r#"
TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:SetItemById(889)
TT:SetOwner(Slot9, "ANCHOR_RIGHT"); TT:BenillaSetItemById(889)
assert(TTTextLeft2:GetText() == "[INVTYPE_FINGER]")
assert(TTTextRight2:GetText() == nil or TTTextRight2:GetText() == "",
"a ring never prints its Miscellaneous type")
@@ -416,13 +416,13 @@ fn required_level_one_is_hidden() {
}
s.run(
r#"
local a = CreateFrame("Button", "SlotR"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "SlotR"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetItemById(11)
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:BenillaSetItemById(11)
assert(tt:NumLines() == 1, "req 0: name only, got " .. tt:NumLines())
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetItemById(12)
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:BenillaSetItemById(12)
assert(tt:NumLines() == 1, "req 1 hides like req 0, got " .. tt:NumLines())
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetItemById(13)
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:BenillaSetItemById(13)
assert(tt:NumLines() == 2, "req 2 prints, got " .. tt:NumLines())
assert(TTTextLeft2:GetText() == "[MIN_LEVEL 2]")
"#,
@@ -515,7 +515,7 @@ fn charter_lines_sit_between_the_name_and_the_signable_line() {
);
s.run(
r#"
local a = CreateFrame("Button", "SlotC"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "SlotC"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetBagItem(0, 1)
"#,
@@ -651,7 +651,7 @@ fn instance_tail_creator_and_readable() {
);
s.run(
r#"
local a = CreateFrame("Button", "SlotL"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "SlotL"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetBagItem(0, 1)
"#,
@@ -770,7 +770,7 @@ fn a_runtime_bound_instance_overrides_the_bind_line_to_soulbound() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetBagItem(0, 1)
@@ -804,7 +804,7 @@ fn a_runtime_bound_instance_overrides_the_bind_line_to_soulbound() {
assert_eq!(bind_line(&mut s).0, "[ITEM_BIND_ON_EQUIP]");
// Control 2: a TEMPLATE hover carries no instance at all, so nothing can override.
s.run(r#"TT:SetItemById(871)"#).unwrap();
s.run(r#"TT:BenillaSetItemById(871)"#).unwrap();
assert_eq!(bind_line(&mut s).0, "[ITEM_BIND_ON_EQUIP]");
// Control 3: the override's other arm is ITEM_BIND_QUEST — the same text 4|5 already print.
@@ -55,7 +55,7 @@ fn bag_item_money_law_and_fallback() {
s.run(
r#"
money_fired = nil
local a = CreateFrame("Button", "Slot3"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot3"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetScript("OnTooltipAddMoney", function(self) money_fired = arg1 end)
-- No merchant: no money handler fires.
@@ -129,10 +129,10 @@ fn wrap_lines_measure_wrapped_in_one_pass_and_survive_the_reenter_loop() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot4"); a:SetPoint("TOPLEFT", 10, -10); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot4"); a:SetPoint("TOPLEFT", 10, -10); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetItemById(7)
tt:BenillaSetItemById(7)
"#,
)
.unwrap();
@@ -174,7 +174,7 @@ fn wrap_lines_measure_wrapped_in_one_pass_and_survive_the_reenter_loop() {
s.run(
r#"
TT:SetOwner(Slot4, "ANCHOR_RIGHT")
TT:SetItemById(7)
TT:BenillaSetItemById(7)
"#,
)
.unwrap();
@@ -219,10 +219,10 @@ fn item_render_hides_the_unit_health_bar() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot6"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot6"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "GameTooltip"); tt:Hide()
local bar = CreateFrame("StatusBar", "GameTooltipStatusBar", tt)
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetSize(100, 8)
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetWidth(100); bar:SetHeight(8)
"#,
)
.unwrap();
@@ -232,7 +232,7 @@ fn item_render_hides_the_unit_health_bar() {
s.run(
r#"
GameTooltip:SetOwner(Slot6, "ANCHOR_RIGHT")
GameTooltip:SetItemById(9)
GameTooltip:BenillaSetItemById(9)
assert(not GameTooltipStatusBar:IsShown(), "item content hides the unit bar")
assert(GameTooltip:IsShown(), "the item tooltip itself shows")
"#,
@@ -278,7 +278,7 @@ fn real_instance_hover_renders_live_durability() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetBagItem(0, 2)
@@ -289,7 +289,7 @@ fn real_instance_hover_renders_live_durability() {
end
assert(found == "[DURABILITY 30/40]", "live pair on a bag hover, got " .. tostring(found))
-- The template/link hover of the SAME item keeps the authored full pair.
tt:SetItemById(2264)
tt:BenillaSetItemById(2264)
found = nil
for i = 1, tt:NumLines() do
local t = getglobal("TTTextLeft" .. i):GetText()
@@ -339,7 +339,7 @@ fn broken_instance_hover_renders_zero_durability() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetBagItem(0, 1)
@@ -415,7 +415,7 @@ fn an_enchanted_instance_renders_its_enchant_line_before_durability() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetBagItem(0, 1)
@@ -442,7 +442,7 @@ fn an_enchanted_instance_renders_its_enchant_line_before_durability() {
);
// The control: the same item as a TEMPLATE hover has no instance, so no enchant line.
s.run(r#"TT:SetItemById(22816)"#).unwrap();
s.run(r#"TT:BenillaSetItemById(22816)"#).unwrap();
let lines = super::lines_of(&mut s);
assert!(
!lines.iter().any(|(t, _)| t.starts_with("Agility")),
@@ -514,7 +514,7 @@ fn enchant_line_colour_is_per_slot_and_sign() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetBagItem(0, 1)
@@ -597,7 +597,7 @@ fn temporary_enchant_line_carries_its_countdown_and_charges() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetBagItem(0, 1)
@@ -623,7 +623,7 @@ fn temporary_enchant_line_carries_its_countdown_and_charges() {
/// `ITEM_RANDOM_ENCHANT` arm is unreachable (§E1's fork). The row's name is the suffix-joined one
/// (`0x5d8b00`, the same string `GetLootSlotInfo` returns), so the plate reads "… of the Monkey".
///
/// This is the reported bug's exact shape: through the template path (`SetItemById`) the same
/// This is the reported bug's exact shape: through the template path (`BenillaSetItemById`) the same
/// hover printed the ITEM_RANDOM_ENCHANT placeholder until the item reached a bag.
#[test]
fn a_looted_roll_shows_its_lines_and_never_the_placeholder() {
@@ -680,7 +680,7 @@ fn a_looted_roll_shows_its_lines_and_never_the_placeholder() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetLootItem(1)
@@ -738,7 +738,7 @@ fn a_looted_item_with_no_roll_shows_neither_line_nor_placeholder() {
}));
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetLootItem(1)
@@ -790,7 +790,7 @@ fn a_linked_roll_shows_its_lines_and_never_the_placeholder() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetHyperlink("|cff1eff00|Hitem:8888:0:584:0|h[Bloodrazor of the Monkey]|h|r")
@@ -835,10 +835,10 @@ fn random_property_template_hover_shows_the_placeholder() {
);
s.run(
r#"
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(Slot, "ANCHOR_RIGHT")
tt:SetItemById(8888)
tt:BenillaSetItemById(8888)
"#,
)
.unwrap();
@@ -60,10 +60,10 @@ fn item_set_block_counts_and_colors() {
s.set_item_set(161, set_view.clone());
s.run(
r#"
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Slot10"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetItemById(6303)
tt:BenillaSetItemById(6303)
-- The spacer is the reference's own literal `0x854b2c` — a space and a newline, which is
-- ONE row carrying one space, not the empty string (an empty AddLine adds no line at all
-- in the reference: `0x530270` bails before its `inc [esi+0x31c]`).
@@ -129,7 +129,7 @@ fn item_set_block_counts_and_colors() {
s.set_item_set(161, gated);
s.run(
r#"
TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:SetItemById(6303)
TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:BenillaSetItemById(6303)
assert(TTTextLeft5:GetText() == "[MIN_SKILL Leatherworking 250]",
"the skill line sits between header and members")
assert(TTTextLeft6:GetText() == " Defias Mark")
@@ -150,7 +150,7 @@ fn item_set_block_counts_and_colors() {
let mut req = PlayerReqState::default();
req.skills.insert(165, 300);
s.set_player_req_state(req);
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:SetItemById(6303)"#)
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:BenillaSetItemById(6303)"#)
.unwrap();
let lines = lines_of(&mut s);
assert_eq!(
@@ -171,7 +171,7 @@ fn item_set_block_counts_and_colors() {
..Default::default()
},
);
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:SetItemById(9999)"#)
s.run(r#"TT:SetOwner(Slot10, "ANCHOR_RIGHT"); TT:BenillaSetItemById(9999)"#)
.unwrap();
assert_eq!(s.take_item_set_asks(), vec![1], "the set ask recorded");
assert!(s.take_errors().is_empty());
@@ -206,7 +206,7 @@ fn quest_item_hovers_render_the_rows_item_by_id() {
}));
s.run(
r#"
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT"); tt:SetQuestItem("choice", 1)
"#,
@@ -36,7 +36,7 @@ fn spellbook_hover_renders_the_verified_shape() {
});
s.run(
r#"
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetSpell(1, "spell")
@@ -102,7 +102,7 @@ fn a_pet_book_hover_reads_the_pets_book_not_the_players() {
});
s.run(
r#"
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "SB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetSpell(1, "pet")
@@ -152,7 +152,7 @@ fn tracking_hover_renders_gold_name_over_white_description() {
);
s.run(
r#"
local f = CreateFrame("Frame", "TRK"); f:SetPoint("CENTER", 0, 0); f:SetSize(10, 10)
local f = CreateFrame("Frame", "TRK"); f:SetPoint("CENTER", 0, 0); f:SetWidth(10); f:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(f, "ANCHOR_BOTTOMLEFT")
tt:SetTrackingSpell()
@@ -206,7 +206,7 @@ fn passive_omits_the_casttime_line() {
});
s.run(
r#"
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetSpell(1, "spell")
@@ -260,7 +260,7 @@ fn player_buff_hover_is_the_aura_variant() {
-- the engine reads the string table at all, not what the strings say.
SPELL_TIME_REMAINING_MIN = "<%d m>"
SPELL_TIME_REMAINING_MIN_P1 = "<%d mm>"
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
-- 0, not 1: SetPlayerBuff takes a 1.12 CACHE POSITION (see `script::aura`'s header).
@@ -329,7 +329,7 @@ fn player_buff_hover_names_the_dispel_class_in_gold() {
);
s.run(
r#"
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetPlayerBuff(0)
@@ -380,7 +380,7 @@ fn requirement_and_reagent_lines_render_in_law_order() {
});
s.run(
r#"
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "SB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetSpell(1, "spell")
@@ -458,7 +458,7 @@ fn unit_buff_and_debuff_hover_render_the_aura_variant_without_remaining() {
);
s.run(
r#"
local a = CreateFrame("Button", "TF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "TF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_BOTTOMRIGHT", 15, -25)
tt:SetUnitDebuff("target", 1)
@@ -535,7 +535,7 @@ fn player_buff_hover_indexes_the_cache_position_not_a_filtered_ordinal() {
);
s.run(
r#"
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "BF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
local function at(i, extra)
tt:SetOwner(a, "ANCHOR_RIGHT")
@@ -587,7 +587,7 @@ fn action_hover_delegates_by_kind() {
);
s.run(
r#"
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetAction(24)
@@ -638,7 +638,7 @@ fn action_hover_on_a_macro_slot_shows_its_name_in_white() {
);
s.run(
r#"
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetAction(24)
@@ -739,7 +739,7 @@ fn set_trainer_service_selects_the_builder_and_never_renders_its_own_line() {
s.run(
r#"
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "AB1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
CreateFrame("GameTooltip", "TT")
-- Row 1 is the "Arms" header: a no-op, not a shifted hit onto a neighbouring service.
@@ -855,7 +855,7 @@ fn set_craft_spell_selects_the_builder_like_the_trainer_hover_does() {
s.run(
r#"
local a = CreateFrame("Button", "AB2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "AB2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
CreateFrame("GameTooltip", "TT")
TT:SetOwner(a, "ANCHOR_RIGHT")
TT:SetCraftSpell(1)
@@ -914,7 +914,7 @@ fn the_remaining_line_is_gated_on_until_cancelled_not_on_the_duration() {
SPELL_TIME_REMAINING_SEC = "<%d s>"
SPELL_TIME_REMAINING_SEC_P1 = "<%d ss>"
BENILLA_ANCHOR = CreateFrame("Button", "BF2")
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetSize(10, 10)
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetWidth(10); BENILLA_ANCHOR:SetHeight(10)
BENILLA_TIP = CreateFrame("GameTooltip", "TT2")
"#,
)
@@ -1003,7 +1003,7 @@ fn quest_reward_spell_getters_and_hovers() {
s.set_spell_tooltip(133, fireball());
s.run(
r#"
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "Q1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
"#,
)
@@ -61,7 +61,7 @@ fn creature_line_law() {
});
s.run(
r#"
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
assert(tt:SetUnit("target") == 1, "SetUnit returns 1 on a live unit")
@@ -130,7 +130,7 @@ fn a_pending_name_titles_unknownobject_and_the_answer_replaces_it() {
s.run(
r#"
UNKNOWNOBJECT = "Unknown" -- GlobalStrings.lua:4444, enUS
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "UF1"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
assert(tt:SetUnit("target") == 1, "a resolved unit shows, name or no name")
@@ -241,7 +241,7 @@ fn faction_line_and_civilian_gate() {
);
s.run(
r#"
local a = CreateFrame("Button", "UF9"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "UF9"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetUnit("target")
@@ -329,7 +329,7 @@ fn level_line_variants() {
);
s.run(
r#"
local a = CreateFrame("Button", "UF2"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "UF2"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
local tt = CreateFrame("GameTooltip", "TT")
tt:SetOwner(a, "ANCHOR_RIGHT")
tt:SetUnit("target")
@@ -450,7 +450,7 @@ fn world_hover_drive_and_health_watcher() {
getglobal("GameTooltipTextLeft1"):SetTextColor(1, 0, 0)
end)
local bar = CreateFrame("StatusBar", "GameTooltipStatusBar", tt)
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetSize(100, 8)
bar:SetPoint("TOPLEFT", tt, "BOTTOMLEFT", 2, -1); bar:SetWidth(100); bar:SetHeight(8)
"#,
)
.unwrap();
@@ -512,7 +512,7 @@ fn minimap_blip_tooltip_shows_and_fades() {
s.run(
r#"
local up = CreateFrame("Frame", "UIParent")
up:SetPoint("BOTTOMLEFT"); up:SetSize(800, 600)
up:SetPoint("BOTTOMLEFT"); up:SetWidth(800); up:SetHeight(600)
"#,
)
.unwrap();
+16 -16
View File
@@ -42,13 +42,13 @@ fn board_and_dialog() -> UiScript {
r#"
Board = CreateFrame("Frame", "Board")
Board:SetPoint("BOTTOMLEFT", 100, 100)
Board:SetSize(300, 300)
Board:SetWidth(300); Board:SetHeight(300)
Board:SetFrameLevel(5)
Board:CreateTexture(nil, "ARTWORK"):SetTexture("Board.blp")
Dialog = CreateFrame("Frame", "Dialog")
Dialog:SetPoint("BOTTOMLEFT", 200, 200) -- overlaps Board's top-right quadrant
Dialog:SetSize(300, 300)
Dialog:SetWidth(300); Dialog:SetHeight(300)
Dialog:CreateTexture(nil, "ARTWORK"):SetTexture("Dialog.blp")
Dialog:Hide()
"#,
@@ -200,7 +200,7 @@ fn the_trigger_is_the_effective_visibility_transition_not_the_show_call() {
r#"
Holder = CreateFrame("Frame", "Holder")
Holder:SetPoint("BOTTOMLEFT", 200, 200)
Holder:SetSize(300, 300)
Holder:SetWidth(300); Holder:SetHeight(300)
Holder:Hide()
Dialog:SetParent(Holder)
Dialog:SetToplevel(true)
@@ -239,7 +239,7 @@ fn a_raise_on_a_frame_that_overlaps_nothing_is_a_total_no_op() {
r#"
Dialog:ClearAllPoints()
Dialog:SetPoint("BOTTOMLEFT", 600, 450) -- clear of Board's (100,100)-(400,400)
Dialog:SetSize(100, 100)
Dialog:SetWidth(100); Dialog:SetHeight(100)
Dialog:SetToplevel(true)
Dialog:Show()
"#,
@@ -311,20 +311,20 @@ fn a_raise_can_never_lift_a_frame_out_of_its_stratum() {
r#"
Med = CreateFrame("Frame", "Med")
Med:SetPoint("BOTTOMLEFT", 100, 100)
Med:SetSize(300, 300)
Med:SetWidth(300); Med:SetHeight(300)
Med:CreateTexture(nil, "ARTWORK"):SetTexture("Med.blp")
LowOther = CreateFrame("Frame", "LowOther")
LowOther:SetFrameStrata("LOW")
LowOther:SetPoint("BOTTOMLEFT", 100, 100)
LowOther:SetSize(300, 300)
LowOther:SetWidth(300); LowOther:SetHeight(300)
LowOther:SetFrameLevel(4)
LowOther:CreateTexture(nil, "ARTWORK"):SetTexture("LowOther.blp")
LowTop = CreateFrame("Frame", "LowTop")
LowTop:SetFrameStrata("LOW")
LowTop:SetPoint("BOTTOMLEFT", 150, 150)
LowTop:SetSize(300, 300)
LowTop:SetWidth(300); LowTop:SetHeight(300)
LowTop:SetToplevel(true)
LowTop:CreateTexture(nil, "ARTWORK"):SetTexture("LowTop.blp")
LowTop:Hide()
@@ -408,11 +408,11 @@ fn compaction_bounds_the_raise_across_repeated_shows() {
r#"
A = CreateFrame("Frame", "A")
A:SetPoint("BOTTOMLEFT", 100, 100)
A:SetSize(300, 300)
A:SetWidth(300); A:SetHeight(300)
A:SetToplevel(true)
B = CreateFrame("Frame", "B")
B:SetPoint("BOTTOMLEFT", 200, 200)
B:SetSize(300, 300)
B:SetWidth(300); B:SetHeight(300)
B:SetToplevel(true)
"#,
)
@@ -451,7 +451,7 @@ fn lua_raise_acts_on_the_nearest_toplevel_ancestor_and_is_silent_without_one() {
Kid = CreateFrame("Frame", "Kid", Dialog)
Loose = CreateFrame("Frame", "Loose")
Loose:SetPoint("BOTTOMLEFT", 200, 200)
Loose:SetSize(300, 300)
Loose:SetWidth(300); Loose:SetHeight(300)
"#,
)
.unwrap();
@@ -575,7 +575,7 @@ fn pressing_a_child_of_a_toplevel_window_raises_the_window() {
Dialog:Show()
Knob = CreateFrame("Frame", "Knob", Dialog)
Knob:SetPoint("CENTER", Dialog, "CENTER")
Knob:SetSize(40, 40)
Knob:SetWidth(40); Knob:SetHeight(40)
Knob:EnableMouse(true)
Board:SetFrameLevel(9)
Dialog:SetFrameLevel(0)
@@ -643,12 +643,12 @@ fn a_chorded_press_raises_the_held_frame_not_the_one_under_the_cursor() {
-- gate is occlusion, so a frame overlapping nothing would decline whatever we press.
Other = CreateFrame("Frame", "Other")
Other:SetPoint("BOTTOMLEFT", 500, 100)
Other:SetSize(200, 200)
Other:SetWidth(200); Other:SetHeight(200)
Other:EnableMouse(true)
Other:SetToplevel(true)
Tile = CreateFrame("Frame", "Tile")
Tile:SetPoint("BOTTOMLEFT", 660, 260) -- clips Other's far corner only
Tile:SetSize(60, 60)
Tile:SetWidth(60); Tile:SetHeight(60)
Tile:SetFrameLevel(9)
Board:SetFrameLevel(9)
@@ -725,7 +725,7 @@ fn a_raise_with_a_hidden_child_keeps_the_windows_own_siblings_level() {
Other:SetFrameStrata("DIALOG")
Other:SetFrameLevel(1)
Other:SetPoint("BOTTOMLEFT", 50, 50)
Other:SetSize(400, 400)
Other:SetWidth(400); Other:SetHeight(400)
Other:EnableMouse(true)
Other:SetToplevel(true)
OtherKid = CreateFrame("Frame", "OtherKid", Other) -- level 2
@@ -734,13 +734,13 @@ fn a_raise_with_a_hidden_child_keeps_the_windows_own_siblings_level() {
Win:SetFrameStrata("DIALOG")
Win:SetFrameLevel(1)
Win:SetPoint("BOTTOMLEFT", 200, 100)
Win:SetSize(400, 400)
Win:SetWidth(400); Win:SetHeight(400)
Win:EnableMouse(true)
Win:SetToplevel(true)
Close = CreateFrame("Button", "Close", Win) -- level 2, visible with Win
Close:SetPoint("BOTTOMRIGHT", Win, "BOTTOMRIGHT", -15, 15)
Close:SetSize(100, 21)
Close:SetWidth(100); Close:SetHeight(21)
Close:EnableMouse(true)
Close:SetScript("OnEnter", function(self) hovered = self:GetName() end)
Close:SetScript("OnClick", function(self) clicked = self:GetName() end)
@@ -0,0 +1,404 @@
//! **The widget-method surface gate** — the sibling of [`super::reference_surface`], for the API
//! an addon reaches through a *widget* rather than through `_G` (decision 2142).
//!
//! `our_globals_stay_inside_the_1_12_surface` has guarded the global namespace since 1189, and the
//! shape gate (1842/1843/2118) guards the arity and return kinds of the methods we *have*. Between
//! them sat the question neither asks: **is the set of method NAMES on each widget class the set
//! 1.12 registers?** A method the reference has and we do not is invisible to the shape gate,
//! which skips what does not exist; a method we have and the reference does not is invisible to
//! everything, and is the superset `api-coverage.sh`'s header calls "not free" — an addon that
//! feature-detects (`if region.GetBlendMode then`) takes a path we cannot honour, and the failure
//! surfaces far from the cause.
//!
//! A corpus census over 110 top-20 and 219 vanilla addons found both directions live at once
//! (2142): `ShaguTweaks` probes `GetBlendMode`, which we were missing, and silently recolours
//! textures the reference leaves alone; `pfUI` and `_Nameplates` call `GameTooltip:GetAnchorType`
//! bare and raise. The demand is in the corpus, which is not in this repo — so this gate does not
//! measure demand. It measures the SURFACE, and every entry in the two declared lists below is
//! where the reading of the demand got written down.
//!
//! ## The reference side is a CHAIN, not 23 flat surfaces
//!
//! The reference's 23 widget registrar tables are mutually **disjoint** — a class's real surface is
//! its own table plus the tables it misses into. Comparing a live `Frame` against `0x878ec0` alone
//! reports 87 of its 88 methods as beyond-1.12, which is how you get a gate nobody believes.
//! [`WIDGET_CHAINS`] is that inheritance, and its two non-obvious rows are byte-settled, not
//! guessed: `0x87c9b8` is the base `Region` map (its 19 names are exactly [`super::super`]'s
//! `REGION_MAP_METHODS`), and `0x847ce4` is `LootButton`, whose whole surface of its own is
//! `SetSlot` (decision 1799, read off the registrar's own `mov edx,1`).
use std::collections::{BTreeMap, BTreeSet};
use crate::script::{widget_method_census, UiScript};
/// `(class, its own registrar table, the tables it inherits)`.
///
/// `WorldFrame` is a `Frame` to Lua (1983/1984) and `TitleRegion` a bare `Region`, so neither has a
/// table of its own among the 23. `TaxiRouteFrame` is absent for the same reason (1828).
const WIDGET_CHAINS: &[(&str, &str, &[&str])] = &[
("Frame", FRAME, &[REGION]),
("WorldFrame", FRAME, &[REGION]),
("Button", BUTTON, FRAME_CHAIN),
("LootButton", "0x847ce4", &[BUTTON, FRAME, REGION]),
("CheckButton", "0x87bf74", &[BUTTON, FRAME, REGION]),
("EditBox", "0x87bb68", FRAME_CHAIN),
("StatusBar", "0x87b010", FRAME_CHAIN),
("Slider", "0x87b260", FRAME_CHAIN),
("ScrollFrame", "0x87b3c0", FRAME_CHAIN),
("Model", MODEL, FRAME_CHAIN),
("PlayerModel", PLAYER_MODEL, &[MODEL, FRAME, REGION]),
(
"DressUpModel",
"0x84f190",
&[PLAYER_MODEL, MODEL, FRAME, REGION],
),
(
"TabardModel",
"0x84ee40",
&[PLAYER_MODEL, MODEL, FRAME, REGION],
),
("MessageFrame", "0x87b960", FRAME_CHAIN),
("ScrollingMessageFrame", "0x87b5c0", FRAME_CHAIN),
("ColorSelect", "0x87abb0", FRAME_CHAIN),
("SimpleHTML", "0x87ba80", FRAME_CHAIN),
("MovieFrame", "0x87ab4c", FRAME_CHAIN),
("GameTooltip", "0x854198", FRAME_CHAIN),
("Minimap", "0x84c538", FRAME_CHAIN),
("Texture", "0x87c128", &[REGION]),
("FontString", "0x87c1d8", &[REGION]),
// The font OBJECT (`CreateFont`) carries its own `GetName`/`GetObjectType`/`IsObjectType` and
// inherits nothing.
("Font", "0x87c7c8", &[]),
("TitleRegion", REGION, &[]),
];
const REGION: &str = "0x87c9b8";
const FRAME: &str = "0x878ec0";
const BUTTON: &str = "0x879d00";
const MODEL: &str = "0x878948";
const PLAYER_MODEL: &str = "0x84f1fc";
const FRAME_CHAIN: &[&str] = &[FRAME, REGION];
/// Methods the reference registers that this client does not answer — **a list that may only
/// shrink**, one row per name, naming every class it is missing on and why it has not been built.
///
/// The corpus demand behind each is 2142's census. A name with real, live callers does not belong
/// here: it belongs implemented.
const NOT_YET_ANSWERED: &[(&str, &str, &str)] = &[
// ── The MovieFrame trio: zero addon demand, and the stock GLUE screen calls all three ──────
// `_extracted_gluexml/MovieFrame.lua:23/30/35/47/71`. The glue screens stay ours permanently
// (0068 §8), so the intro-movie screen is owed these however little the ecosystem wants them —
// which makes this the one group here that is a real backlog item rather than a name nobody
// has ever typed. benilla has no movie playback at all, so it is a feature, not a verb.
(
"StartMovie",
"MovieFrame",
"the intro-movie screen's own verb; benilla plays no movie yet",
),
("StopMovie", "MovieFrame", "the same, on the skip path"),
(
"EnableSubtitles",
"MovieFrame",
"the same — `MovieFrame.lua:35` passes `GetMovieSubtitles()`",
),
// ── The Minimap's four content setters ────────────────────────────────────────────────────
// benilla draws the minimap's blips, arrow and mask from the app side (`minimap/blips`), never
// through Lua; the stock UI does not call these either. Wiring them means routing the art
// choice back through the VM, which nothing has asked for.
(
"SetArrowModel",
"Minimap",
"app-side art; no caller in the stock UI or either corpus",
),
("SetBlipTexture", "Minimap", "the same"),
("SetIconTexture", "Minimap", "the same"),
("SetPlayerModel", "Minimap", "the same"),
// ── The Button font-object and text-colour family ─────────────────────────────────────────
// Seven getters and one setter over a button's disabled/highlight/pushed font state. benilla
// keeps that state (`button.rs`'s `Slot` triple) but publishes none of it back. **Zero call
// sites in 329 addon folders and zero in the stock UI** — the whole family is completeness.
(
"GetDisabledFontObject",
"Button CheckButton LootButton",
"the button font-state family; no caller anywhere measured",
),
(
"GetDisabledTextColor",
"Button CheckButton LootButton",
"the same family",
),
(
"GetHighlightFontObject",
"Button CheckButton LootButton",
"the same family",
),
(
"GetHighlightTextColor",
"Button CheckButton LootButton",
"the same family",
),
(
"GetTextFontObject",
"Button CheckButton LootButton",
"the same family",
),
(
"GetPushedTextOffset",
"Button CheckButton LootButton",
"the same family",
),
(
"SetPushedTextOffset",
"Button CheckButton LootButton",
"the same family",
),
// ── Line spacing, on the five classes that carry text ─────────────────────────────────────
// A real layout property benilla's text layout does not model — a setter here is not a store,
// it is a change to how `ui_text` lays out wrapped lines. No caller anywhere measured.
(
"GetSpacing",
"EditBox Font FontString MessageFrame ScrollingMessageFrame",
"line spacing is not modelled by `ui_text`'s layout; no caller anywhere measured",
),
(
"SetSpacing",
"EditBox Font FontString MessageFrame ScrollingMessageFrame",
"the same — the setter is a layout change, not a stored value",
),
// ── EditBox mode read-backs ───────────────────────────────────────────────────────────────
// The four modes are all SET here (`editbox`'s `SetAutoFocus`/`SetMultiLine`/`SetNumeric`/
// `SetPassword`) and none is readable. Cheap, and nothing has ever asked.
(
"IsAutoFocus",
"EditBox",
"the mode is set and not readable back; no caller anywhere measured",
),
("IsMultiLine", "EditBox", "the same"),
("IsNumeric", "EditBox", "the same"),
("IsPassword", "EditBox", "the same"),
// ── ScrollingMessageFrame's line-position surface ─────────────────────────────────────────
// The scroll POSITION verbs (`ScrollUp`/`ScrollDown`/`ScrollToTop`…) are all here; these four
// report or set where that position sits, which our message buffer tracks internally and does
// not publish. No caller anywhere measured.
(
"GetCurrentLine",
"ScrollingMessageFrame",
"the buffer's position is internal; no caller measured",
),
("GetCurrentScroll", "ScrollingMessageFrame", "the same"),
("GetNumLinesDisplayed", "ScrollingMessageFrame", "the same"),
("SetScrollFromBottom", "ScrollingMessageFrame", "the same"),
// ── The colour picker's HSV pair ──────────────────────────────────────────────────────────
// `ColorSelect` publishes the RGB pair; the HSV twin would need the conversion the reference
// does in C. The stock `ColorPickerFrame` uses RGB only, and so does every corpus caller.
(
"GetColorHSV",
"ColorSelect",
"the RGB pair is published; nothing measured asks for HSV",
),
("SetColorHSV", "ColorSelect", "the same"),
// ── One texture read-back ─────────────────────────────────────────────────────────────────
(
"IsDesaturated",
"Texture",
"`SetDesaturated` is here and not readable back; no caller measured",
),
];
/// Methods this client answers that 1.12 does not register on that class — **each a deliberate,
/// reasoned exception, never a quiet superset**. Same contract as
/// [`super::reference_surface::allowed_beyond_1_12`]: a name here must say why it stays.
const ALLOWED_BEYOND: &[(&str, &str, &str)] = &[];
/// The reference's widget rows, `table_va -> {name}`.
fn reference_tables() -> BTreeMap<String, BTreeSet<String>> {
let tsv = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-shapes.tsv"
);
let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv");
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for line in text.lines() {
if line.starts_with('#') || line.starts_with("name\t") {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 5 || f[4] != "widget" {
continue;
}
out.entry(f[3].to_string())
.or_default()
.insert(f[0].to_string());
}
out
}
/// `class -> the effective reference surface` (own table + everything it inherits).
fn reference_surface_by_class() -> BTreeMap<&'static str, BTreeSet<String>> {
let tables = reference_tables();
assert!(
tables.len() == 23,
"the shapes table gave {} widget registrar tables, not the reference's 23 — it changed \
shape, and every count below is against the wrong denominator",
tables.len()
);
let mut out = BTreeMap::new();
for (class, own, bases) in WIDGET_CHAINS {
let mut set = tables.get(*own).cloned().unwrap_or_default();
for base in *bases {
set.extend(tables.get(*base).cloned().unwrap_or_default());
}
assert!(!set.is_empty(), "{class}: empty reference surface");
out.insert(*class, set);
}
out
}
/// `class -> what this VM answers`, asked of a live instance of each.
fn ours_by_class() -> BTreeMap<String, BTreeSet<String>> {
let script = UiScript::new().expect("VM");
let rows = widget_method_census(&script).expect("census");
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (class, method) in rows {
assert_ne!(
method, "!NOT-INSTANTIABLE",
"{class} could not be instantiated — the census cannot speak for a class it never made"
);
out.entry(class).or_default().insert(method);
}
out
}
/// A declared list, `name -> (classes, reason)`.
fn declared(list: &[(&str, &str, &str)]) -> BTreeMap<String, BTreeSet<String>> {
list.iter()
.map(|(name, classes, _)| {
(
(*name).to_string(),
classes.split_whitespace().map(str::to_string).collect(),
)
})
.collect()
}
/// Render a `class -> names` map as the declared-list rows it would need, so a failure hands you
/// the entry to paste rather than a set difference to transcribe.
fn as_rows(diff: &BTreeMap<String, BTreeSet<String>>) -> String {
let mut by_name: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
for (class, names) in diff {
for n in names {
by_name.entry(n).or_default().push(class);
}
}
by_name
.iter()
.map(|(name, classes)| {
format!(
" (\"{name}\", \"{}\", \"WHY\"),",
classes
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(" ")
)
})
.collect::<Vec<_>>()
.join("\n")
}
/// Every widget method the 1.12 client registers on a class is answered by ours, or listed.
#[test]
fn every_1_12_widget_method_is_answered() {
let reference = reference_surface_by_class();
let ours = ours_by_class();
let listed = declared(NOT_YET_ANSWERED);
let mut missing: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut total = 0usize;
for (class, want) in &reference {
let have = ours.get(*class).cloned().unwrap_or_default();
total += want.len();
let gap: BTreeSet<String> = want
.iter()
.filter(|n| !have.contains(*n))
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(*class)))
.cloned()
.collect();
if !gap.is_empty() {
missing.insert((*class).to_string(), gap);
}
}
assert!(
total > 2_000,
"only {total} class/method pairs on the reference side — the chain stopped resolving"
);
assert!(
missing.is_empty(),
"the 1.12 client registers these on a class benilla does not answer them on, and they are \
not listed:\n{}\n\nImplement it, or add the row to NOT_YET_ANSWERED with the reason it \
has not been built.",
as_rows(&missing)
);
// A listed name that came back fails, so the list cannot outlive its fix.
let mut healed: Vec<String> = Vec::new();
for (name, classes, _) in NOT_YET_ANSWERED {
for class in classes.split_whitespace() {
if ours.get(class).is_some_and(|h| h.contains(*name)) {
healed.push(format!("{class}:{name}"));
}
}
}
assert!(
healed.is_empty(),
"these are answered now — take them out of NOT_YET_ANSWERED so the list keeps meaning what \
it says: {healed:?}"
);
}
/// Every widget method benilla answers is one 1.12 registers on that class, or a listed exception.
#[test]
fn our_widget_methods_stay_inside_the_1_12_surface() {
let reference = reference_surface_by_class();
let ours = ours_by_class();
let listed = declared(ALLOWED_BEYOND);
let mut beyond: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (class, have) in &ours {
let Some(want) = reference.get(class.as_str()) else {
panic!("{class} is instantiable here but has no row in WIDGET_CHAINS");
};
let extra: BTreeSet<String> = have
.iter()
.filter(|n| !want.contains(*n))
// Our host bridge and the VM's own internals declare themselves ours by prefix, the
// same discipline `reference_surface` uses for globals.
.filter(|n| !n.starts_with("Benilla") && !n.starts_with("__benilla_"))
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(class)))
.cloned()
.collect();
if !extra.is_empty() {
beyond.insert(class.clone(), extra);
}
}
assert!(
beyond.is_empty(),
"benilla answers these on a class the 1.12 client does not register them on:\n{}\n\n\
1.12 is the target (decision 1188). Either remove it, give it its 1.12 spelling, or add \
the row to ALLOWED_BEYOND WITH the reason it has to stay — an unexplained superset is \
what 1189 had to roll back, and an addon that feature-detects one takes a path we cannot \
honour.",
as_rows(&beyond)
);
let mut gone: Vec<String> = Vec::new();
for (name, classes, _) in ALLOWED_BEYOND {
for class in classes.split_whitespace() {
if !ours.get(class).is_some_and(|h| h.contains(*name)) {
gone.push(format!("{class}:{name}"));
}
}
}
assert!(
gone.is_empty(),
"these are no longer exposed — take them out of ALLOWED_BEYOND: {gone:?}"
);
}