A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
397 lines
14 KiB
Rust
397 lines
14 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());
|
|
}
|
|
|
|
/// `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_sized_regions_center() {
|
|
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, .. }
|
|
));
|
|
// The sized ring centers on the 36px button: 100..136 → 64px box at 86..150.
|
|
let r = find("Interface\\Ring.blp").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);
|
|
}
|