NTFS load order, a title-sorted AddOns list, and tooltips at the cursor
- addons load in the order NTFS lists a folder, case-insensitive with an underscore before the letters, the order the vanilla corpus was written against and the one that decides which addon provides a shared library (2178) - GetNumAddOns and the index form of every AddOn verb walk a title-sorted list that hides the Blizzard addons the server says to hide, and the character-select AddOns list is in title order (2175) - SetPoint and SetAllPoints raise the reference's own errors for a frame name that does not exist or an omitted relative frame instead of quietly anchoring to the parent, and an explicit nil means the screen root (2176) - a tooltip owned with ANCHOR_CURSOR follows the mouse, which pfUI's tooltip, xpbar and chat modules, pfQuest's browser and TipBuddy ask for; an omitted or unknown anchor is ANCHOR_LEFT (2176) - a $parent name handed to CreateFrame, CreateTexture or CreateFontString expands as it does in XML, so FonzAppraiser's thirty dropdowns and pfQuest's search icon get the names they asked for (2176) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,11 @@ fn seeded() -> UiScript {
|
||||
None,
|
||||
None,
|
||||
);
|
||||
// **The index space exists only once the server has answered** (decision 2175). These tests
|
||||
// are about the prologue's bounds and raises, and a bound of zero would make every one of
|
||||
// them pass vacuously — so the fixture seats the reply an in-world VM has always had, hiding
|
||||
// nothing.
|
||||
s.note_addon_info_reply(&[]);
|
||||
s
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//! **The Lua index space is not the registry** (decision 2175) — a different order, over a
|
||||
//! different set, that does not exist until the server answers.
|
||||
//!
|
||||
//! `GetNumAddOns` and the *index* form of every AddOn verb address a flat array the client
|
||||
//! rebuilds in exactly one place: the tail of `AddOn_ReadAddonInfoReply 0x51da70`
|
||||
//! (`[0x51dc30, 0x51dcdf)`), driven by `SMSG_ADDON_INFO`. It is `## Title`-sorted with
|
||||
//! `SStrCmpI`, filtered by `[rec+0x29]`, and empty before the reply — three properties benilla had
|
||||
//! none of, because it indexed the registry list directly. wow-re
|
||||
//! `system/ui/scratch/addon-registry-scan-and-order.md` §7 (byte-read, and §11-executed over the
|
||||
//! binary's own `qsort 0x73f727` and comparator `0x51deb0`).
|
||||
|
||||
use crate::script::{AddOnInfo, UiScript};
|
||||
|
||||
/// Three addons whose FOLDER order and TITLE order are deliberately different permutations, so a
|
||||
/// test cannot pass by accident on a registry that happens to be sorted.
|
||||
///
|
||||
/// Folder order (registration): `Zulu`, `Alpha`, `Mike`.
|
||||
/// Title order: `Zulu` (`"aardvark"`), `Mike` (`"Middle"`), `Alpha` (`"zebra"`).
|
||||
fn seeded() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let row = |name: &str, title: Option<&str>| AddOnInfo {
|
||||
name: name.into(),
|
||||
title: title.map(str::to_owned),
|
||||
interface: 11200,
|
||||
enabled: true,
|
||||
..Default::default()
|
||||
};
|
||||
s.register_addons(
|
||||
vec![
|
||||
row("Zulu", Some("aardvark")),
|
||||
row("Alpha", Some("zebra")),
|
||||
row("Mike", Some("Middle")),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
/// The names `GetAddOnInfo(i)` answers, in index order.
|
||||
fn by_index(s: &UiScript) -> Vec<String> {
|
||||
s.eval::<Vec<String>>(
|
||||
"local t = {} for i = 1, GetNumAddOns() do t[i] = (GetAddOnInfo(i)) end return t",
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// **The index space is `## Title`-sorted, case-insensitively — not folder order.**
|
||||
///
|
||||
/// Comparator `0x51deb0` resolves both sides through `AddOn_GetTitle 0x51df20` and compares with
|
||||
/// `SStrCmpI` (`0x64a4c0` → `_strnicmp 0x414310`, which folds `'A'..'Z'` by `+0x20` on both
|
||||
/// operands — ASCII only, which is what `to_ascii_lowercase` is).
|
||||
///
|
||||
/// `"Middle"` is the case control: a byte-wise sort would put every capital before every
|
||||
/// lowercase and answer `Middle, aardvark, zebra`.
|
||||
#[test]
|
||||
fn the_index_space_is_title_sorted_case_insensitively_and_not_registry_order() {
|
||||
let mut s = seeded();
|
||||
s.note_addon_info_reply(&[]);
|
||||
assert_eq!(by_index(&s), vec!["Zulu", "Mike", "Alpha"]);
|
||||
}
|
||||
|
||||
/// **A record with no `## Title` sorts under its FOLDER name**, which is the comparator's
|
||||
/// fallback: `AddOn_GetTitle` misses (`0x51e046` returns 0) and `0x51ded0`/`0x51ded6` substitute
|
||||
/// the name the array holds.
|
||||
#[test]
|
||||
fn a_titleless_addon_sorts_under_its_folder_name() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.register_addons(
|
||||
vec![
|
||||
AddOnInfo {
|
||||
name: "Yankee".into(),
|
||||
title: Some("zzz".into()),
|
||||
interface: 11200,
|
||||
enabled: true,
|
||||
..Default::default()
|
||||
},
|
||||
// No title at all — sorts as "bravo", between nothing and "zzz".
|
||||
AddOnInfo {
|
||||
name: "bravo".into(),
|
||||
interface: 11200,
|
||||
enabled: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
s.note_addon_info_reply(&[]);
|
||||
assert_eq!(by_index(&s), vec!["bravo", "Yankee"]);
|
||||
}
|
||||
|
||||
/// **There is no index space until `SMSG_ADDON_INFO` arrives** — `[0xbe1b90]` is zeroed by the
|
||||
/// registry reset `0x51fad1` and written nowhere but the reply's own rebuild.
|
||||
///
|
||||
/// The registry is fully populated here; only the array is absent. The raise is the same one an
|
||||
/// out-of-range index takes, because it *is* the same unsigned bound against a count of zero.
|
||||
#[test]
|
||||
fn there_is_no_index_space_until_the_server_answers() {
|
||||
let s = seeded();
|
||||
assert_eq!(s.eval::<i64>("return GetNumAddOns()").ok(), Some(0));
|
||||
let raised = s
|
||||
.eval::<String>(
|
||||
"local ok, e = pcall(function() GetAddOnInfo(1) end) \
|
||||
if ok then return \"<no raise>\" end return tostring(e)",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
raised.contains("AddOn index must be in the range of 1 to 0"),
|
||||
"{raised}"
|
||||
);
|
||||
// The registry is there all along — the NAME form never went through the array, it probes the
|
||||
// master registry hash (`0x51df20`'s own lookup), so it answers for an addon the index space
|
||||
// cannot reach.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return (GetAddOnInfo('Alpha'))").ok(),
|
||||
Some("Alpha".into())
|
||||
);
|
||||
}
|
||||
|
||||
/// **A record the server hid is gone from the index space and still answers by name.**
|
||||
///
|
||||
/// `status = 2` sets `[rec+0x29] = 1` (`0x51db84`) and the rebuild's `0x51dc4f`/`0x51dc54` drops
|
||||
/// it. On a stock install that is all twelve `Blizzard_*` addons, which is why the reference's own
|
||||
/// AddOns list shows the player's addons and none of Blizzard's — and it is why the name form has
|
||||
/// to keep working, since `LoadAddOn("Blizzard_TalentUI")` is how the talent window opens.
|
||||
#[test]
|
||||
fn a_hidden_addon_leaves_the_index_space_but_still_answers_by_name() {
|
||||
let mut s = seeded();
|
||||
s.note_addon_info_reply(&["mike".into()]); // case-insensitive, as every name compare here is
|
||||
assert_eq!(s.eval::<i64>("return GetNumAddOns()").ok(), Some(2));
|
||||
assert_eq!(by_index(&s), vec!["Zulu", "Alpha"]);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return (GetAddOnInfo('Mike'))").ok(),
|
||||
Some("Mike".into())
|
||||
);
|
||||
}
|
||||
|
||||
/// **An empty reply is not the same as no reply.** A server that hides nothing still brings the
|
||||
/// array into existence; only silence leaves it at zero.
|
||||
#[test]
|
||||
fn a_reply_that_hides_nothing_still_builds_the_array() {
|
||||
let mut s = seeded();
|
||||
assert_eq!(s.eval::<i64>("return GetNumAddOns()").ok(), Some(0));
|
||||
s.note_addon_info_reply(&[]);
|
||||
assert_eq!(s.eval::<i64>("return GetNumAddOns()").ok(), Some(3));
|
||||
}
|
||||
@@ -75,39 +75,157 @@ fn getwidth_falls_back_to_explicit_size_before_resolve() {
|
||||
assert_eq!(w, 123.0);
|
||||
}
|
||||
|
||||
// A *named* relativeTo that doesn't resolve falls back to the parent/owner — the client's
|
||||
// behavior — but must SAY so: the silent version misdirected ItemTextFrame's scrollbar track
|
||||
// onto the parchment (an XML forward reference; anchors resolve at SetPoint time). Both the
|
||||
// frame and the region SetPoint paths warn.
|
||||
/// A *named* `relativeTo` that does not resolve **RAISES** and abandons the call — the reference's
|
||||
/// `luaL_error(0x87ccd4, "%s:SetPoint(): Couldn't find region named '%s'")`, with no fallback and
|
||||
/// no no-op (decision 2176; 2105 pinned the bytes and deferred the change until the addon survey
|
||||
/// could see engine warnings). The region path is the same registered function, so it raises the
|
||||
/// same way — and both quote the receiver's own name, `<unnamed>` (`0x84c7f0`) when it has none.
|
||||
///
|
||||
/// **This replaces a parent/owner fallback plus a warning.** That fallback is what misdirected
|
||||
/// ItemTextFrame's scrollbar track onto the parchment; the XML half of the same miss is a
|
||||
/// different law and keeps its warning (`Loader::apply_anchor`, `0x767800`).
|
||||
#[test]
|
||||
fn setpoint_unresolved_name_warns() {
|
||||
fn setpoint_unresolved_name_raises() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Orphan")
|
||||
f:SetPoint("TOPLEFT", "NoSuchFrame", "TOPLEFT", 0, 0)
|
||||
local t = f:CreateTexture(nil, "ARTWORK")
|
||||
t:SetPoint("TOPRIGHT", "NoSuchRegion")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let w = s.take_warnings();
|
||||
s.run(r#"f = CreateFrame("Frame", "Orphan")"#).unwrap();
|
||||
|
||||
let e = s
|
||||
.run(r#"Orphan:SetPoint("TOPLEFT", "NoSuchFrame", "TOPLEFT", 0, 0)"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
w.iter()
|
||||
.any(|w| w.contains("Orphan") && w.contains("NoSuchFrame")),
|
||||
"frame path: {w:#?}"
|
||||
e.contains("Orphan:SetPoint(): Couldn't find region named 'NoSuchFrame'"),
|
||||
"frame path: {e}"
|
||||
);
|
||||
|
||||
let e = s
|
||||
.run(
|
||||
r#"t = Orphan:CreateTexture(nil, "ARTWORK")
|
||||
t:SetPoint("TOPRIGHT", "NoSuchRegion")"#,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
w.iter()
|
||||
.any(|w| w.contains("Orphan") && w.contains("NoSuchRegion")),
|
||||
"region path: {w:#?}"
|
||||
e.contains("<unnamed>:SetPoint(): Couldn't find region named 'NoSuchRegion'"),
|
||||
"region path (anonymous receiver): {e}"
|
||||
);
|
||||
// A resolvable name stays silent.
|
||||
|
||||
// The anchor was ABANDONED, not applied to the parent: the frame still has none.
|
||||
let n: i64 = s.eval("return Orphan:GetNumPoints()").unwrap();
|
||||
assert_eq!(n, 0, "the raise must leave no anchor behind");
|
||||
|
||||
// `SetAllPoints` has its own string, and a NUMBER takes the name path there (`lua_isstring`)
|
||||
// where `SetPoint` would read it as an offset.
|
||||
let e = s
|
||||
.run(r#"Orphan:SetAllPoints("NoSuchFrame")"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
e.contains("Orphan:SetAllPoints(): Couldn't find region named 'NoSuchFrame'"),
|
||||
"{e}"
|
||||
);
|
||||
let e = s.run("Orphan:SetAllPoints(42)").unwrap_err().to_string();
|
||||
assert!(
|
||||
e.contains("Orphan:SetAllPoints(): Couldn't find region named '42'"),
|
||||
"a number is a string to `lua_isstring`, so it is looked up as `_G[\"42\"]`: {e}"
|
||||
);
|
||||
|
||||
// A resolvable name still works, and warns about nothing.
|
||||
s.run(r#"CreateFrame("Frame", "Target"); Orphan:SetPoint("BOTTOMLEFT", "Target", "TOPLEFT")"#)
|
||||
.unwrap();
|
||||
assert!(s.take_warnings().is_empty());
|
||||
}
|
||||
|
||||
/// The other four raises of the same ladder, each read out of the reference image.
|
||||
///
|
||||
/// * **Usage** `0x87cc28` — an absent `relativeTo` is `TNONE` and fails the `lua_type(L,3)` gate
|
||||
/// (`0x7a25e6`–`0x7a2620`), so a one-argument `SetPoint` is an error on a 1.12 client. Stock
|
||||
/// content makes none (349 call sites, arity histogram `{3:5, 4:1, 5:343}`); five sites in the
|
||||
/// 219-addon vanilla corpus do, all of them in multi-client code.
|
||||
/// * **Unknown region point** `0x87cd04`, from the 9-entry scan `0x6f1840`.
|
||||
/// * **trying to anchor to itself** `0x87cca8` / `0x87cd54`.
|
||||
/// * And the ORDER is observable: the two tag gates run before the point-name scan, so an unknown
|
||||
/// point with an absent `relativeTo` answers Usage, not "Unknown region point".
|
||||
#[test]
|
||||
fn the_other_setpoint_raises() {
|
||||
let s = script();
|
||||
s.run(r#"f = CreateFrame("Frame", "Ladder")"#).unwrap();
|
||||
|
||||
let e = s
|
||||
.run(r#"Ladder:SetPoint("CENTER")"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(e.contains("Usage: Ladder:SetPoint(\"point\""), "{e}");
|
||||
|
||||
let e = s
|
||||
.run(r#"Ladder:SetPoint("MIDDLE", nil, "MIDDLE", 0, 0)"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(e.contains("Ladder:SetPoint(): Unknown region point"), "{e}");
|
||||
|
||||
// Unknown point AND absent relativeTo: the tag gate is first, so this is Usage.
|
||||
let e = s
|
||||
.run(r#"Ladder:SetPoint("MIDDLE")"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(e.contains("Usage:"), "the tag gate runs first: {e}");
|
||||
|
||||
let e = s
|
||||
.run(r#"Ladder:SetPoint("CENTER", Ladder, "CENTER", 0, 0)"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
e.contains("Ladder:SetPoint(): trying to anchor to itself"),
|
||||
"{e}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **A trailing explicit `nil` is PRESENT, and absent is absent** — the one thing that would make
|
||||
/// the Usage raise over-fire. `lua_type(L,3)` distinguishes `LUA_TNIL` (0, the screen-root arm)
|
||||
/// from `LUA_TNONE` (−1, which fails the gate), so the ladder must see the argument count Lua saw
|
||||
/// and not a tuple padded or trimmed on the way in.
|
||||
#[test]
|
||||
fn a_trailing_nil_relative_to_is_present_where_an_absent_one_is_not() {
|
||||
let s = script();
|
||||
s.run(r#"f = CreateFrame("Frame", "Trail")"#).unwrap();
|
||||
// Present-and-nil: legal, the screen root, no offsets.
|
||||
s.run(r#"Trail:SetPoint("CENTER", nil)"#).unwrap();
|
||||
assert_eq!(s.eval::<i64>("return Trail:GetNumPoints()").unwrap(), 1);
|
||||
// Absent: Usage.
|
||||
let e = s
|
||||
.run(r#"Trail:SetPoint("CENTER")"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(e.contains("Usage:"), "{e}");
|
||||
}
|
||||
|
||||
/// An explicit `nil` `relativeTo` is the **SCREEN ROOT** (`0x7a2710 mov eax,ds:0xcf0bd8`), not the
|
||||
/// parent — the arm benilla answered with the parent until 2176. `UIParent.lua:1549` is the one
|
||||
/// stock site; fourteen corpus sites take it, every one of them meaning "the screen"
|
||||
/// (`pfUI`'s screenshot caption, `FixCTGroups`' cursor-space plate).
|
||||
#[test]
|
||||
fn an_explicit_nil_relative_to_is_the_screen_not_the_parent() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
host = CreateFrame("Frame", "NilHost")
|
||||
NilHost:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 200, 100)
|
||||
NilHost:SetWidth(100) NilHost:SetHeight(100)
|
||||
kid = CreateFrame("Frame", "NilKid", NilHost)
|
||||
NilKid:SetWidth(10) NilKid:SetHeight(10)
|
||||
NilKid:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let left: f32 = s.eval("return NilKid:GetLeft()").unwrap();
|
||||
assert_eq!(
|
||||
left, 0.0,
|
||||
"the screen's BOTTOMLEFT, not NilHost's (which is 200)"
|
||||
);
|
||||
}
|
||||
|
||||
/// **A region whose OWNER has no rect still resolves from its own anchors.**
|
||||
///
|
||||
/// `layout.rs`'s region pass used to `continue` when `resolved` held nothing for the owner frame.
|
||||
@@ -141,7 +259,7 @@ fn a_region_resolves_even_when_its_owner_frame_has_no_rect() {
|
||||
|
||||
-- control: identical region, owner that DOES resolve
|
||||
sized = CreateFrame("Frame", "SizedOwner")
|
||||
sized:SetWidth(10) sized:SetHeight(10) sized:SetPoint("CENTER")
|
||||
sized:SetWidth(10) sized:SetHeight(10) sized:SetPoint("CENTER", 0, 0)
|
||||
ctl = sized:CreateTexture("SizedMark", "ARTWORK")
|
||||
ctl:SetWidth(20) ctl:SetHeight(10)
|
||||
ctl:SetPoint("BOTTOMLEFT", host, "BOTTOMLEFT", 5, 7)
|
||||
@@ -201,7 +319,7 @@ fn an_unanchored_owners_region_chain_resolves_nowhere() {
|
||||
-- $parentLeft: anchored to the OWNER (no relativeTo = the owner frame), which has no rect.
|
||||
cap = bare:CreateTexture("StrayLeft", "ARTWORK")
|
||||
cap:SetWidth(25) cap:SetHeight(64)
|
||||
cap:SetPoint("TOPLEFT")
|
||||
cap:SetPoint("TOPLEFT", 0, 0)
|
||||
|
||||
-- $parentMiddle: the sibling chain that turned a zero rect into 115x64 of visible capsule.
|
||||
mid = bare:CreateTexture("StrayMiddle", "ARTWORK")
|
||||
|
||||
@@ -132,7 +132,7 @@ fn get_backdrop_round_trips_through_set_backdrop() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "BC2Plate")
|
||||
f:SetPoint("CENTER")
|
||||
f:SetPoint("CENTER", 0, 0)
|
||||
f:SetWidth(54); f:SetHeight(54)
|
||||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge", tile = true,
|
||||
tileSize = 32, edgeSize = 32,
|
||||
@@ -169,7 +169,7 @@ fn set_backdrop_nil_tears_down() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Plate2")
|
||||
f:SetPoint("CENTER")
|
||||
f:SetPoint("CENTER", 0, 0)
|
||||
f:SetWidth(100); f:SetHeight(100)
|
||||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge" })
|
||||
f:SetBackdrop(nil)
|
||||
|
||||
@@ -255,7 +255,7 @@ fn set_font_object_takes_the_object_or_the_name() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "TwoWays")
|
||||
f:SetWidth(200); f:SetHeight(40); f:SetPoint("CENTER")
|
||||
f:SetWidth(200); f:SetHeight(40); f:SetPoint("CENTER", 0, 0)
|
||||
byObject = f:CreateFontString(nil, "ARTWORK")
|
||||
byObject:SetText("obj")
|
||||
byObject:SetFontObject(GameFontHighlightSmall)
|
||||
@@ -320,7 +320,7 @@ fn dewdrop_recolors_a_row_from_its_own_font_object() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "DdRow")
|
||||
f:SetWidth(120); f:SetHeight(16); f:SetPoint("CENTER")
|
||||
f:SetWidth(120); f:SetHeight(16); f:SetPoint("CENTER", 0, 0)
|
||||
button = { text = f:CreateFontString(nil, "ARTWORK") }
|
||||
button.text:SetText("row")
|
||||
button.text:SetFontObject(GameFontHighlightSmall)
|
||||
@@ -362,7 +362,7 @@ fn mutating_a_font_object_repaints_everything_that_inherits_it() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "ThemeHost")
|
||||
f:SetWidth(200); f:SetHeight(60); f:SetPoint("CENTER")
|
||||
f:SetWidth(200); f:SetHeight(60); f:SetPoint("CENTER", 0, 0)
|
||||
a = f:CreateFontString(nil, "ARTWORK"); a:SetText("inherited")
|
||||
a:SetFontObject(ThemeFont)
|
||||
b = f:CreateFontString(nil, "ARTWORK"); b:SetText("overridden")
|
||||
@@ -445,7 +445,7 @@ fn propagation_is_scoped_to_the_object_that_changed() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "ScopeHost")
|
||||
f:SetWidth(200); f:SetHeight(60); f:SetPoint("CENTER")
|
||||
f:SetWidth(200); f:SetHeight(60); f:SetPoint("CENTER", 0, 0)
|
||||
one = f:CreateFontString(nil, "ARTWORK"); one:SetText("one"); one:SetFontObject(FontOne)
|
||||
two = f:CreateFontString(nil, "ARTWORK"); two:SetText("two"); two:SetFontObject(FontTwo)
|
||||
FontOne:SetFont("Fonts\\CHANGED.TTF", 30)
|
||||
@@ -477,7 +477,7 @@ fn create_font_mints_publishes_and_paints() {
|
||||
Nameplate.Font:SetTextColor(0, 0.5, 1)
|
||||
|
||||
f = CreateFrame("Frame", "PlateHost")
|
||||
f:SetWidth(120); f:SetHeight(20); f:SetPoint("CENTER")
|
||||
f:SetWidth(120); f:SetHeight(20); f:SetPoint("CENTER", 0, 0)
|
||||
Name = f:CreateFontString(nil, "ARTWORK")
|
||||
Name:SetText("plate")
|
||||
Name:SetFontObject(Nameplate.Font)
|
||||
@@ -626,7 +626,7 @@ fn button_state_fonts_take_the_object_and_follow_its_mutation() {
|
||||
s.run(
|
||||
r#"
|
||||
b = CreateFrame("Button", "StateFontButton")
|
||||
b:SetWidth(80); b:SetHeight(22); b:SetPoint("CENTER")
|
||||
b:SetWidth(80); b:SetHeight(22); b:SetPoint("CENTER", 0, 0)
|
||||
b:SetText("go")
|
||||
b:SetTextFontObject(GameFontNormal)
|
||||
"#,
|
||||
@@ -671,7 +671,7 @@ fn an_empty_font_object_copies_nothing_onto_a_fontstring() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "EmptyFontHost")
|
||||
f:SetWidth(120); f:SetHeight(20); f:SetPoint("CENTER")
|
||||
f:SetWidth(120); f:SetHeight(20); f:SetPoint("CENTER", 0, 0)
|
||||
fs = f:CreateFontString(nil, "ARTWORK")
|
||||
fs:SetText("keep")
|
||||
fs:SetFontObject(DressedFont)
|
||||
@@ -859,7 +859,7 @@ fn a_cross_axis_token_erases_the_axis_but_still_draws_centred() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
"f = CreateFrame('Frame', 'ClearAxisFrame')\n\
|
||||
f:SetWidth(200) f:SetHeight(40) f:SetPoint('CENTER')\n\
|
||||
f:SetWidth(200) f:SetHeight(40) f:SetPoint('CENTER', 0, 0)\n\
|
||||
fs = f:CreateFontString()\n\
|
||||
fs:SetAllPoints(f)\n\
|
||||
fs:SetText('erased')",
|
||||
@@ -924,9 +924,9 @@ fn the_font_block_reaches_both_message_frame_tables() {
|
||||
"fo = CreateFont('MsgBlockFont')\n\
|
||||
fo:SetFont('Fonts\\\\FRIZQT__.TTF', 14)\n\
|
||||
mf = CreateFrame('MessageFrame', 'MsgBlockPlain')\n\
|
||||
mf:SetWidth(300) mf:SetHeight(80) mf:SetPoint('CENTER')\n\
|
||||
mf:SetWidth(300) mf:SetHeight(80) mf:SetPoint('CENTER', 0, 0)\n\
|
||||
smf = CreateFrame('ScrollingMessageFrame', 'MsgBlockScroll')\n\
|
||||
smf:SetWidth(300) smf:SetHeight(80) smf:SetPoint('TOPLEFT')",
|
||||
smf:SetWidth(300) smf:SetHeight(80) smf:SetPoint('TOPLEFT', 0, 0)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -382,9 +382,9 @@ 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:SetWidth(400); f:SetHeight(300)
|
||||
f:SetPoint("TOPLEFT", 0, 0); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetPoint("TOPLEFT", 0, 0)
|
||||
-- the corpus idiom: set, then measure, in one statement sequence
|
||||
fs:SetText("Onewarrior")
|
||||
Answer = fs:GetStringWidth()
|
||||
@@ -408,9 +408,9 @@ 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:SetWidth(400); f:SetHeight(300)
|
||||
f:SetPoint("TOPLEFT", 0, 0); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetPoint("TOPLEFT", 0, 0)
|
||||
fs:SetText("Onewarrior")
|
||||
Answer = fs:GetStringWidth()
|
||||
"#,
|
||||
@@ -435,9 +435,9 @@ fn resolve_closes_the_round_trip_when_an_engine_is_installed() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
f:SetPoint("TOPLEFT", 0, 0); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetPoint("TOPLEFT", 0, 0)
|
||||
fs:SetWidth(35) -- a declared width ⇒ the two extents differ
|
||||
fs:SetText("Onewarrior") -- 70 natural, wraps to 2 rows inside 35
|
||||
"#,
|
||||
@@ -469,9 +469,9 @@ fn a_synchronous_measure_satisfies_the_batch_request_too() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
f:SetPoint("TOPLEFT", 0, 0); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetPoint("TOPLEFT", 0, 0)
|
||||
fs:SetText("hello")
|
||||
Answer = fs:GetStringWidth()
|
||||
"#,
|
||||
@@ -505,9 +505,9 @@ fn a_settled_region_is_refound_after_each_measure_input_write() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "Host")
|
||||
f:SetPoint("TOPLEFT"); f:SetWidth(400); f:SetHeight(300)
|
||||
f:SetPoint("TOPLEFT", 0, 0); f:SetWidth(400); f:SetHeight(300)
|
||||
local fs = f:CreateFontString("Label", "ARTWORK")
|
||||
fs:SetPoint("TOPLEFT")
|
||||
fs:SetPoint("TOPLEFT", 0, 0)
|
||||
fs:SetText("one")
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ fn minimap_zoom_api_and_extract() {
|
||||
s.run(
|
||||
r#"
|
||||
m = CreateFrame("Minimap", "TestMinimap")
|
||||
m:SetWidth(140); m:SetHeight(140); m:SetPoint("TOPRIGHT")
|
||||
m:SetWidth(140); m:SetHeight(140); m:SetPoint("TOPRIGHT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -80,7 +80,7 @@ fn minimap_indoor_and_outdoor_zoom_indices_are_independent() {
|
||||
s.run(
|
||||
r#"
|
||||
m = CreateFrame("Minimap", "TestMinimap")
|
||||
m:SetWidth(140); m:SetHeight(140); m:SetPoint("TOPRIGHT")
|
||||
m:SetWidth(140); m:SetHeight(140); m:SetPoint("TOPRIGHT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! Split by subject; the shared `script()` fixture lives in [`common`].
|
||||
|
||||
mod addon_argument_abi;
|
||||
mod addon_index_space;
|
||||
mod anchors;
|
||||
mod backdrop;
|
||||
mod button;
|
||||
|
||||
@@ -435,9 +435,9 @@ fn a_size_less_pane_takes_its_files_rect_in_layout_units() {
|
||||
s.run(
|
||||
r#"
|
||||
a = CreateFrame("Model", "Sized", UIParent)
|
||||
a:SetPoint("CENTER")
|
||||
a:SetPoint("CENTER", 0, 0)
|
||||
b = CreateFrame("Model", "Authored", UIParent)
|
||||
b:SetPoint("CENTER") b:SetWidth(50) b:SetHeight(20)
|
||||
b:SetPoint("CENTER", 0, 0) b:SetWidth(50) b:SetHeight(20)
|
||||
a:SetModel("Interface\\Minimap\\MinimapArrow.mdx")
|
||||
b:SetModel("Interface\\Minimap\\MinimapArrow.mdx")
|
||||
"#,
|
||||
|
||||
@@ -107,8 +107,9 @@ fn setpoint_relative_to_resolves_a_region_alias_global() {
|
||||
assert_eq!(bottom, 20.0);
|
||||
}
|
||||
|
||||
/// A global that is not a widget resolves to nothing — the reference's type-5 + tag check. The
|
||||
/// binding then takes its usual unresolved-target path (parent + a warning), never the value.
|
||||
/// A global that is not a widget resolves to nothing — the reference's type-5 + tag check — and
|
||||
/// the binding then takes its unresolved-name leg, which **raises** (`0x87ccd4`; decision 2176).
|
||||
/// The value is never used, and no anchor is left behind.
|
||||
#[test]
|
||||
fn setpoint_relative_to_ignores_a_non_widget_global() {
|
||||
let mut s = script();
|
||||
@@ -121,20 +122,19 @@ fn setpoint_relative_to_ignores_a_non_widget_global() {
|
||||
NotAWidget = 5
|
||||
Child = CreateFrame("Frame", "Child", Host)
|
||||
Child:SetWidth(20); Child:SetHeight(20)
|
||||
Child:SetPoint("BOTTOMLEFT", "NotAWidget", "BOTTOMRIGHT", 0, 0)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
// Fell back to the parent Host, whose BOTTOMRIGHT is (110, 10).
|
||||
let left: f32 = s.eval("return Child:GetLeft()").unwrap();
|
||||
assert_eq!(left, 110.0);
|
||||
let warnings = s.take_warnings();
|
||||
let e = s
|
||||
.run(r#"Child:SetPoint("BOTTOMLEFT", "NotAWidget", "BOTTOMRIGHT", 0, 0)"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
warnings.iter().any(|w| w.contains("NotAWidget")),
|
||||
"an unresolved named target is reported, not silently redirected: {warnings:?}"
|
||||
e.contains("Child:SetPoint(): Couldn't find region named 'NotAWidget'"),
|
||||
"a global holding a non-widget names nothing: {e}"
|
||||
);
|
||||
let n: i64 = s.eval("return Child:GetNumPoints()").unwrap();
|
||||
assert_eq!(n, 0, "the raise leaves no anchor behind");
|
||||
}
|
||||
|
||||
/// **`$parent` is expanded at RUNTIME, on the anchor path only.** `0x76c5b0` has exactly two call
|
||||
@@ -242,3 +242,49 @@ fn setallpoints_resolves_an_alias_global() {
|
||||
.unwrap();
|
||||
assert_eq!((l, b, w, h), (30.0, 40.0, 120.0, 60.0));
|
||||
}
|
||||
|
||||
/// **A leading `$parent` in a Lua constructor's NAME argument is expanded**, exactly as in XML —
|
||||
/// `CreateFrame 0x7060b0`, `CreateTexture 0x773a20` and `CreateFontString 0x773c30` all build a
|
||||
/// synthetic node carrying `name=` and read it back through `CScriptRegion::SetName 0x76c650`,
|
||||
/// which is one of the expander `0x76c5b0`'s two call sites.
|
||||
///
|
||||
/// Found by decision 2176's raise, which is the point of a raise: the unexpanded name had been
|
||||
/// there all along, degrading quietly into a parent-anchored fallback. `FonzAppraiser` names
|
||||
/// roughly thirty widgets this way and `pfQuest/browser.lua:723` builds its search icon as
|
||||
/// `input:CreateTexture("$parentSearchIcon", "OVERLAY")`.
|
||||
#[test]
|
||||
fn a_constructor_name_expands_parent() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
Root = CreateFrame("Frame", "Root")
|
||||
Mid = CreateFrame("Frame", nil, Root) -- anonymous: the walk skips it
|
||||
Kid = CreateFrame("Frame", "$parentKid", Mid)
|
||||
Icon = Kid:CreateTexture("$parentIcon", "OVERLAY")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return Kid:GetName()").unwrap(),
|
||||
"RootKid",
|
||||
"the base is the first NAMED ancestor of the frame's parent, not the parent itself"
|
||||
);
|
||||
// …and the expanded name is what reached `_G`, which is the only widget namespace (2105).
|
||||
assert_eq!(
|
||||
s.eval::<String>("return RootKid:GetName()").unwrap(),
|
||||
"RootKid"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(r#"return getglobal("$parentKid") == nil"#)
|
||||
.unwrap(),
|
||||
"the raw token names nothing"
|
||||
);
|
||||
// A region's `$parent` is its OWNER frame, so the walk starts one link lower.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return RootKidIcon:GetName()").unwrap(),
|
||||
"RootKidIcon"
|
||||
);
|
||||
// The payoff: an anchor by the built name resolves instead of raising.
|
||||
s.run(r#"Icon:SetPoint("TOPLEFT", "RootKid", "TOPLEFT", 0, 0)"#)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1025,9 +1025,9 @@ fn the_size_getters_take_the_author_first_then_the_natural_width_and_wrapped_hei
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
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")
|
||||
Sized:SetPoint("TOPLEFT", 0, 0); Sized:SetWidth(300); Sized:SetText("Name")
|
||||
Auto = f:CreateFontString("Auto", "ARTWORK")
|
||||
Auto:SetPoint("TOPLEFT"); Auto:SetText("Name")
|
||||
Auto:SetPoint("TOPLEFT", 0, 0); Auto:SetText("Name")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1067,9 +1067,9 @@ fn an_empty_string_reads_back_one_unit_and_a_pending_measure_reads_back_zero() {
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
|
||||
Pending = f:CreateFontString("Pending", "ARTWORK")
|
||||
Pending:SetPoint("TOPLEFT"); Pending:SetText("Name")
|
||||
Pending:SetPoint("TOPLEFT", 0, 0); Pending:SetText("Name")
|
||||
Empty = f:CreateFontString("Empty", "ARTWORK")
|
||||
Empty:SetPoint("TOPLEFT"); Empty:SetText("")
|
||||
Empty:SetPoint("TOPLEFT", 0, 0); Empty:SetText("")
|
||||
assert(Pending:GetHeight() == 0, "a pending measure is not a size, got " .. tostring(Pending:GetHeight()))
|
||||
assert(Empty:GetHeight() == 1, "an EMPTY string is one unit, got " .. tostring(Empty:GetHeight()))
|
||||
assert(Empty:GetWidth() == 1, "both axes")
|
||||
@@ -1093,11 +1093,11 @@ fn a_textures_getters_report_its_texel_span_on_an_unsized_axis() {
|
||||
local f = CreateFrame("Frame", "Owner")
|
||||
f:SetPoint("BOTTOMLEFT", 0, 0); f:SetWidth(100); f:SetHeight(50)
|
||||
Art = f:CreateTexture("Art", "ARTWORK")
|
||||
Art:SetTexture("Interface\\Crest"); Art:SetPoint("TOPLEFT")
|
||||
Art:SetTexture("Interface\\Crest"); Art:SetPoint("TOPLEFT", 0, 0)
|
||||
Half = f:CreateTexture("Half", "ARTWORK")
|
||||
Half:SetTexture("Interface\\Crest"); Half:SetPoint("TOPLEFT"); Half:SetHeight(13)
|
||||
Half:SetTexture("Interface\\Crest"); Half:SetPoint("TOPLEFT", 0, 0); Half:SetHeight(13)
|
||||
Bare = f:CreateTexture("Bare", "ARTWORK")
|
||||
Bare:SetPoint("TOPLEFT")
|
||||
Bare:SetPoint("TOPLEFT", 0, 0)
|
||||
assert(Art:GetWidth() == 128 and Art:GetHeight() == 96, "the art's own texels")
|
||||
assert(Half:GetWidth() == 128 and Half:GetHeight() == 13, "per AXIS: authored 13 wins, width still derived")
|
||||
assert(Bare:GetWidth() == 0, "no art, no span — and no floor on a texture")
|
||||
|
||||
@@ -34,7 +34,7 @@ fn taxi_snapshot_surfaces_and_intents_drain() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "TaxiHost")
|
||||
f:SetWidth(316); f:SetHeight(352); f:SetPoint("CENTER")
|
||||
f:SetWidth(316); f:SetHeight(352); f:SetPoint("CENTER", 0, 0)
|
||||
map = f:CreateTexture("TaxiMapTex", "OVERLAY")
|
||||
SetTaxiMap(map)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ fn set_tex_coord_changes_extracted_uv() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "TcF")
|
||||
f:SetWidth(100); f:SetHeight(100); f:SetPoint("CENTER")
|
||||
f:SetWidth(100); f:SetHeight(100); f:SetPoint("CENTER", 0, 0)
|
||||
t = f:CreateTexture("TcTex", "ARTWORK")
|
||||
t:SetTexture("Interface\\Foo")
|
||||
t:SetTexCoord(0.1, 0.6, 0.2, 0.8)
|
||||
@@ -107,7 +107,7 @@ fn set_font_object_repoints_fontstring() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "FoF")
|
||||
f:SetWidth(120); f:SetHeight(30); f:SetPoint("CENTER")
|
||||
f:SetWidth(120); f:SetHeight(30); f:SetPoint("CENTER", 0, 0)
|
||||
fs = f:CreateFontString("FoText", "ARTWORK")
|
||||
fs:SetText("Hi")
|
||||
fs:SetFontObject("Big")
|
||||
@@ -170,7 +170,7 @@ fn justify_v_defaults_middle_and_overrides() {
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "JvF")
|
||||
f:SetWidth(100); f:SetHeight(30); f:SetPoint("CENTER")
|
||||
f:SetWidth(100); f:SetHeight(30); f:SetPoint("CENTER", 0, 0)
|
||||
fs = f:CreateFontString("JvText", "ARTWORK")
|
||||
fs:SetText("Hi")
|
||||
"#,
|
||||
|
||||
@@ -590,6 +590,144 @@ fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_c
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// **`SetOwner`'s omitted `anchorType` is mode 0 = ANCHOR_LEFT**, and the clear-vs-keep split runs
|
||||
/// between NONE and PRESERVE, not between "points" and "doesn't".
|
||||
///
|
||||
/// 2142's open thread 1 predicted the opposite on both counts — a mode-7 default, and a NONE arm
|
||||
/// that must stop dropping anchors — and the wow-re §5 dispatched for the CURSOR mechanism refuted
|
||||
/// both at the bytes (`system/ui/scratch/tooltip-cursor-anchor-law.md` §0.2/§0.3/§2; decision
|
||||
/// 2176). What is actually there:
|
||||
///
|
||||
/// * `0x53120d` zeroes the binding's local mode before any compare, and `0x531214`'s
|
||||
/// `lua_isstring` gate jumps the whole `SStrCmpI` chain when arg 3 is absent, nil, a boolean or
|
||||
/// a table — so the answer is **0 = ANCHOR_LEFT**, silently, with no `luaL_error` anywhere in
|
||||
/// `[0x531221, 0x53133a)`. The core's `mov [+0x318],7` at `0x530012` is an unconditional
|
||||
/// pre-store `0x530031` overwrites; it survives only when the owner is NULL.
|
||||
/// * `0x52fe90` returns for mode **8** at `0x52fead`, *before* `0x52fec2 call 0x767ed0`, and the
|
||||
/// mode-7 skip beside that clear is gated on an arg1 `SetOwner`'s core always passes as 1. So
|
||||
/// every mode 0..7 clears, and only PRESERVE keeps the placement.
|
||||
#[test]
|
||||
fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
Plate = CreateFrame("GameTooltip", "Plate")
|
||||
Owner = CreateFrame("Frame", "Owner")
|
||||
Owner:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 300, 300)
|
||||
Owner:SetWidth(40) Owner:SetHeight(40)
|
||||
Plate:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
|
||||
Plate:SetWidth(50) Plate:SetHeight(20)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f32>("return Plate:GetLeft()").unwrap(), 100.0);
|
||||
|
||||
// ANCHOR_PRESERVE (mode 8) is the ONE mode that leaves the plate alone.
|
||||
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_PRESERVE")"#)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return Plate:GetLeft()").unwrap(),
|
||||
100.0,
|
||||
"mode 8 returns before the ClearAllPoints"
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return Plate:GetNumPoints()").unwrap(), 1);
|
||||
|
||||
// ANCHOR_NONE (mode 7) clears — which is what makes `GameTooltip_SetDefaultAnchor` safe.
|
||||
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_NONE")"#).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return Plate:GetNumPoints()").unwrap(),
|
||||
0,
|
||||
"mode 7 reaches the clear: arg1 is always 1 from SetOwner's core"
|
||||
);
|
||||
|
||||
// `SetOwner(f)` — no anchor string at all. Mode 0, and mode 0 PLACES: ANCHOR_LEFT pins the
|
||||
// plate's BOTTOMRIGHT to the owner's TOPLEFT, so its right edge sits at the owner's left.
|
||||
s.run(r#"Plate:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)"#)
|
||||
.unwrap();
|
||||
s.run("Plate:SetOwner(Owner)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return Plate:GetAnchorType()").unwrap(),
|
||||
"ANCHOR_LEFT",
|
||||
"the omitted anchorType is the binding's zero-initialised local, mode 0"
|
||||
);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return Plate:GetLeft()").unwrap(),
|
||||
250.0,
|
||||
"the owner's left edge (300) less the plate's own 50 wide"
|
||||
);
|
||||
|
||||
// An UNRECOGNISED string is the same silent mode 0 — the compare chain falls through with the
|
||||
// local untouched, and raises nothing.
|
||||
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_SIDEWAYS")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return Plate:GetAnchorType()").unwrap(),
|
||||
"ANCHOR_LEFT"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`ANCHOR_CURSOR` is a real ninth mode**, not a string to warn about (2142's open thread 2, and
|
||||
/// nine corpus files ask for it): mode 6 clears at `SetOwner` time like mode 7, and then the
|
||||
/// per-frame update `0x530b20` pins the plate's **BOTTOM** to the screen's **BOTTOMLEFT** at the
|
||||
/// cursor's absolute position, divided by the plate's own effective scale. See
|
||||
/// `script::tooltip::cursor_anchor`.
|
||||
#[test]
|
||||
fn anchor_cursor_follows_the_cursor_every_frame() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
Tip = CreateFrame("GameTooltip", "Tip")
|
||||
Host = CreateFrame("Frame", "Host")
|
||||
Tip:SetOwner(Host, "ANCHOR_CURSOR")
|
||||
Tip:AddLine("Copper Ore")
|
||||
-- An explicit size, because this VM has no text measurer: the auto-size pre-pass leaves a
|
||||
-- plate with no measured extents at its declared size, and the placement is what is under
|
||||
-- test, not the width.
|
||||
Tip:SetWidth(120) Tip:SetHeight(40)
|
||||
Tip:Show()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return Tip:GetAnchorType()").unwrap(),
|
||||
"ANCHOR_CURSOR",
|
||||
"the mode round-trips; it is no longer recorded as whatever it was placed by"
|
||||
);
|
||||
assert!(
|
||||
!s.warnings().iter().any(|w| w.contains("ANCHOR_CURSOR")),
|
||||
"a mode we honour is not warned about: {:?}",
|
||||
s.warnings()
|
||||
);
|
||||
|
||||
s.mouse_move(300.0, 200.0);
|
||||
s.resolve();
|
||||
let (left, right, bottom): (f32, f32, f32) = s
|
||||
.eval("return Tip:GetLeft(), Tip:GetRight(), Tip:GetBottom()")
|
||||
.unwrap();
|
||||
assert_eq!(bottom, 200.0, "the plate's BOTTOM sits at the cursor");
|
||||
assert!(
|
||||
((left + right) / 2.0 - 300.0).abs() < 0.001,
|
||||
"centred horizontally on the cursor: {left}..{right}"
|
||||
);
|
||||
|
||||
// It follows. The whole point of mode 6 is that no second SetOwner is needed.
|
||||
s.mouse_move(120.0, 480.0);
|
||||
s.resolve();
|
||||
let (left, right, bottom): (f32, f32, f32) = s
|
||||
.eval("return Tip:GetLeft(), Tip:GetRight(), Tip:GetBottom()")
|
||||
.unwrap();
|
||||
assert_eq!(bottom, 480.0);
|
||||
assert!(
|
||||
((left + right) / 2.0 - 120.0).abs() < 0.001,
|
||||
"{left}..{right}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `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`).
|
||||
@@ -625,7 +763,7 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
|
||||
"a plate nothing has owned is anchored to nothing"
|
||||
);
|
||||
|
||||
// The eight modes SetOwner accepts, each answered back verbatim — ANCHOR_PRESERVE included,
|
||||
// All NINE 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",
|
||||
@@ -634,6 +772,7 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
|
||||
"ANCHOR_TOPLEFT",
|
||||
"ANCHOR_BOTTOMRIGHT",
|
||||
"ANCHOR_BOTTOMLEFT",
|
||||
"ANCHOR_CURSOR",
|
||||
"ANCHOR_NONE",
|
||||
"ANCHOR_PRESERVE",
|
||||
] {
|
||||
@@ -656,20 +795,19 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
|
||||
"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")"#)
|
||||
// An anchor SetOwner does not recognise is the binding's zero-initialised local, mode 0 —
|
||||
// silently, with no raise (`0x53120d`, and no `luaL_error` in the compare chain). Ours adds a
|
||||
// warning the reference does not have, which is a diagnostic and not API surface.
|
||||
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "ANCHOR_SIDEWAYS")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return AnchorTip:GetAnchorType()")
|
||||
.unwrap(),
|
||||
"ANCHOR_RIGHT"
|
||||
"ANCHOR_LEFT"
|
||||
);
|
||||
assert!(
|
||||
s.warnings().iter().any(|w| w.contains("ANCHOR_CURSOR")),
|
||||
"the unhonoured anchor is warned, not silently taken: {:?}",
|
||||
s.warnings().iter().any(|w| w.contains("ANCHOR_SIDEWAYS")),
|
||||
"the unrecognised anchor is still reported to us: {:?}",
|
||||
s.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -512,7 +512,7 @@ fn minimap_blip_tooltip_shows_and_fades() {
|
||||
s.run(
|
||||
r#"
|
||||
local up = CreateFrame("Frame", "UIParent")
|
||||
up:SetPoint("BOTTOMLEFT"); up:SetWidth(800); up:SetHeight(600)
|
||||
up:SetPoint("BOTTOMLEFT", 0, 0); up:SetWidth(800); up:SetHeight(600)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user