Mouse disable drops the press; a chorded press goes to the held frame (#899)
`EnableMouse(false)` only flipped the bit, so a frame disabled while pressed still got OnMouseUp and OnDragStart, and a hovered one kept its hover. A second mouse button pressed while one was held fired on the frame under the cursor rather than the held one. In the reference, a mouse disable on a visible frame runs the same removal a hide does: the capture and the press drop, the hover ends with OnLeave at once. The down handler takes the held capture first and fires there, and a Button clicks on release only while PUSHED and with the cursor inside its own rect. `EnableMouse` now runs that shared removal, the drag arm included. Presses go to the capture when one is held, and the Button release tests pushed and inside rather than topmost. Tests cover a disable while pressed or hovered, chorded presses and the release gate. Fixes #886 Fixes #887 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
07d8c8d4cf
commit
a1130c693a
@@ -920,6 +920,15 @@ pub(super) fn wants_click(model: &Model, h: FrameHandle, name: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The state half of the release click gate (`0x7792fc`): a Button clicks only while PUSHED, which
|
||||
/// the press art sets and a drag start, a hide or a lost press clears; any other kind has no state.
|
||||
pub(super) fn release_may_click(model: &Model, h: FrameHandle) -> bool {
|
||||
match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Button(bs)) => bs.button_state() == ButtonVisualState::Pushed,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `h` is registered for `button` (`"LeftButton"`, …) as Up or Down, the press-art gate.
|
||||
/// `OnMouseDown` (`0x779210`) tests `m | m << 8` against `[this+0x330]` (Down in byte 0, Up in
|
||||
/// byte 1, `0x77924b`) before the Down-only `OnClick` test (`0x77926b`), so any registration for
|
||||
|
||||
@@ -209,9 +209,9 @@ pub(crate) struct Model {
|
||||
/// reference's `[root+0x1100]` flag (set at `0x764cbb`/`0x764b8d`, pumped at `0x7657a1` into
|
||||
/// `0x7660d0`).
|
||||
pub(crate) hover_repick: bool,
|
||||
/// Per button held, the frame its press landed on, for `OnClick`'s same-frame release test;
|
||||
/// `None` once that frame hid (the removal tail emptied the capture), so the release has no
|
||||
/// target but the button still counts as held.
|
||||
/// Per button held, the frame its press went to, the capture else the hover (`0x7662c0`), for
|
||||
/// the release; `None` once that frame left the mouse index (hide or mouse disable emptied
|
||||
/// the capture), so the release has no target but the button still counts as held.
|
||||
pub(crate) mouse_down_on: HashMap<String, Option<FrameHandle>>,
|
||||
/// The client's one mouse-capture slot (`root+0x80`), read by the mouse-down raise. Set at
|
||||
/// `0x7663e6` to the capture, else the hovered frame, so a chorded press keeps the capture;
|
||||
|
||||
@@ -590,10 +590,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"EnableMouse",
|
||||
lua.create_function(|lua, (this, enable): (Table, bool)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.arena
|
||||
.set_mouse_enabled(h, enable);
|
||||
visibility::set_mouse_enabled(lua, h, enable);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//! in the order of the hover walk (`0x7661cd` in `0x7660d0`), which [`crate::order::hit_test`]
|
||||
//! implements.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mlua::Value;
|
||||
|
||||
use crate::layout::Rect;
|
||||
@@ -107,11 +109,7 @@ impl UiScript {
|
||||
// ground-targeted spell is armed a plate refuses the hit before its rect is
|
||||
// tested, so the reticle is placed through it.
|
||||
&& !model.nameplates.vetoes(fh)
|
||||
&& model.resolved.get(&fh).is_some_and(|r| {
|
||||
point_in_rect(hit_rect(&model, fh, *r), x, y)
|
||||
})
|
||||
&& effective_clip(&model, &scroll_sources, fh)
|
||||
.is_none_or(|c| point_in_rect(c, x, y))
|
||||
&& frame_contains(&model, &scroll_sources, fh, x, y)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -263,32 +261,56 @@ impl UiScript {
|
||||
/// the button name. Returns whether the UI consumed the event.
|
||||
pub fn mouse_button(&mut self, x: f32, y: f32, button: &str, down: bool) -> bool {
|
||||
let hit_id = self.hit_test(x, y);
|
||||
// ── The press target: the capture, else the hover ──────────────────────────────────────
|
||||
//
|
||||
// The press handler `0x7662c0` takes `root+0x80` first (`0x76637f`) and the hover
|
||||
// `root+0x7c` only when none is held (`0x766389`); everything after, the raise, the
|
||||
// title-region test, the capture store and the press slot (`vt+0x68`: `OnMouseDown`, the
|
||||
// drag arm, a Button's press art and Down click), runs on that one frame. So a second
|
||||
// button pressed over another frame fires on the held one. `mouse_down_on` keeps the
|
||||
// frame per button for the release.
|
||||
let press_target: Option<crate::widget::FrameHandle> = if down {
|
||||
let mut model = self.model_mut();
|
||||
// A press for a button still held means its release never came (a focus loss); the
|
||||
// reference's capture never loses one. That frame gets the release edge so it does not
|
||||
// stay pushed, and with no other button down the stale capture goes too, so this
|
||||
// press is not aimed at it.
|
||||
if let Some(stale) = model.mouse_down_on.remove(button).flatten() {
|
||||
button::edge(&mut model, stale, ButtonState::on_mouse_up);
|
||||
}
|
||||
if model.mouse_down_on.is_empty() {
|
||||
model.mouse_capture = None;
|
||||
}
|
||||
let held = model
|
||||
.mouse_capture
|
||||
.filter(|&c| model.arena.frame(c).is_some());
|
||||
held.or_else(|| hit_id.and_then(|id| model.id_to_frame.get(&id).copied()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let press_id: Option<u32> =
|
||||
press_target.and_then(|h| self.model_ref().frame_to_id.get(&h).copied());
|
||||
// ── The raise, first ────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The press handler `0x7662c0` raises the capture (`root+0x80`) else the hover frame
|
||||
// (`root+0x7c`) through `0x76a5b0`, called unguarded at `0x766392` (the toplevel walk is
|
||||
// inside `0x7650f0`), before the title-region test: a click on a child or a title bar
|
||||
// brings its window forward, and a chorded press raises the window being dragged. The
|
||||
// release handler `0x766420` never raises.
|
||||
if down {
|
||||
let target = self.model_ref().mouse_capture;
|
||||
if let Some(t) = target.or_else(|| self.hit_test_frame(x, y)) {
|
||||
let mut model = self.model_mut();
|
||||
super::object::toplevel::raise(&mut model, t);
|
||||
}
|
||||
// `0x76a5b0` is called unguarded at `0x766392` (the toplevel walk is inside `0x7650f0`),
|
||||
// before the title-region test: a click on a child or a title bar brings its window
|
||||
// forward, and a chorded press raises the window being dragged. The release handler
|
||||
// `0x766420` never raises.
|
||||
if let Some(t) = press_target {
|
||||
let mut model = self.model_mut();
|
||||
super::object::toplevel::raise(&mut model, t);
|
||||
}
|
||||
// ── The title region swallows the press ─────────────────────────────────────────────────
|
||||
//
|
||||
// A press inside the title region starts a mode-2 move and ends there: `0x7662c0` tests
|
||||
// the region first and only a miss reaches `0x7663e6`, the capture and `OnMouseDown`. So
|
||||
// no `OnMouseDown`, drag arm, `OnClick` or double-click stamp, and the event is consumed.
|
||||
if down {
|
||||
if let Some(h) = self.hit_test_frame(x, y) {
|
||||
if self.title_region_hit(h, x, y) {
|
||||
self.model_mut().cursor_pos = (x, y);
|
||||
crate::script::object::movable::start_title_move(&mut self.model_mut(), h);
|
||||
return true;
|
||||
}
|
||||
// A press inside the target's title region starts a mode-2 move and ends there:
|
||||
// `0x7662c0` tests the region first and only a miss reaches `0x7663e6`, the capture and
|
||||
// `OnMouseDown`. So no `OnMouseDown`, drag arm, `OnClick` or double-click stamp, and the
|
||||
// event is consumed.
|
||||
if let Some(h) = press_target {
|
||||
if self.title_region_hit(h, x, y) {
|
||||
self.model_mut().cursor_pos = (x, y);
|
||||
crate::script::object::movable::start_title_move(&mut self.model_mut(), h);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// ── The release ends a title move ───────────────────────────────────────────────────────
|
||||
@@ -327,6 +349,7 @@ impl UiScript {
|
||||
return hit_id.is_some();
|
||||
}
|
||||
};
|
||||
let mut press_art: Option<crate::widget::FrameHandle> = None;
|
||||
#[allow(clippy::type_complexity)]
|
||||
let (
|
||||
click_id,
|
||||
@@ -348,48 +371,45 @@ impl UiScript {
|
||||
let mut model = self.model_mut();
|
||||
let hit_handle = hit_id.and_then(|id| model.id_to_frame.get(&id).copied());
|
||||
if down {
|
||||
let displaced = match hit_handle {
|
||||
match press_target {
|
||||
Some(h) => model.mouse_down_on.insert(button.to_string(), Some(h)),
|
||||
None => model.mouse_down_on.remove(button),
|
||||
}
|
||||
.flatten();
|
||||
// The press edge, `SetButtonState(PUSHED)` (`0x7792b1`), for a button registered
|
||||
// for this mouse button up or down (`0x779210` tests `m | m << 8` at `0x77924b`).
|
||||
if let Some(h) = hit_handle.filter(|&h| button::wants_press_visual(&model, h, button))
|
||||
{
|
||||
button::edge(&mut model, h, ButtonState::on_mouse_down);
|
||||
}
|
||||
// A press while this mouse button still holds another frame means a release was
|
||||
// lost (a focus loss); that frame gets it so it does not stay pushed. The
|
||||
// reference's capture never loses one.
|
||||
if let Some(h) = displaced {
|
||||
button::edge(&mut model, h, ButtonState::on_mouse_up);
|
||||
}
|
||||
// `0x7663e6` sets the capture `root+0x80` only when none is held: a second button
|
||||
// pressed elsewhere does not displace it.
|
||||
model.mouse_capture = model.mouse_capture.or(hit_handle);
|
||||
};
|
||||
// The Button press slot `0x779210` runs both legs only with the cursor inside the
|
||||
// button (`0x77925c`) and the mouse button registered for it (`0x77924b` tests
|
||||
// `m | m << 8`): the Down click for `…ButtonDown`, then the press edge,
|
||||
// `SetButtonState(PUSHED)` (`0x7792b1`), for either registration, applied below
|
||||
// after the scripts as the slot orders them.
|
||||
let sources = scroll_clip_sources(&model);
|
||||
let inside = press_target.filter(|&h| {
|
||||
button::wants_press_visual(&model, h, button)
|
||||
&& frame_contains(&model, &sources, h, x, y)
|
||||
});
|
||||
press_art = inside;
|
||||
// `0x7663e9` stores the target as the capture: the one held, else the hover.
|
||||
model.mouse_capture = press_target;
|
||||
// A press replaces any in-flight gesture; a started one gets its `OnDragStop`
|
||||
// below, outside this borrow.
|
||||
let abandoned = cursor::abandon_drag(&mut model);
|
||||
cursor::arm_drag(&mut model, hit_handle, button, (x, y));
|
||||
cursor::arm_drag(&mut model, press_target, button, (x, y));
|
||||
// A left press on a Slider grabs the thumb, or on the track seats the thumb under
|
||||
// the cursor first (the returned value jump).
|
||||
let jump = if button == "LeftButton" {
|
||||
super::slider::begin_drag(&mut model, hit_handle, x, y)
|
||||
super::slider::begin_drag(&mut model, press_target, x, y)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A left press in a ColorSelect's wheel or value strip also jumps the colour to the
|
||||
// click point: its press handler calls its cursor handler (`0x78bf70`).
|
||||
let color_jump = if button == "LeftButton" {
|
||||
super::colorselect::begin_drag(&mut model, hit_handle, x, y)
|
||||
super::colorselect::begin_drag(&mut model, press_target, x, y)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let wants = format!("{button}Down");
|
||||
let click = hit_handle
|
||||
let click = inside
|
||||
.filter(|&h| button::wants_click(&model, h, &wants))
|
||||
.and(hit_id);
|
||||
.and(press_id);
|
||||
(click, None, false, jump, None, abandoned, color_jump)
|
||||
} else {
|
||||
let entry = model.mouse_down_on.remove(button);
|
||||
@@ -406,12 +426,15 @@ impl UiScript {
|
||||
super::slider::end_drag(&mut model);
|
||||
super::colorselect::end_drag(&mut model);
|
||||
}
|
||||
let same_frame = matches!(
|
||||
(pressed, hit_handle),
|
||||
(Some(p), Some(released)) if p == released
|
||||
);
|
||||
let release = cursor::take_drag(&mut model, button);
|
||||
let started = release.as_ref().is_some_and(|r| r.started);
|
||||
// The Button release slot `0x7792d0` clicks only a PUSHED button (`0x7792fc`) with
|
||||
// the cursor inside its own rect (`0x779335`), not "the topmost frame under the
|
||||
// cursor", read before the release edge below un-pushes it.
|
||||
let sources = scroll_clip_sources(&model);
|
||||
let may_click = pressed.filter(|&h| {
|
||||
button::release_may_click(&model, h) && frame_contains(&model, &sources, h, x, y)
|
||||
});
|
||||
// The release edge, `SetButtonState(NORMAL)` (`0x7793de`), on the pressed frame
|
||||
// wherever the cursor is, skipped after a started drag (`0x7792df` tests
|
||||
// `0x76c040`), which already un-pressed it.
|
||||
@@ -431,16 +454,16 @@ impl UiScript {
|
||||
let click = if started {
|
||||
None
|
||||
} else {
|
||||
hit_handle
|
||||
.filter(|&h| same_frame && button::wants_click(&model, h, &wants))
|
||||
.and(hit_id)
|
||||
may_click
|
||||
.filter(|&h| button::wants_click(&model, h, &wants))
|
||||
.and_then(|h| model.frame_to_id.get(&h).copied())
|
||||
};
|
||||
// The double click, on the release that qualified as a click: `0x77938d` in the
|
||||
// mouse-up dispatcher `0x7792d0` fires `OnDoubleClick` instead of the second
|
||||
// `OnClick` (`0x77939d jmp 0x7793b5` skips the single leg), only for a frame with
|
||||
// an `OnDoubleClick` script (`[+0x4d4] != 0`). What suppresses the click
|
||||
// suppresses it too.
|
||||
let target = click.and(hit_handle);
|
||||
let target = click.and(may_click);
|
||||
let double = target.filter(|&h| {
|
||||
model
|
||||
.scripts
|
||||
@@ -488,7 +511,7 @@ impl UiScript {
|
||||
// that captured nothing fires nothing (`0x766498`). A resize grip dragged out from under
|
||||
// the cursor still gets its `OnMouseUp`, so its resize ends.
|
||||
let script = if down { "OnMouseDown" } else { "OnMouseUp" };
|
||||
if let Some(id) = if down { hit_id } else { captured_id } {
|
||||
if let Some(id) = if down { press_id } else { captured_id } {
|
||||
if let Err(e) = event::fire_widget_handler(&self.lua, id, script, vec![btn.clone()]) {
|
||||
self.push_error(e);
|
||||
}
|
||||
@@ -534,7 +557,7 @@ impl UiScript {
|
||||
// whatever its `autoFocus`, before any `OnClick` (`0x77b800`).
|
||||
if button == "LeftButton" {
|
||||
if down {
|
||||
if let Some(id) = hit_id {
|
||||
if let Some(id) = press_id {
|
||||
editbox::click(&self.lua, id, x, y);
|
||||
}
|
||||
} else {
|
||||
@@ -561,6 +584,12 @@ impl UiScript {
|
||||
// before `OnClick` fires.
|
||||
button::click_button(&self.lua, id, button, down, false);
|
||||
}
|
||||
// The press art last (`0x7792b1` follows the Down click `0x779285`): `OnMouseDown`
|
||||
// (`0x76bf70`, at the top of the slot) and a Down `OnClick` see the button unpushed, and
|
||||
// the release's pushed gate reads what they left.
|
||||
if let Some(h) = press_art {
|
||||
button::edge(&mut self.model_mut(), h, ButtonState::on_mouse_down);
|
||||
}
|
||||
// `OnDoubleClick(self, button)` in place of `OnClick` (`0x77938d call [edx+0x98]`), with
|
||||
// the same click sound: its firer `0x779650` is the twin of `OnClick`'s `0x779540` and
|
||||
// passes only the button name of the completing release.
|
||||
@@ -632,6 +661,23 @@ impl UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// The probe `0x76b020` alone: whether `(x, y)` is inside the frame's hit rect and its effective
|
||||
/// ScrollFrame clip, whatever its mouse bit. The Button click legs test it against the capture
|
||||
/// (`0x77925c`, `0x779335`).
|
||||
fn frame_contains(
|
||||
model: &Model,
|
||||
scroll_sources: &HashMap<FrameHandle, Rect>,
|
||||
fh: FrameHandle,
|
||||
x: f32,
|
||||
y: f32,
|
||||
) -> bool {
|
||||
model
|
||||
.resolved
|
||||
.get(&fh)
|
||||
.is_some_and(|r| point_in_rect(hit_rect(model, fh, *r), x, y))
|
||||
&& effective_clip(model, scroll_sources, fh).is_none_or(|c| point_in_rect(c, x, y))
|
||||
}
|
||||
|
||||
/// Point-in-rect in UI space (y-up), inclusive on all four edges, as the reference's probe
|
||||
/// `0x76b020` tests it.
|
||||
pub(super) fn point_in_rect(r: Rect, x: f32, y: f32) -> bool {
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
//! The mouse capture (`root+0x80`): what drops it, and where a press goes while one is held.
|
||||
|
||||
use super::common::script;
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// Two side-by-side mouse frames A (x 100..300) and B (x 400..600), both y 200..400, each logging
|
||||
/// `name:down:<button>` / `name:up:<button>` / `name:enter` / `name:leave`.
|
||||
fn two_frames(kind: &str) -> UiScript {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(&format!(
|
||||
r#"
|
||||
log = {{}}
|
||||
local function note(f, what) table.insert(log, f:GetName()..":"..what) end
|
||||
local function mk(name, x)
|
||||
local f = CreateFrame("{kind}", name)
|
||||
f:SetPoint("BOTTOMLEFT", x, 200); f:SetWidth(200); f:SetHeight(200); f:EnableMouse(true)
|
||||
f:SetScript("OnMouseDown", function() note(f, "down:"..arg1) end)
|
||||
f:SetScript("OnMouseUp", function() note(f, "up:"..arg1) end)
|
||||
f:SetScript("OnEnter", function() note(f, "enter") end)
|
||||
f:SetScript("OnLeave", function() note(f, "leave") end)
|
||||
return f
|
||||
end
|
||||
mk("A", 100)
|
||||
mk("B", 400)
|
||||
"#
|
||||
))
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
fn log_of(s: &mut UiScript) -> String {
|
||||
s.eval::<String>("return table.concat(log, ' ')").unwrap()
|
||||
}
|
||||
|
||||
// ── EnableMouse(false): the removal tail (`0x77711a` → `0x76af50` → `0x764ba0`) ─────────────────
|
||||
|
||||
/// A frame that turns click-through in its own `OnMouseDown` loses the capture with the index
|
||||
/// entry (`0x764ca5`–`0x764cad`), and the release goes to the capture alone (`0x766498`).
|
||||
#[test]
|
||||
fn a_frame_mouse_disabled_while_pressed_gets_no_release() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.run(
|
||||
r#"A:SetScript("OnMouseDown", function() table.insert(log, "A:down") A:EnableMouse(false) end)"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
assert!(
|
||||
s.model_ref().mouse_capture.is_none(),
|
||||
"the disable cleared the capture slot"
|
||||
);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(log_of(&mut s), "A:enter A:down A:leave");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The same removal runs for a disable outside the handler, and a pressed frame that is not the
|
||||
/// hover loses its press just as well.
|
||||
#[test]
|
||||
fn a_disable_after_the_press_drops_the_release_whatever_the_hover() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.mouse_move(500.0, 300.0); // the hover moved on to B; A keeps the capture
|
||||
s.run("A:EnableMouse(false)").unwrap();
|
||||
s.mouse_button(500.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(log_of(&mut s), "A:enter A:down:LeftButton A:leave B:enter");
|
||||
}
|
||||
|
||||
/// The armed drag (`+0x100`/`+0x104`) goes with the leave notify (`0x76b6f0`): a frame that
|
||||
/// disables its mouse in `OnMouseDown` gets no `OnDragStart` when the cursor moves.
|
||||
#[test]
|
||||
fn a_frame_mouse_disabled_while_pressed_gets_no_drag_start() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.run(
|
||||
r#"
|
||||
drags = 0
|
||||
A:RegisterForDrag("LeftButton")
|
||||
A:SetScript("OnDragStart", function() drags = drags + 1 end)
|
||||
A:SetScript("OnMouseDown", function() A:EnableMouse(false) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.mouse_move(260.0, 300.0);
|
||||
assert_eq!(s.eval::<i64>("return drags").unwrap(), 0);
|
||||
}
|
||||
|
||||
/// The arm goes with the capture even when the frame is not the hover: its threshold poll runs
|
||||
/// only through the capture and the index (`0x7661ca`, `0x7661cd`), and the frame is out of both,
|
||||
/// for a mouse disable and a hide alike.
|
||||
#[test]
|
||||
fn a_pressed_frame_out_of_the_index_never_starts_its_drag() {
|
||||
for removal in ["A:EnableMouse(false)", "A:Hide()"] {
|
||||
let mut s = two_frames("Frame");
|
||||
s.run(
|
||||
r#"
|
||||
drags = 0
|
||||
A:RegisterForDrag("LeftButton")
|
||||
A:SetScript("OnDragStart", function() drags = drags + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(299.0, 300.0);
|
||||
s.mouse_button(299.0, 300.0, "LeftButton", true);
|
||||
s.mouse_move(301.0, 300.0); // 2px: the hover left A, the arm is not yet past its threshold
|
||||
s.run(removal).unwrap();
|
||||
s.mouse_move(310.0, 300.0);
|
||||
assert_eq!(s.eval::<i64>("return drags").unwrap(), 0, "{removal}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The leave is at once: the hover clears and `OnLeave` runs inside the call (`0x764cd4`), the
|
||||
/// re-pick it arms (`0x764cbb`) then enters the frame beneath on the next tick.
|
||||
#[test]
|
||||
fn a_hovered_frame_mouse_disabled_gets_its_leave_at_once() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.run(
|
||||
r#"
|
||||
local under = CreateFrame("Frame", "Under")
|
||||
under:SetPoint("BOTTOMLEFT", 100, 200); under:SetWidth(200); under:SetHeight(200)
|
||||
under:EnableMouse(true)
|
||||
under:SetScript("OnEnter", function() table.insert(log, "Under:enter") end)
|
||||
A:SetFrameLevel(under:GetFrameLevel() + 5)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.run("A:EnableMouse(false) table.insert(log, 'focus:'..tostring(GetMouseFocus()))")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
log_of(&mut s),
|
||||
"A:enter A:leave focus:nil",
|
||||
"OnLeave ran inside the call and the hover is already clear"
|
||||
);
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
log_of(&mut s),
|
||||
"A:enter A:leave focus:nil Under:enter",
|
||||
"the armed re-pick enters the frame beneath with no mouse move"
|
||||
);
|
||||
}
|
||||
|
||||
/// The disable removes only the frame it names: another frame's hover, capture and press stay.
|
||||
#[test]
|
||||
fn disabling_another_frame_keeps_the_hover_and_the_press() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.run("B:EnableMouse(false)").unwrap();
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(log_of(&mut s), "A:enter A:down:LeftButton A:up:LeftButton");
|
||||
}
|
||||
|
||||
/// `EnableMouse(true)` inserts with flag -1 and arms no re-pick (`0x76af00(2, -1)`, gate
|
||||
/// `0x764b82`): a still cursor over the frame enters it only at the next move.
|
||||
#[test]
|
||||
fn enabling_the_mouse_re_picks_nothing() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.run("A:EnableMouse(false)").unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.run("A:EnableMouse(true)").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(log_of(&mut s), "", "no re-pick under a still cursor");
|
||||
s.mouse_move(201.0, 300.0);
|
||||
assert_eq!(log_of(&mut s), "A:enter");
|
||||
}
|
||||
|
||||
/// A pressed Button stays pushed when it is mouse-disabled: no release reaches it, so nothing
|
||||
/// un-presses it and nothing clicks.
|
||||
#[test]
|
||||
fn a_pressed_button_mouse_disabled_stays_pushed_and_never_clicks() {
|
||||
let mut s = two_frames("Button");
|
||||
s.run(
|
||||
r#"
|
||||
A:SetScript("OnClick", function() table.insert(log, "A:click") end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.run("A:EnableMouse(false)").unwrap();
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return A:GetButtonState()").unwrap(),
|
||||
"PUSHED"
|
||||
);
|
||||
assert_eq!(log_of(&mut s), "A:enter A:down:LeftButton A:leave");
|
||||
}
|
||||
|
||||
// ── A chorded press goes to the capture (`0x7662c0`) ───────────────────────────────────────────
|
||||
|
||||
/// Left held on A, right pressed over B: the press handler takes the capture first (`0x76637f`),
|
||||
/// so A gets both edges of the right button and B none (`0x7664a4`).
|
||||
#[test]
|
||||
fn a_chorded_press_fires_on_the_held_frame() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.mouse_move(500.0, 300.0);
|
||||
s.mouse_button(500.0, 300.0, "RightButton", true);
|
||||
s.mouse_button(500.0, 300.0, "RightButton", false);
|
||||
s.mouse_button(500.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(
|
||||
log_of(&mut s),
|
||||
"A:enter A:down:LeftButton A:leave B:enter A:down:RightButton A:up:RightButton A:up:LeftButton"
|
||||
);
|
||||
}
|
||||
|
||||
/// With nothing held the capture becomes the hovered frame (`0x7663e6`), so a lone press is
|
||||
/// unchanged and the capture clears with the last button.
|
||||
#[test]
|
||||
fn a_lone_press_still_goes_to_the_frame_under_the_cursor() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.mouse_button(500.0, 300.0, "RightButton", true);
|
||||
s.mouse_button(500.0, 300.0, "RightButton", false);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(
|
||||
log_of(&mut s),
|
||||
"B:down:RightButton B:up:RightButton A:down:LeftButton A:up:LeftButton"
|
||||
);
|
||||
}
|
||||
|
||||
/// A Button on the chorded press: the press art and the click need the cursor inside the capture
|
||||
/// (`0x77925c`, `0x779335`), and the frame under the cursor is neither pushed nor clicked.
|
||||
#[test]
|
||||
fn a_chorded_button_press_pushes_and_clicks_only_inside_the_held_button() {
|
||||
let mut s = two_frames("Button");
|
||||
s.run(
|
||||
r#"
|
||||
A:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
B:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
A:SetScript("OnClick", function() table.insert(log, "A:click:"..arg1) end)
|
||||
B:SetScript("OnClick", function() table.insert(log, "B:click:"..arg1) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.mouse_move(500.0, 300.0); // over B, outside A
|
||||
s.mouse_button(500.0, 300.0, "RightButton", true);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return B:GetButtonState()").unwrap(),
|
||||
"NORMAL",
|
||||
"the frame under the cursor takes no press art"
|
||||
);
|
||||
s.mouse_button(500.0, 300.0, "RightButton", false);
|
||||
assert!(
|
||||
!log_of(&mut s).contains("click"),
|
||||
"outside the held button no click fires, and none on B: {}",
|
||||
log_of(&mut s)
|
||||
);
|
||||
// Inside the held button the release clicks it.
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "RightButton", true);
|
||||
s.mouse_button(200.0, 300.0, "RightButton", false);
|
||||
assert!(
|
||||
log_of(&mut s).ends_with("A:up:RightButton A:click:RightButton"),
|
||||
"{}",
|
||||
log_of(&mut s)
|
||||
);
|
||||
assert!(!log_of(&mut s).contains("B:click"));
|
||||
}
|
||||
|
||||
/// The title-region test runs on the capture (`0x766397`): a chorded press over another frame's
|
||||
/// title region starts no move there.
|
||||
#[test]
|
||||
fn a_chorded_press_over_another_title_region_starts_no_move() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.run(r#"B:CreateTitleRegion():SetAllPoints(B)"#).unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.mouse_move(500.0, 300.0);
|
||||
s.mouse_button(500.0, 300.0, "RightButton", true);
|
||||
s.mouse_move(560.0, 300.0);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f64>("return B:GetLeft()").unwrap(), 400.0);
|
||||
assert!(log_of(&mut s).ends_with("A:down:RightButton"));
|
||||
}
|
||||
|
||||
/// A release lost to a focus change leaves its button held; the next press of that button must
|
||||
/// not be aimed at the stale capture.
|
||||
#[test]
|
||||
fn a_press_after_a_lost_release_goes_to_the_frame_under_the_cursor() {
|
||||
let mut s = two_frames("Frame");
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true); // the release never arrives
|
||||
s.mouse_move(500.0, 300.0);
|
||||
s.mouse_button(500.0, 300.0, "LeftButton", true);
|
||||
s.mouse_button(500.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(
|
||||
log_of(&mut s),
|
||||
"A:enter A:down:LeftButton A:leave B:enter B:down:LeftButton B:up:LeftButton"
|
||||
);
|
||||
}
|
||||
|
||||
// ── The Button click legs on the capture (`0x779210`, `0x7792d0`) ──────────────────────────────
|
||||
|
||||
/// The release click tests the pressed button's own rect (`0x779335`), not whether it is the top
|
||||
/// frame under the cursor: releasing over a frame that overlaps it still clicks.
|
||||
#[test]
|
||||
fn a_release_over_an_overlapping_frame_still_clicks_the_pressed_button() {
|
||||
let mut s = two_frames("Button");
|
||||
s.run(
|
||||
r#"
|
||||
A:SetScript("OnClick", function() table.insert(log, "A:click") end)
|
||||
local top = CreateFrame("Frame", "Top")
|
||||
top:SetPoint("BOTTOMLEFT", 200, 200); top:SetWidth(100); top:SetHeight(200)
|
||||
top:EnableMouse(true); top:SetFrameLevel(A:GetFrameLevel() + 5)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_move(120.0, 300.0);
|
||||
s.mouse_button(120.0, 300.0, "LeftButton", true);
|
||||
s.mouse_move(250.0, 300.0); // inside A, under Top
|
||||
assert_eq!(s.hit_test_name(250.0, 300.0).as_deref(), Some("Top"));
|
||||
s.mouse_button(250.0, 300.0, "LeftButton", false);
|
||||
assert!(
|
||||
log_of(&mut s).ends_with("A:up:LeftButton A:click"),
|
||||
"{}",
|
||||
log_of(&mut s)
|
||||
);
|
||||
}
|
||||
|
||||
/// The release slot clicks only a PUSHED button (`0x7792fc`): one a script set back to NORMAL
|
||||
/// between the edges does not.
|
||||
#[test]
|
||||
fn a_button_no_longer_pushed_at_the_release_does_not_click() {
|
||||
let mut s = two_frames("Button");
|
||||
s.run(r#"A:SetScript("OnClick", function() table.insert(log, "A:click") end)"#)
|
||||
.unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.run(r#"A:SetButtonState("NORMAL")"#).unwrap();
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert!(!log_of(&mut s).contains("click"), "{}", log_of(&mut s));
|
||||
}
|
||||
|
||||
/// The press slot pushes the button last: `OnMouseDown` and a Down `OnClick` see it unpushed
|
||||
/// (`0x76bf70`, `0x779285`, then `0x7792b1`), and a state set in `OnMouseDown` is overwritten.
|
||||
#[test]
|
||||
fn the_press_art_comes_after_on_mouse_down_and_the_down_click() {
|
||||
let mut s = two_frames("Button");
|
||||
s.run(
|
||||
r#"
|
||||
A:RegisterForClicks("LeftButtonDown", "LeftButtonUp")
|
||||
A:SetScript("OnMouseDown", function() table.insert(log, "down:"..A:GetButtonState()) A:SetButtonState("NORMAL") end)
|
||||
A:SetScript("OnClick", function() table.insert(log, "click:"..A:GetButtonState()) end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(200.0, 300.0);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
assert_eq!(log_of(&mut s), "A:enter down:NORMAL click:NORMAL");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return A:GetButtonState()").unwrap(),
|
||||
"PUSHED"
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod addon_index_space;
|
||||
mod anchors;
|
||||
mod backdrop;
|
||||
mod button;
|
||||
mod capture;
|
||||
mod channel;
|
||||
mod common;
|
||||
mod cooldown;
|
||||
|
||||
@@ -93,8 +93,7 @@ pub(super) fn base_hide(lua: &Lua, h: FrameHandle) {
|
||||
if !model.arena.hide_step(h) {
|
||||
return;
|
||||
}
|
||||
drop_capture(&mut model, h);
|
||||
leave_if_hovered(&mut model, h)
|
||||
leave_mouse_index(&mut model, h)
|
||||
};
|
||||
if let Some(id) = left {
|
||||
let fired = event::fire_widget_handler(lua, id, "OnLeave", vec![Value::Boolean(true)]);
|
||||
@@ -132,6 +131,37 @@ fn takes_mouse(model: &Model, h: FrameHandle) -> bool {
|
||||
.is_some_and(|spans| !spans.is_empty())
|
||||
}
|
||||
|
||||
/// The Lua `EnableMouse` (`0x777070`). Enabling inserts with flag -1 and arms nothing
|
||||
/// (`0x77710d` → `0x76af00(2, -1)`, gate `0x764b82`). Disabling (`0x77711a` → `0x76af50(2)`) runs
|
||||
/// the removal tail for a frame that was in the mouse index, which is a visible one with the bit
|
||||
/// set (`0x76af67`, `0x76af6b`); the bit clears after it (`0x76af8e`), so the leave's `OnLeave`
|
||||
/// still sees the frame mouse-enabled.
|
||||
pub(super) fn set_mouse_enabled(lua: &Lua, h: FrameHandle, enabled: bool) {
|
||||
let left = {
|
||||
let mut model = model_mut(lua);
|
||||
let in_index = model.arena.is_mouse_enabled(h)
|
||||
&& model.arena.frame(h).is_some_and(|f| f.effective_visible);
|
||||
if enabled || !in_index {
|
||||
model.arena.set_mouse_enabled(h, enabled);
|
||||
return;
|
||||
}
|
||||
leave_mouse_index(&mut model, h)
|
||||
};
|
||||
if let Some(id) = left {
|
||||
let fired = event::fire_widget_handler(lua, id, "OnLeave", vec![Value::Boolean(true)]);
|
||||
record(lua, fired);
|
||||
}
|
||||
model_mut(lua).arena.set_mouse_enabled(h, false);
|
||||
}
|
||||
|
||||
/// The mouse kind's removal tail (`0x764ba0`, kind 2, flag 0), shared by the hide and a mouse
|
||||
/// disable: the capture and the press it took drop, then the hover ends. The id whose `OnLeave`
|
||||
/// the caller fires.
|
||||
fn leave_mouse_index(model: &mut Model, h: FrameHandle) -> Option<u32> {
|
||||
drop_capture(model, h);
|
||||
leave_if_hovered(model, h)
|
||||
}
|
||||
|
||||
/// The removal tail's capture clear (`0x764ca5`–`0x764cad`): a frame leaving the mouse index
|
||||
/// that holds the capture slot (`root+0x80`) empties it. `0x766420` delivers a release to the
|
||||
/// capture alone, so the press this frame took reaches no one: no `OnMouseUp`, no click, and no
|
||||
@@ -146,6 +176,11 @@ fn drop_capture(model: &mut Model, h: FrameHandle) {
|
||||
*pressed = None;
|
||||
}
|
||||
}
|
||||
// Its drag arm (`+0x100`) is polled only through the capture and the index
|
||||
// (`0x7661ca`, `0x7661cd`), and the frame is out of both: nothing starts or stops it.
|
||||
if model.drag.as_ref().is_some_and(|d| d.source == h) {
|
||||
model.drag = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The hide's de-registration (`0x764920`, at `0x76ad9d`, before the child walk) ends a hover on
|
||||
|
||||
Reference in New Issue
Block a user