Files
benilla-octo/crates/benilla-ui/src/script/tests/input.rs
T
SamandClaude Fable 5 c1ba444119 the micro menu, the ping meter, and the PvP flag
- The main menu bar's two empty recesses fill. The micro menu — the eight arched
  buttons: character, spellbook, talents, quest log, socials, world map, main menu,
  help — sits in the dark panel between the page arrows and the bags, transcribed
  from the reference's own layout: the character button wears your portrait, a button
  holds itself down while its panel is up, and the talent button appears at level 10.
  The latency meter fills the one slot the bar art leaves transparent for exactly it,
  tinted green/yellow/red on the reference's 300/600 ms thresholds and drawn UNDER
  the bar art, seen through its window, the way 1.12 actually does it. The socials
  and main-menu buttons find their panels one snapshot later; help still waits.
- The UI explains itself on hover: 1.12 ships its detailed two-line tooltips ON — a
  FrameXML default, not the opt-in extra it is often remembered as — so the micro
  buttons, your own portrait, the XP bar, the five bag buttons and the ping meter
  all answer a hover with the reference's own explanations.
- The PvP flag, end to end: crossed swords on the player, target and party frames,
  driven by the two descriptor bits the wire actually carries, and `/pvp` speaks —
  the yellow toast plus the system chat line, the OFF sentence being the one that
  warns the flag lingers for five minutes. It really does linger: turning it off
  waits out the server's timer, which looks like a bug and is the reference.
- Quest markers stopped freezing. The grey `!` over a questgiver above your level
  goes gold the moment you ding, an NPC re-entering view is asked afresh instead of
  wearing a session-old answer, and the marker drops when the questgiver bit goes
  off — the re-ask triggers mirror the reference's own set, plus one named addition
  (your own level changing).
- Water and heights: swimming downhill no longer bounces between swim and fall ten
  times a second — the surface line is a two-sided constraint, so the swimmer
  settles with the river instead of being left behind by it. And collision height
  is per-unit now, read from the unit's own model: a gnome used to swim with a
  third of a yard of water over her head because five depth lines were quoting a
  human's number.
- A round of pixel honesty in text and chat: the newest chat line no longer wears a
  wider drop shadow than every line above it (shadows move in whole pixels now),
  keeps its descenders, and hover boxes stop painting over the chat body. The chat
  input's history browse survives programmatic edits, a failed script chunk warns
  instead of vanishing, and recall deepens to 64 lines. Idle animation got honest
  too: a model's idle is Stand resolved through its own lookup table — never
  file order, which had the duel flag bobbing on its spawn clip — and an armed idle
  is culled on the box its animation authors, not the bind pose, so the flag stops
  vanishing at most camera angles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:17:47 +07:00

