//! 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::("return f.SetText == nil").unwrap()); assert!(s.eval::("return f.SetChecked == nil").unwrap()); assert!(s.eval::("return b.SetText ~= nil").unwrap()); assert!(s.eval::("return b.SetChecked == nil").unwrap()); assert!(s.eval::("return b.SetValue == nil").unwrap()); assert!(s.eval::("return cb.SetChecked ~= nil").unwrap()); assert!(s.eval::("return cb.SetNormalTexture ~= nil").unwrap()); // Buttons are mouse-enabled by construction (the client ctor enables input). assert!(s.eval::("return b:IsMouseEnabled()").unwrap()); assert!(!s.eval::("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 { 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::("return StateBtn:IsEnabled()").unwrap()); s.run("StateBtn:Enable()").unwrap(); assert_eq!(s.eval::("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::("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::("return PushBtn:GetButtonState()").unwrap(), "NORMAL" ); s.run(r#"PushBtn:SetButtonState("PUSHED")"#).unwrap(); assert_eq!( s.eval::("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::("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::("return clicks").unwrap(), 1); assert!(s.eval::("return seen_checked == true").unwrap()); assert!(s.eval::("return Toggler:GetChecked()").unwrap()); // Programmatic Click() rides the same path: toggles back off. s.run("Toggler:Click()").unwrap(); assert_eq!(s.eval::("return clicks").unwrap(), 2); assert!(!s.eval::("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::("return clicks").unwrap(), 2); assert!(!s.eval::("return Toggler:GetChecked()").unwrap()); // SetChecked takes 1/true/nil (1.12 and Era forms). s.run("Toggler:SetChecked(1)").unwrap(); assert!(s.eval::("return Toggler:GetChecked()").unwrap()); s.run("Toggler:SetChecked(nil)").unwrap(); assert!(!s.eval::("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::("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::("return clicks").unwrap(), 1); assert_eq!(s.eval::("return click_btn").unwrap(), "RightButton"); assert_eq!(s.eval::("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::("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::("return clicks").unwrap(), 1); assert!(s.eval::("return seen_down == true").unwrap()); assert!(s.eval::("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::("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 ``): 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 `` 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::("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::("return WidthBtn:GetTextWidth()").unwrap(), 47.0, "the Button forwards to its own label" ); assert_eq!( s.eval::("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::("return LabellessBtn:GetTextWidth()").unwrap(), 0.0 ); // …and it is a BUTTON method: a plain Frame must not have grown one. assert!( s.eval::(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::("return b:GetFontString() == fs").unwrap()); s.run("b:SetText(\"Okay\")").unwrap(); assert_eq!(s.eval::("return fs:GetText()").unwrap(), "Okay"); assert_eq!(s.eval::("return b:GetText()").unwrap(), "Okay"); // Re-parented — always, whatever it was parented to before. assert!(s.eval::("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::("return b:GetFontString() == fs").unwrap()); assert_eq!(s.eval::("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::("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::("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::("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::("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::("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) ); }