- the four extra action bars become per-character options, off by default, on a new Action Bars options page — the toggles ride the server's own byte (`CMSG_SET_ACTIONBAR_TOGGLES` into `PLAYER_FIELD_BYTES`), so a fresh character's zero is the default for free; turning the bottom-left bar off gives the stance bar its previously unbuilt unraised look, end-caps and 64×64 rings - bag windows stop drawing straight through a raised bottom bar: the bottom band has ONE owner — the manage pass — the bag stack joins it through a listener seam, and a guard test walks all eight raisable-bar combinations across 24 bar/tenant pairs and refuses overlap - the item-push card takes the reference's real geometry, closing the gap 0887 measured - resizable addon windows stop at their bounds instead of pinning and walking across the screen: four per-frame f32s with 0.0 the disable sentinel, the clamp on the interactive drag only with the residual rebated out of the drag delta, `<ResizeBounds>` in XML honoured — and `SetFontString`'s three raises land exactly as the bytes have them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
678 lines
26 KiB
Rust
678 lines
26 KiB
Rust
//! Button / CheckButton: method sets, state textures, click registration, additive highlight.
|
||
|
||
use super::common::script;
|
||
use crate::script::*;
|
||
|
||
#[test]
|
||
fn button_methods_exist_only_on_buttons() {
|
||
let s = script();
|
||
s.run(
|
||
r#"
|
||
f = CreateFrame("Frame", "PlainF")
|
||
b = CreateFrame("Button", "Btn")
|
||
cb = CreateFrame("CheckButton", "CBtn")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
// Duck-typing honesty across the class chain (RF-28 method sets).
|
||
assert!(s.eval::<bool>("return f.SetText == nil").unwrap());
|
||
assert!(s.eval::<bool>("return f.SetChecked == nil").unwrap());
|
||
assert!(s.eval::<bool>("return b.SetText ~= nil").unwrap());
|
||
assert!(s.eval::<bool>("return b.SetChecked == nil").unwrap());
|
||
assert!(s.eval::<bool>("return b.SetValue == nil").unwrap());
|
||
assert!(s.eval::<bool>("return cb.SetChecked ~= nil").unwrap());
|
||
assert!(s.eval::<bool>("return cb.SetNormalTexture ~= nil").unwrap());
|
||
// Buttons are mouse-enabled by construction (the client ctor enables input).
|
||
assert!(s.eval::<bool>("return b:IsMouseEnabled()").unwrap());
|
||
assert!(!s.eval::<bool>("return f:IsMouseEnabled()").unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn button_state_textures_switch_with_interaction() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
local b = CreateFrame("Button", "StateBtn")
|
||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||
b:SetNormalTexture("Interface\\N.blp")
|
||
b:SetPushedTexture("Interface\\P.blp")
|
||
b:SetDisabledTexture("Interface\\D.blp")
|
||
b:SetHighlightTexture("Interface\\H.blp")
|
||
b:SetText("Go")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
|
||
let visible = |s: &UiScript| -> Vec<String> {
|
||
s.extract()
|
||
.iter()
|
||
.filter_map(|q| match &q.content {
|
||
QuadContent::Texture { path: Some(p), .. } => Some(p.clone()),
|
||
_ => None,
|
||
})
|
||
.collect()
|
||
};
|
||
|
||
// Idle: normal only (pushed/disabled suppressed; not hovered ⇒ no highlight).
|
||
assert_eq!(visible(&s), vec!["Interface\\N.blp".to_string()]);
|
||
// The ButtonText always draws.
|
||
assert!(s
|
||
.extract()
|
||
.iter()
|
||
.any(|q| matches!(&q.content, QuadContent::Text { text: Some(t), .. } if t == "Go")));
|
||
|
||
// Hover: normal + additive highlight.
|
||
s.mouse_move(50.0, 50.0);
|
||
let v = visible(&s);
|
||
assert!(
|
||
v.contains(&"Interface\\N.blp".to_string()) && v.contains(&"Interface\\H.blp".to_string())
|
||
);
|
||
|
||
// Held down over it: pushed replaces normal (highlight still hovering).
|
||
s.mouse_button(50.0, 50.0, "LeftButton", true);
|
||
let v = visible(&s);
|
||
assert!(
|
||
v.contains(&"Interface\\P.blp".to_string()) && !v.contains(&"Interface\\N.blp".to_string())
|
||
);
|
||
s.mouse_button(50.0, 50.0, "LeftButton", false);
|
||
|
||
// Disabled: disabled texture only, and no highlight even under the cursor.
|
||
s.run("StateBtn:Disable()").unwrap();
|
||
assert_eq!(visible(&s), vec!["Interface\\D.blp".to_string()]);
|
||
assert!(!s.eval::<bool>("return StateBtn:IsEnabled()").unwrap());
|
||
s.run("StateBtn:Enable()").unwrap();
|
||
assert_eq!(s.eval::<String>("return StateBtn:GetText()").unwrap(), "Go");
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
/// **A right-click lights a button up too** — the director's report, and `0x77924b`'s law.
|
||
///
|
||
/// `CButton::OnMouseDown` gates the PushedTexture on `[this+0x330] & (m | m << 8)`: the button
|
||
/// registered for that mouse button in **either** variant. Not on the click firing, not on the
|
||
/// handler doing anything — which is why right-clicking an action, spellbook or pet slot flashes,
|
||
/// while right-clicking a default `LeftButtonUp` button does not.
|
||
#[test]
|
||
fn any_registered_mouse_button_shows_the_pushed_texture() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
local function slot(name, x)
|
||
local b = CreateFrame("Button", name)
|
||
b:SetPoint("BOTTOMLEFT", x, 0); b:SetSize(100, 100)
|
||
b:SetNormalTexture("Interface\\" .. name .. "N.blp")
|
||
b:SetPushedTexture("Interface\\" .. name .. "P.blp")
|
||
return b
|
||
end
|
||
-- A bar slot: both buttons registered, exactly as ActionButton/PetActionButton do.
|
||
slot("Bar", 0):RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||
-- A plain button: the default set, {LeftButtonUp}.
|
||
slot("Plain", 200)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
let shows = |s: &UiScript, path: &str| {
|
||
s.extract()
|
||
.iter()
|
||
.any(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == path))
|
||
};
|
||
|
||
// The bar slot lights under EITHER button.
|
||
for button in ["LeftButton", "RightButton"] {
|
||
s.mouse_move(50.0, 50.0);
|
||
s.mouse_button(50.0, 50.0, button, true);
|
||
assert!(
|
||
shows(&s, "Interface\\BarP.blp") && !shows(&s, "Interface\\BarN.blp"),
|
||
"{button} down must show the pushed art"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return Bar:GetButtonState()").unwrap(),
|
||
"PUSHED",
|
||
"and the state variable the engine writes is one variable ({button})"
|
||
);
|
||
s.mouse_button(50.0, 50.0, button, false);
|
||
assert!(shows(&s, "Interface\\BarN.blp"), "the release restores it");
|
||
}
|
||
|
||
// The plain button lights under the left only — `0x77924b` is a real gate, not a formality.
|
||
s.mouse_move(250.0, 50.0);
|
||
s.mouse_button(250.0, 50.0, "RightButton", true);
|
||
assert!(
|
||
shows(&s, "Interface\\PlainN.blp") && !shows(&s, "Interface\\PlainP.blp"),
|
||
"an unregistered button must not light"
|
||
);
|
||
s.mouse_button(250.0, 50.0, "RightButton", false);
|
||
s.mouse_button(250.0, 50.0, "LeftButton", true);
|
||
assert!(shows(&s, "Interface\\PlainP.blp"));
|
||
s.mouse_button(250.0, 50.0, "LeftButton", false);
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
/// `SetButtonState`/`GetButtonState` (`0x780270`/`0x780180`) — the scripted press state the ref's
|
||
/// `ActionButtonDown/Up` keybind pair drives: PUSHED shows the pushed texture with no mouse
|
||
/// involved, NORMAL restores, unknown states error, and a disabled button answers DISABLED.
|
||
#[test]
|
||
fn set_button_state_drives_the_pushed_visual() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
local b = CreateFrame("Button", "PushBtn")
|
||
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
|
||
b:SetNormalTexture("Interface\\N.blp")
|
||
b:SetPushedTexture("Interface\\P.blp")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
let shows = |s: &UiScript, path: &str| {
|
||
s.extract()
|
||
.iter()
|
||
.any(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == path))
|
||
};
|
||
|
||
assert_eq!(
|
||
s.eval::<String>("return PushBtn:GetButtonState()").unwrap(),
|
||
"NORMAL"
|
||
);
|
||
s.run(r#"PushBtn:SetButtonState("PUSHED")"#).unwrap();
|
||
assert_eq!(
|
||
s.eval::<String>("return PushBtn:GetButtonState()").unwrap(),
|
||
"PUSHED"
|
||
);
|
||
assert!(shows(&s, "Interface\\P.blp") && !shows(&s, "Interface\\N.blp"));
|
||
s.run(r#"PushBtn:SetButtonState("NORMAL")"#).unwrap();
|
||
assert!(shows(&s, "Interface\\N.blp") && !shows(&s, "Interface\\P.blp"));
|
||
// Unknown state: a runtime error, not a silent no-op.
|
||
assert!(s.run(r#"PushBtn:SetButtonState("SIDEWAYS")"#).is_err());
|
||
// Disabled wins the read (Enable/Disable owns that state, not SetButtonState).
|
||
s.run("PushBtn:Disable()").unwrap();
|
||
assert_eq!(
|
||
s.eval::<String>("return PushBtn:GetButtonState()").unwrap(),
|
||
"DISABLED"
|
||
);
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn disabled_button_swallows_clicks_checkbutton_toggles_before_onclick() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
clicks, seen_checked = 0, nil
|
||
local cb = CreateFrame("CheckButton", "Toggler")
|
||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
|
||
cb:SetScript("OnClick", function(self, button, down)
|
||
clicks = clicks + 1
|
||
seen_checked = self:GetChecked()
|
||
end)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
|
||
// Physical click: the checked state flips BEFORE OnClick (the documented contract).
|
||
s.mouse_button(50.0, 50.0, "LeftButton", true);
|
||
s.mouse_button(50.0, 50.0, "LeftButton", false);
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
|
||
assert!(s.eval::<bool>("return seen_checked == true").unwrap());
|
||
assert!(s.eval::<bool>("return Toggler:GetChecked()").unwrap());
|
||
|
||
// Programmatic Click() rides the same path: toggles back off.
|
||
s.run("Toggler:Click()").unwrap();
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 2);
|
||
assert!(!s.eval::<bool>("return Toggler:GetChecked()").unwrap());
|
||
|
||
// Disabled: the click is swallowed — no OnClick, no toggle.
|
||
s.run("Toggler:Disable()").unwrap();
|
||
s.mouse_button(50.0, 50.0, "LeftButton", true);
|
||
s.mouse_button(50.0, 50.0, "LeftButton", false);
|
||
s.run("Toggler:Click()").unwrap();
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 2);
|
||
assert!(!s.eval::<bool>("return Toggler:GetChecked()").unwrap());
|
||
|
||
// SetChecked takes 1/true/nil (1.12 and Era forms).
|
||
s.run("Toggler:SetChecked(1)").unwrap();
|
||
assert!(s.eval::<bool>("return Toggler:GetChecked()").unwrap());
|
||
s.run("Toggler:SetChecked(nil)").unwrap();
|
||
assert!(!s.eval::<bool>("return Toggler:GetChecked()").unwrap());
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn default_registration_is_left_click_only_right_click_reaches_nothing() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
clicks = 0
|
||
local btn = CreateFrame("Button", "Vendor")
|
||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
|
||
btn:SetScript("OnClick", function(self, button, down) clicks = clicks + 1 end)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
|
||
// A right-click (press+release, same frame) reaches OnMouseDown/OnMouseUp but never OnClick —
|
||
// the client's own default registered-click set is {"LeftButtonUp"} only.
|
||
s.mouse_button(50.0, 50.0, "RightButton", true);
|
||
s.mouse_button(50.0, 50.0, "RightButton", false);
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 0);
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn register_for_clicks_grows_right_click_and_carries_the_button_name() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
clicks, click_btn, arg1_btn = 0, nil, nil
|
||
local btn = CreateFrame("Button", "Vendor")
|
||
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
|
||
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||
btn:SetScript("OnClick", function(self, button, down)
|
||
clicks = clicks + 1
|
||
click_btn = button
|
||
arg1_btn = arg1 -- the 1.12 legacy-global convention, same value
|
||
end)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
|
||
s.mouse_button(50.0, 50.0, "RightButton", true);
|
||
s.mouse_button(50.0, 50.0, "RightButton", false);
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
|
||
assert_eq!(s.eval::<String>("return click_btn").unwrap(), "RightButton");
|
||
assert_eq!(s.eval::<String>("return arg1_btn").unwrap(), "RightButton");
|
||
|
||
// RegisterForClicks replaces the set with its whole vararg list — LeftButtonUp still fires.
|
||
s.mouse_button(50.0, 50.0, "LeftButton", true);
|
||
s.mouse_button(50.0, 50.0, "LeftButton", false);
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 2);
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn down_registration_fires_on_press_and_toggles_checked_once() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
clicks, seen_down = 0, nil
|
||
local cb = CreateFrame("CheckButton", "QuickSell")
|
||
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
|
||
cb:RegisterForClicks("LeftButtonDown")
|
||
cb:SetScript("OnClick", function(self, button, down)
|
||
clicks = clicks + 1
|
||
seen_down = down
|
||
end)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
|
||
// The press alone fires OnClick (down=true) and toggles Checked exactly once.
|
||
s.mouse_button(50.0, 50.0, "LeftButton", true);
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
|
||
assert!(s.eval::<bool>("return seen_down == true").unwrap());
|
||
assert!(s.eval::<bool>("return QuickSell:GetChecked()").unwrap());
|
||
|
||
// The release does not fire again — "LeftButtonUp" is no longer in the registered set
|
||
// (RegisterForClicks replaced it, it did not add to it).
|
||
s.mouse_button(50.0, 50.0, "LeftButton", false);
|
||
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
#[test]
|
||
fn highlight_is_additive_and_state_textures_fill_then_anchor() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
local b = CreateFrame("Button", "AddBtn")
|
||
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetSize(36, 36)
|
||
b:SetNormalTexture("Interface\\Ring.blp")
|
||
b:GetNormalTexture():SetSize(64, 64)
|
||
b:SetHighlightTexture("Interface\\Hi.blp")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
s.mouse_move(118.0, 118.0); // hover so the highlight draws
|
||
|
||
let quads = s.extract();
|
||
let find = |path: &str| {
|
||
quads
|
||
.iter()
|
||
.find(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == path))
|
||
.expect(path)
|
||
};
|
||
// The highlight carries the ADD contract (the client's SetHighlightTexture default).
|
||
assert!(matches!(
|
||
&find("Interface\\Hi.blp").content,
|
||
QuadContent::Texture { additive: true, .. }
|
||
));
|
||
// 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
|
||
// FILLS the 36px button.
|
||
let r = find("Interface\\Ring.blp").rect.unwrap();
|
||
assert_eq!(
|
||
(r.left, r.right, r.bottom, r.top),
|
||
(100.0, 136.0, 100.0, 136.0)
|
||
);
|
||
// The real quickslot-overhang idiom is an ANCHOR, not an anchorless size (ActionButton's
|
||
// 66×66 UI-Quickslot2 authors `<Anchor point="CENTER">`): one CENTER point replaces only its
|
||
// own slot, but clearing first leaves the single anchor + the 64px size → the centered
|
||
// overhang, 100..136 → 86..150.
|
||
s.run(
|
||
r#"
|
||
local n = AddBtn:GetNormalTexture()
|
||
n:ClearAllPoints()
|
||
n:SetPoint("CENTER", AddBtn, "CENTER", 0, 0)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
let quads = s.extract();
|
||
let ring = quads
|
||
.iter()
|
||
.find(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == "Interface\\Ring.blp"))
|
||
.unwrap();
|
||
let r = ring.rect.unwrap();
|
||
assert_eq!(
|
||
(r.left, r.right, r.bottom, r.top),
|
||
(86.0, 150.0, 86.0, 150.0)
|
||
);
|
||
// SetBlendMode overrides back to straight alpha.
|
||
s.run(r#"AddBtn:GetHighlightTexture():SetBlendMode("BLEND")"#)
|
||
.unwrap();
|
||
let quads = s.extract();
|
||
let hi = quads
|
||
.iter()
|
||
.find(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == "Interface\\Hi.blp"))
|
||
.unwrap();
|
||
assert!(matches!(
|
||
&hi.content,
|
||
QuadContent::Texture {
|
||
additive: false,
|
||
..
|
||
}
|
||
));
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
}
|
||
|
||
/// The per-state label fonts (`SetTextFontObject`/`SetDisabledFontObject`, XML `<NormalFont>` etc.):
|
||
/// the ButtonText re-points to the CURRENT state's font object at extract — gold while enabled,
|
||
/// gray after Disable() — with no Lua repaint (the client's UIPanelButtonTemplate label behavior).
|
||
/// An explicit SetTextColor survives the re-point (the client's explicitly-set mask).
|
||
#[test]
|
||
fn button_label_repaints_by_state_font_object() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.register_font_object(
|
||
"GoldFont",
|
||
FontObject {
|
||
color: Some([1.0, 0.82, 0.0, 1.0]),
|
||
height: Some(12.0),
|
||
..Default::default()
|
||
},
|
||
);
|
||
s.register_font_object(
|
||
"GrayFont",
|
||
FontObject {
|
||
color: Some([0.5, 0.5, 0.5, 1.0]),
|
||
height: Some(12.0),
|
||
..Default::default()
|
||
},
|
||
);
|
||
s.run(
|
||
r#"
|
||
b = CreateFrame("Button", "FontBtn")
|
||
b:SetPoint("CENTER", 0, 0); b:SetSize(100, 20)
|
||
b:SetText("Label")
|
||
b:SetTextFontObject("GoldFont")
|
||
b:SetDisabledFontObject("GrayFont")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
|
||
let label_color = |s: &mut crate::script::UiScript| {
|
||
s.resolve();
|
||
s.extract()
|
||
.into_iter()
|
||
.find_map(|q| match q.content {
|
||
QuadContent::Text {
|
||
text: Some(t),
|
||
color,
|
||
..
|
||
} if t == "Label" => Some(color),
|
||
_ => None,
|
||
})
|
||
.expect("label text quad")
|
||
};
|
||
|
||
assert_eq!(
|
||
label_color(&mut s),
|
||
Some([1.0, 0.82, 0.0, 1.0]),
|
||
"enabled: gold"
|
||
);
|
||
s.run("b:Disable()").unwrap();
|
||
assert_eq!(
|
||
label_color(&mut s),
|
||
Some([0.5, 0.5, 0.5, 1.0]),
|
||
"disabled: gray"
|
||
);
|
||
s.run("b:Enable()").unwrap();
|
||
assert_eq!(
|
||
label_color(&mut s),
|
||
Some([1.0, 0.82, 0.0, 1.0]),
|
||
"re-enabled: gold again"
|
||
);
|
||
|
||
// An explicit SetTextColor wins over the state font's color (explicitly-set mask).
|
||
s.run("b:GetFontString():SetTextColor(0.1, 0.2, 0.3)")
|
||
.unwrap();
|
||
let c = label_color(&mut s).expect("colored");
|
||
assert!((c[0] - 0.1).abs() < 1e-6 && (c[1] - 0.2).abs() < 1e-6);
|
||
}
|
||
|
||
/// **`Button:GetTextWidth` / `GetTextHeight`** — the reference's own Button text-extent readers
|
||
/// (`0x782290` / `0x782390`; wow-re `widget-api-batch-benilla.md` Q8 carves them present on Button
|
||
/// and `GetStringWidth` **absent**). Both forward to the label FontString's own extent slots, which
|
||
/// is what this asserts: answer a host measure for the label, and the BUTTON must report it.
|
||
///
|
||
/// `Bagnon_Forever/database/ui.lua:61` sizes its character-switch dropdown from
|
||
/// `button:GetTextWidth() + 40`, so a nil method took the whole dropdown down — the director could
|
||
/// not switch characters in Bagnon at all.
|
||
#[test]
|
||
fn a_button_reports_its_labels_extent() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
b = CreateFrame("Button", "WidthBtn", UIParent)
|
||
b:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||
b:SetWidth(120) b:SetHeight(24)
|
||
b:SetText("A Label")
|
||
plain = CreateFrame("Button", "LabellessBtn", UIParent)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
|
||
// Unmeasured is 0 — the same "converges next frame" contract every other metric read has.
|
||
assert_eq!(
|
||
s.eval::<f64>("return WidthBtn:GetTextWidth()").unwrap(),
|
||
0.0
|
||
);
|
||
|
||
let req = s
|
||
.fontstrings_needing_measure()
|
||
.into_iter()
|
||
.find(|r| r.text == "A Label")
|
||
.expect("the label asks the host for its extent");
|
||
s.set_measured_text_unwrapped(&[(req.id, 47.0, 14.0, req.key)]);
|
||
|
||
assert_eq!(
|
||
s.eval::<f64>("return WidthBtn:GetTextWidth()").unwrap(),
|
||
47.0,
|
||
"the Button forwards to its own label"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<f64>("return WidthBtn:GetTextHeight()").unwrap(),
|
||
14.0
|
||
);
|
||
|
||
// A Button with no label at all answers 0 rather than raising: the reference dereferences a
|
||
// FontString pointer (`+0x338`) here that a bare CreateFrame("Button") leaves null, and what it
|
||
// does then is not byte-read — so this takes the harmless number instead of guessing a crash.
|
||
assert_eq!(
|
||
s.eval::<f64>("return LabellessBtn:GetTextWidth()").unwrap(),
|
||
0.0
|
||
);
|
||
|
||
// …and it is a BUTTON method: a plain Frame must not have grown one.
|
||
assert!(
|
||
s.eval::<bool>(r#"return CreateFrame("Frame").GetTextWidth == nil"#)
|
||
.unwrap(),
|
||
"GetTextWidth is Button's, not Region's (wow-re Q8's own split)"
|
||
);
|
||
}
|
||
|
||
/// **`Button:SetFontString` adopts aggressively, and it RAISES** — wow-re
|
||
/// `system/ui/scratch/resize-bounds-and-button-fontstring.md` §5, byte-carved.
|
||
///
|
||
/// The first cut of this binding was a lenient no-op on a bad argument and a bare pointer swap on
|
||
/// a good one. Every line below is a clause of that carve that the plausible reading got wrong.
|
||
#[test]
|
||
fn set_font_string_adopts_the_label_and_raises_on_anything_else() {
|
||
let mut s = crate::script::UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
s.run(
|
||
r#"
|
||
b = CreateFrame("Button", "Btn")
|
||
b:SetWidth(120) b:SetHeight(24)
|
||
b:SetPoint("CENTER", nil, "CENTER", 0, 0)
|
||
fs = b:CreateFontString("MyLabel", "BACKGROUND")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
|
||
// Adoption binds the string as the label the whole rest of the surface reads.
|
||
s.run("b:SetFontString(fs)").unwrap();
|
||
assert!(s.eval::<bool>("return b:GetFontString() == fs").unwrap());
|
||
s.run("b:SetText(\"Okay\")").unwrap();
|
||
assert_eq!(s.eval::<String>("return fs:GetText()").unwrap(), "Okay");
|
||
assert_eq!(s.eval::<String>("return b:GetText()").unwrap(), "Okay");
|
||
|
||
// Re-parented — always, whatever it was parented to before.
|
||
assert!(s.eval::<bool>("return fs:GetParent() == b").unwrap());
|
||
// The draw layer is forced to ARTWORK on the same call (`0x77fd10(parent, 2, 1)`), and that
|
||
// half is NOT asserted here for a stated reason: `GetDrawLayer` is one of the client's nine
|
||
// per-leaf verbs we deliberately do not install (`script::REGION_LEAF_SHARED`'s own note —
|
||
// "absent is absent"), so there is no Lua way to read it back yet. The binding does it; when
|
||
// that verb lands, this is where it gets pinned.
|
||
|
||
// Adopting the SAME string again is a total no-op — the first compare in the client, and the
|
||
// one path that must not run the destroy below.
|
||
s.run("b:SetFontString(fs)").unwrap();
|
||
assert!(s.eval::<bool>("return b:GetFontString() == fs").unwrap());
|
||
assert_eq!(s.eval::<String>("return fs:GetText()").unwrap(), "Okay");
|
||
|
||
// A SECOND adoption DESTROYS the first label rather than orphaning it.
|
||
s.run("fs2 = b:CreateFontString(\"MyLabel2\", \"OVERLAY\") b:SetFontString(fs2)")
|
||
.unwrap();
|
||
assert!(s.eval::<bool>("return b:GetFontString() == fs2").unwrap());
|
||
let err = s.run("fs:GetText()").unwrap_err().to_string();
|
||
assert!(
|
||
err.contains("stale") || err.contains("invalid"),
|
||
"the replaced label must be destroyed, not left live: {err}"
|
||
);
|
||
|
||
// Three distinct raises, all naming the BUTTON — never a silent no-op, and `nil` cannot clear
|
||
// the label from Lua at all.
|
||
for (call, want) in [
|
||
("b:SetFontString()", "Usage: Btn:SetFontString(fontstring)"),
|
||
(
|
||
"b:SetFontString(nil)",
|
||
"Usage: Btn:SetFontString(fontstring)",
|
||
),
|
||
("b:SetFontString(7)", "Usage: Btn:SetFontString(fontstring)"),
|
||
(
|
||
"b:SetFontString(\"x\")",
|
||
"Usage: Btn:SetFontString(fontstring)",
|
||
),
|
||
(
|
||
"b:SetFontString({})",
|
||
"Btn:SetFontString(): Couldn't find 'this' in fontstring",
|
||
),
|
||
(
|
||
"b:SetFontString(b)",
|
||
"Btn:SetFontString(): Wrong object type, expected fontstring",
|
||
),
|
||
] {
|
||
let err = s.run(call).unwrap_err().to_string();
|
||
assert!(err.contains(want), "`{call}` → {err}");
|
||
}
|
||
// A TEXTURE is the near-miss the type gate exists for: it is a Region, and it still fails.
|
||
s.run("tex = b:CreateTexture(nil, \"ARTWORK\")").unwrap();
|
||
let err = s.run("b:SetFontString(tex)").unwrap_err().to_string();
|
||
assert!(
|
||
err.contains("Wrong object type, expected fontstring"),
|
||
"{err}"
|
||
);
|
||
// …and none of that changed the label.
|
||
assert!(s.eval::<bool>("return b:GetFontString() == fs2").unwrap());
|
||
}
|
||
|
||
/// Adoption anchors the string **only when it has none of its own** — and leaves an already
|
||
/// anchored one exactly where the caller put it.
|
||
#[test]
|
||
fn set_font_string_anchors_only_an_unanchored_label() {
|
||
let mut s = crate::script::UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
s.run(
|
||
r#"
|
||
b = CreateFrame("Button", "Btn")
|
||
b:SetWidth(120) b:SetHeight(24)
|
||
b:SetPoint("CENTER", nil, "CENTER", 0, 0)
|
||
bare = b:CreateFontString(nil, "OVERLAY")
|
||
bare:ClearAllPoints()
|
||
placed = b:CreateFontString(nil, "OVERLAY")
|
||
placed:ClearAllPoints()
|
||
placed:SetPoint("TOPLEFT", b, "TOPLEFT", 7, -3)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
assert_eq!(s.eval::<i64>("return bare:GetNumPoints()").unwrap(), 0);
|
||
|
||
s.run("b:SetFontString(placed)").unwrap();
|
||
let (p, _rel, rp, x, y): (String, mlua::Value, String, f64, f64) =
|
||
s.eval("return placed:GetPoint(1)").unwrap();
|
||
assert_eq!(
|
||
(p.as_str(), rp.as_str(), x, y),
|
||
("TOPLEFT", "TOPLEFT", 7.0, -3.0),
|
||
"an already-anchored label keeps its own anchors"
|
||
);
|
||
assert_eq!(s.eval::<i64>("return placed:GetNumPoints()").unwrap(), 1);
|
||
|
||
// The unanchored one gets the justify-derived point, to the matching point on the button.
|
||
s.run("b:SetFontString(bare)").unwrap();
|
||
assert_eq!(s.eval::<i64>("return bare:GetNumPoints()").unwrap(), 1);
|
||
let (p, _rel, rp, x, y): (String, mlua::Value, String, f64, f64) =
|
||
s.eval("return bare:GetPoint(1)").unwrap();
|
||
assert_eq!(
|
||
(p.as_str(), rp.as_str(), x, y),
|
||
("CENTER", "CENTER", 0.0, 0.0)
|
||
);
|
||
}
|