264 lines
9.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Input / hit-testing (decision 0068; spec-faithful, not byte-pinned).
use super::common::script;
#[test]
fn enable_mouse_gates_hit_testing() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
who = nil
-- Two full-screen frames. `b` is created later (drawn on top) but mouse-disabled, so it is
-- transparent to hits; the enabled frame behind it (`a`) must capture.
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
a:SetScript("OnEnter", function(self) who = self:GetName() end)
local b = CreateFrame("Frame", "B")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(800, 600); b:EnableMouse(false)
b:SetScript("OnEnter", function(self) who = self:GetName() end)
assert(a:IsMouseEnabled() == true and b:IsMouseEnabled() == false)
"#,
)
.unwrap();
s.resolve();
let hit = s.mouse_move(400.0, 300.0);
assert!(
hit.is_some(),
"a mouse-enabled frame under the cursor captures"
);
assert_eq!(
s.eval::<String>("return who").unwrap(),
"A",
"the top frame is mouse-disabled ⇒ the enabled frame behind it captures"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn topmost_by_draw_order_captures_among_overlapping_enabled_frames() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
who = nil
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
a:SetScript("OnEnter", function(self) who = self:GetName() end)
local b = CreateFrame("Frame", "B")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(800, 600); b:EnableMouse(true)
b:SetScript("OnEnter", function(self) who = self:GetName() end)
"#,
)
.unwrap();
s.resolve();
// Same strata/level: B has the later insertion ⇒ drawn on top ⇒ captures.
s.mouse_move(400.0, 300.0);
assert_eq!(s.eval::<String>("return who").unwrap(), "B");
// Move off everything to clear the mouseover (fires OnLeave, resets focus) before re-testing.
s.mouse_move(-10.0, -10.0);
// Raise A above B by strata (no rect change ⇒ no re-resolve needed): A now captures.
s.run("A:SetFrameStrata('DIALOG')").unwrap();
s.mouse_move(400.0, 300.0);
assert_eq!(s.eval::<String>("return who").unwrap(), "A");
s.mouse_move(-10.0, -10.0);
// Put B in the same (DIALOG) strata but a higher frame level: B captures again.
s.run("B:SetFrameStrata('DIALOG'); B:SetFrameLevel(10)")
.unwrap();
s.mouse_move(400.0, 300.0);
assert_eq!(s.eval::<String>("return who").unwrap(), "B");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn mouse_move_fires_enter_then_leave_across_a_boundary_with_correct_self() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
enters, leaves = 0, 0
enter_self_ok, leave_self_ok = false, false
local a = CreateFrame("Frame", "A") -- left half only
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
a:SetScript("OnEnter", function(self) enters = enters + 1; enter_self_ok = (self == a) end)
a:SetScript("OnLeave", function(self) leaves = leaves + 1; leave_self_ok = (self == a) end)
"#,
)
.unwrap();
s.resolve();
s.mouse_move(200.0, 300.0); // inside A ⇒ OnEnter
s.mouse_move(600.0, 300.0); // outside A ⇒ OnLeave
let (enters, leaves): (i64, i64) = s.eval("return enters, leaves").unwrap();
assert_eq!((enters, leaves), (1, 1));
assert!(
s.eval::<bool>("return enter_self_ok").unwrap(),
"OnEnter self is the frame"
);
assert!(
s.eval::<bool>("return leave_self_ok").unwrap(),
"OnLeave self is the frame"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn onclick_fires_on_press_release_same_frame_not_when_release_lands_elsewhere() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
clicks_a, clicks_b, click_btn = 0, 0, nil
local a = CreateFrame("Frame", "A") -- left half
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(400, 600); a:EnableMouse(true)
a:SetScript("OnClick", function(self, button, down) clicks_a = clicks_a + 1; click_btn = button end)
local b = CreateFrame("Frame", "B") -- right half
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetSize(400, 600); b:EnableMouse(true)
b:SetScript("OnClick", function(self, button, down) clicks_b = clicks_b + 1 end)
"#,
)
.unwrap();
s.resolve();
// Press + release both on A ⇒ OnClick on A (button arg == "LeftButton").
s.mouse_button(200.0, 300.0, "LeftButton", true);
s.mouse_button(200.0, 300.0, "LeftButton", false);
assert_eq!(s.eval::<i64>("return clicks_a").unwrap(), 1);
assert_eq!(s.eval::<String>("return click_btn").unwrap(), "LeftButton");
// Press on A, release on B ⇒ no OnClick on either frame.
s.mouse_button(200.0, 300.0, "LeftButton", true);
s.mouse_button(600.0, 300.0, "LeftButton", false);
assert_eq!(
s.eval::<i64>("return clicks_a").unwrap(),
1,
"release landed off A ⇒ no click on A"
);
assert_eq!(
s.eval::<i64>("return clicks_b").unwrap(),
0,
"release on B was not preceded by a press on B"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn hidden_or_effective_hidden_frame_never_captures() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
entered = false
-- own shown = false
local own = CreateFrame("Frame", "OwnHidden")
own:SetPoint("BOTTOMLEFT", 0, 0); own:SetSize(800, 600); own:EnableMouse(true)
own:SetScript("OnEnter", function() entered = true end)
own:Hide()
-- effective-hidden: child is shown but its parent is hidden
local parent = CreateFrame("Frame", "Par")
parent:SetPoint("BOTTOMLEFT", 0, 0); parent:SetSize(800, 600)
local child = CreateFrame("Frame", "Ch", parent)
child:SetPoint("BOTTOMLEFT", 0, 0); child:SetSize(800, 600); child:EnableMouse(true)
child:SetScript("OnEnter", function() entered = true end)
parent:Hide()
"#,
)
.unwrap();
s.resolve();
assert!(
s.hit_test(400.0, 300.0).is_none(),
"no visible mouse-enabled frame ⇒ no capture"
);
assert!(s.mouse_move(400.0, 300.0).is_none());
assert!(
!s.eval::<bool>("return entered").unwrap(),
"hidden / effective-hidden frames fire no OnEnter"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn mouse_wheel_passes_delta_to_the_captured_frame() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
wheel = nil
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(800, 600); a:EnableMouse(true)
a:SetScript("OnMouseWheel", function(self, delta) wheel = delta end)
"#,
)
.unwrap();
s.resolve();
s.mouse_wheel(400.0, 300.0, 1.0);
assert!((s.eval::<f64>("return wheel").unwrap() - 1.0).abs() < 1e-6);
s.mouse_wheel(400.0, 300.0, -1.0);
assert!((s.eval::<f64>("return wheel").unwrap() + 1.0).abs() < 1e-6);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn hit_rect_insets_shrink_the_mouse_rect_only() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
// A 100×100 frame at the origin with the micro-button shape: an 18-unit dead header at the
// top, and 5 off each other side.
s.run(
r#"
entered = false
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetSize(100, 100); a:EnableMouse(true)
a:SetScript("OnEnter", function() entered = true end)
a:SetHitRectInsets(5, 5, 18, 5)
local l, r, t, b = a:GetHitRectInsets()
assert(l == 5 and r == 5 and t == 18 and b == 5)
"#,
)
.unwrap();
s.resolve();
// Geometry is untouched — insets move the MOUSE rect, never the frame.
assert_eq!(s.eval::<f64>("return A:GetHeight()").unwrap(), 100.0);
assert_eq!(s.eval::<f64>("return A:GetTop()").unwrap(), 100.0);
// Inside the frame but inside the dead header ⇒ no capture (the case the ref's top=18 buys:
// the empty band above a micro button's art must not eat the click).
assert!(
s.hit_test(50.0, 90.0).is_none(),
"a point in the inset header is outside the hit rect"
);
assert!(s.mouse_move(50.0, 90.0).is_none());
assert!(!s.eval::<bool>("return entered").unwrap());
// …and each of the other three insets bites too.
assert!(s.hit_test(2.0, 50.0).is_none(), "left inset");
assert!(s.hit_test(98.0, 50.0).is_none(), "right inset");
assert!(s.hit_test(50.0, 2.0).is_none(), "bottom inset");
// Just below the header, still inside the shrunken rect ⇒ captures.
assert!(s.hit_test(50.0, 80.0).is_some());
assert!(s.mouse_move(50.0, 80.0).is_some());
assert!(s.eval::<bool>("return entered").unwrap());
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn hit_rect_insets_default_to_zero() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(r#"local a = CreateFrame("Frame", "Plain")"#).unwrap();
let (l, r, t, b) = s
.eval::<(f64, f64, f64, f64)>("return Plain:GetHitRectInsets()")
.unwrap();
assert_eq!((l, r, t, b), (0.0, 0.0, 0.0, 0.0), "no inset by default");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}