A hide drops the press and capture; only a mouse frame's show re-picks (#888)
A frame hidden while a button was pressed on it still got OnMouseUp on the release, and its stale capture aimed the next chorded press at it. And showing any frame, even one that takes no mouse input, scheduled a hover re-pick, so a still cursor could fire OnEnter or OnLeave a tick early. In the reference, the hide's input-index removal clears the capture slot when it holds the hidden frame, and the release goes only to the capture, so it reaches no one. The hover re-pick is armed only when a show inserts a frame into the mouse index. The hide now drops the capture and the pressed frame; the release then runs nothing, no world click either. A show arms the re-pick only for a frame that takes the mouse. Tests cover hides while pressed and shows with and without mouse input. Fixes #844 Fixes #845 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
c0630f3d9d
commit
481cc8870e
@@ -473,6 +473,45 @@ mod tests {
|
||||
assert!(s.cursor_item().is_some());
|
||||
}
|
||||
|
||||
/// A press whose frame hid (the removal cleared the capture, `0x764cad`) reaches no one on the
|
||||
/// release: `0x766420` dispatches to the capture alone, and the world hook `0x492b50` runs
|
||||
/// only in freelook, so the world click `0x495300` never runs.
|
||||
#[test]
|
||||
fn a_press_whose_frame_hid_is_not_a_world_drop() {
|
||||
let mut s = drag_script();
|
||||
s.run(
|
||||
r#"
|
||||
WorldFrame = CreateFrame("WorldFrame", "WorldFrame") WorldFrame:SetAllPoints()
|
||||
heard = 0
|
||||
local a = CreateFrame("Frame", "A")
|
||||
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
|
||||
a:SetScript("OnMouseDown", function() A:Hide() end)
|
||||
local f = CreateFrame("Frame", "Listener")
|
||||
f:RegisterEvent("DELETE_ITEM_CONFIRM")
|
||||
f:SetScript("OnEvent", function() heard = heard + 1 end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.set_cursor_for_test(CursorPayload::Item(CursorItem {
|
||||
bar_placeable: true,
|
||||
bag: 0,
|
||||
slot: 1,
|
||||
item_id: 117,
|
||||
texture: None,
|
||||
link: None,
|
||||
count: None,
|
||||
quality: None,
|
||||
equip_slots: Vec::new(),
|
||||
}));
|
||||
|
||||
s.mouse_button(100.0, 300.0, "LeftButton", true); // press on A, which hides itself
|
||||
s.mouse_button(600.0, 300.0, "LeftButton", false); // release over the world
|
||||
s.tick(0.01);
|
||||
assert_eq!(s.eval::<i64>("return heard").unwrap(), 0, "no popup");
|
||||
assert!(s.cursor_item().is_some());
|
||||
}
|
||||
|
||||
/// A non-item payload's world click clears it silently (`0x495300`); a drag release keeps it.
|
||||
#[test]
|
||||
fn world_click_with_a_spell_payload_clears_silently() {
|
||||
|
||||
@@ -1008,7 +1008,8 @@ impl UiScript {
|
||||
self.fire_drag_stop(source);
|
||||
}
|
||||
let mut model = self.model_mut();
|
||||
let held: Vec<crate::widget::FrameHandle> = model.mouse_down_on.values().copied().collect();
|
||||
let held: Vec<crate::widget::FrameHandle> =
|
||||
model.mouse_down_on.values().copied().flatten().collect();
|
||||
model.mouse_down_on.clear();
|
||||
// Every held button gets the release edge never fed (`0x7793de`), else it stays pushed.
|
||||
for h in held {
|
||||
|
||||
@@ -204,12 +204,15 @@ pub(crate) struct Model {
|
||||
|
||||
/// The mouse-enabled frame under the cursor, for the next move's `OnLeave`/`OnEnter`.
|
||||
pub(crate) mouseover: Option<FrameHandle>,
|
||||
/// A frame under a still cursor hid or showed, so the tick re-runs the hover walk at the saved
|
||||
/// position and a newly exposed frame gets `OnEnter` without a mouse move: the reference's
|
||||
/// `[root+0x1100]` flag (set at `0x764cbb`/`0x764b8d`, pumped at `0x7657a1` into `0x7660d0`).
|
||||
/// The hovered frame hid, or a mouse-enabled frame showed, so the tick re-runs the hover walk
|
||||
/// at the saved position and a newly exposed frame gets `OnEnter` without a mouse move: the
|
||||
/// reference's `[root+0x1100]` flag (set at `0x764cbb`/`0x764b8d`, pumped at `0x7657a1` into
|
||||
/// `0x7660d0`).
|
||||
pub(crate) hover_repick: bool,
|
||||
/// Per button, the frame its press landed on, for `OnClick`'s same-frame release test.
|
||||
pub(crate) mouse_down_on: HashMap<String, FrameHandle>,
|
||||
/// 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.
|
||||
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;
|
||||
/// cleared at `0x7664bb` once no button is down. A title-region press captures nothing.
|
||||
|
||||
@@ -313,7 +313,9 @@ impl UiScript {
|
||||
model
|
||||
.mouse_down_on
|
||||
.get(button)
|
||||
.and_then(|h| model.frame_to_id.get(h).copied())
|
||||
.copied()
|
||||
.flatten()
|
||||
.and_then(|h| model.frame_to_id.get(&h).copied())
|
||||
};
|
||||
// The `GetTime()` clock for the double-click detector, read before the borrow.
|
||||
let now = self.now();
|
||||
@@ -347,9 +349,10 @@ impl UiScript {
|
||||
let hit_handle = hit_id.and_then(|id| model.id_to_frame.get(&id).copied());
|
||||
if down {
|
||||
let displaced = match hit_handle {
|
||||
Some(h) => model.mouse_down_on.insert(button.to_string(), h),
|
||||
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))
|
||||
@@ -389,7 +392,11 @@ impl UiScript {
|
||||
.and(hit_id);
|
||||
(click, None, false, jump, None, abandoned, color_jump)
|
||||
} else {
|
||||
let pressed = model.mouse_down_on.remove(button);
|
||||
let entry = model.mouse_down_on.remove(button);
|
||||
let pressed = entry.flatten();
|
||||
// The pressed frame hid (`None` entry): the capture is gone, so nothing of this
|
||||
// release runs, the world click included (`over_world(None)` would take it).
|
||||
let capture_lost = entry == Some(None);
|
||||
// `0x7664bb` clears the capture only when no button is still held, so a chorded
|
||||
// release keeps it.
|
||||
if model.mouse_down_on.is_empty() {
|
||||
@@ -418,6 +425,7 @@ impl UiScript {
|
||||
&& !started
|
||||
&& over_world(&model, hit_handle)
|
||||
&& over_world(&model, pressed)
|
||||
&& !capture_lost
|
||||
&& cursor::world_drop_click(&mut model);
|
||||
let wants = format!("{button}Up");
|
||||
let click = if started {
|
||||
|
||||
@@ -793,6 +793,59 @@ fn a_press_and_release_on_one_frame_is_unchanged() {
|
||||
assert_eq!(log_of(&mut s), "A:down A:up");
|
||||
}
|
||||
|
||||
/// Hiding a frame runs the input-index removal (`0x764ba0`), whose mouse tail clears the capture
|
||||
/// when the frame holds it (`0x764ca5`, `0x764cad`), and `0x766420` delivers `OnMouseUp` to the
|
||||
/// capture alone: a frame hidden while pressed gets no `OnMouseUp` on the release, hidden still
|
||||
/// or shown again.
|
||||
#[test]
|
||||
fn a_frame_hidden_while_pressed_gets_no_release() {
|
||||
let mut s = two_frames();
|
||||
s.run(r#"A:SetScript("OnMouseDown", function() table.insert(log, "A:down") A:Hide() end)"#)
|
||||
.unwrap();
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
assert!(
|
||||
s.model_ref().mouse_capture.is_none(),
|
||||
"the hide cleared the capture slot"
|
||||
);
|
||||
s.run("A:Show()").unwrap(); // shown again before the release: the capture stays lost
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(log_of(&mut s), "A:down");
|
||||
}
|
||||
|
||||
/// The removal runs for a frame that hid because an ancestor did (`0x76ad50` walks its children
|
||||
/// through slot `+0x84`, each gated on its own effective visibility).
|
||||
#[test]
|
||||
fn a_frame_whose_ancestor_hid_while_pressed_gets_no_release() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
log = {}
|
||||
local p = CreateFrame("Frame", "P")
|
||||
p:SetPoint("BOTTOMLEFT", 0, 0); p:SetWidth(800); p:SetHeight(600)
|
||||
local c = CreateFrame("Frame", "C", p)
|
||||
c:SetPoint("BOTTOMLEFT", 100, 200); c:SetWidth(200); c:SetHeight(200); c:EnableMouse(true)
|
||||
c:SetScript("OnMouseDown", function() table.insert(log, "C:down") P:Hide() end)
|
||||
c:SetScript("OnMouseUp", function() table.insert(log, "C:up") end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(log_of(&mut s), "C:down");
|
||||
}
|
||||
|
||||
/// Only the held frame loses the press: hiding another frame leaves the release on its target.
|
||||
#[test]
|
||||
fn hiding_another_frame_keeps_the_press() {
|
||||
let mut s = two_frames();
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", true);
|
||||
s.run("B:Hide()").unwrap();
|
||||
s.mouse_button(200.0, 300.0, "LeftButton", false);
|
||||
assert_eq!(log_of(&mut s), "A:down A:up");
|
||||
}
|
||||
|
||||
/// `Model::mouse_down_on` is keyed by button name, so the capture is per button.
|
||||
#[test]
|
||||
fn the_capture_is_per_button() {
|
||||
@@ -865,3 +918,109 @@ fn a_disabled_button_takes_the_hover_but_fires_no_enter_or_leave() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
// ── The hover re-pick on a show (`0x764b8d`) ──
|
||||
|
||||
/// A mouse-enabled `Mover` hidden-cursor setup: the cursor rests at (150, 150) over nothing, then
|
||||
/// `Mover` is moved under it with no mouse move. Whether the next tick enters it is whether a
|
||||
/// re-pick was armed.
|
||||
fn mover_under_a_still_cursor() -> crate::script::UiScript {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
entered = {}
|
||||
Mover = CreateFrame("Frame", "Mover")
|
||||
Mover:SetPoint("BOTTOMLEFT", 500, 400); Mover:SetWidth(100); Mover:SetHeight(100)
|
||||
Mover:EnableMouse(true)
|
||||
Mover:SetScript("OnEnter", function() table.insert(entered, "mover") end)
|
||||
|
||||
Plain = CreateFrame("Frame", "Plain")
|
||||
Plain:SetPoint("BOTTOMLEFT", 0, 0); Plain:SetWidth(10); Plain:SetHeight(10)
|
||||
Plain:Hide()
|
||||
Plain:SetScript("OnShow", function() table.insert(entered, "plain-shown") end)
|
||||
|
||||
PlainParent = CreateFrame("Frame", "PlainParent")
|
||||
PlainParent:Hide()
|
||||
PlainKid = CreateFrame("Frame", "PlainKid", PlainParent)
|
||||
PlainKid:SetPoint("BOTTOMLEFT", 0, 0); PlainKid:SetWidth(10); PlainKid:SetHeight(10)
|
||||
|
||||
Shown = CreateFrame("Frame", "Shown")
|
||||
Shown:SetPoint("BOTTOMLEFT", 0, 0); Shown:SetWidth(10); Shown:SetHeight(10)
|
||||
|
||||
Taker = CreateFrame("Frame", "Taker")
|
||||
Taker:SetPoint("BOTTOMLEFT", 0, 0); Taker:SetWidth(10); Taker:SetHeight(10)
|
||||
Taker:EnableMouse(true)
|
||||
Taker:Hide()
|
||||
|
||||
MouseParent = CreateFrame("Frame", "MouseParent")
|
||||
MouseParent:Hide()
|
||||
MouseKid = CreateFrame("Frame", "MouseKid", MouseParent)
|
||||
MouseKid:SetPoint("BOTTOMLEFT", 0, 0); MouseKid:SetWidth(10); MouseKid:SetHeight(10)
|
||||
MouseKid:EnableMouse(true)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s.mouse_move(150.0, 150.0); // over nothing
|
||||
s.tick(0.016); // drains whatever the setup armed
|
||||
s.run(
|
||||
r#"
|
||||
Mover:ClearAllPoints()
|
||||
Mover:SetPoint("BOTTOMLEFT", 100, 100)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
fn entered(s: &mut crate::script::UiScript) -> String {
|
||||
s.eval::<String>("return table.concat(entered, ',')")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// A show arms the re-pick only through the input-index insert, which a frame reaches only with
|
||||
/// its mouse bit set (`0x76afa0` tests `[frame+0xcc]` per kind), so showing frames that take no
|
||||
/// mouse input leaves the hover alone until the mouse moves.
|
||||
#[test]
|
||||
fn showing_a_frame_that_takes_no_mouse_does_not_re_pick() {
|
||||
let mut s = mover_under_a_still_cursor();
|
||||
s.run("Plain:Show() PlainParent:Show()").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
entered(&mut s),
|
||||
"plain-shown",
|
||||
"no mouse-enabled frame showed, so no re-pick: Mover is entered on the next move"
|
||||
);
|
||||
s.mouse_move(151.0, 150.0);
|
||||
assert_eq!(entered(&mut s), "plain-shown,mover");
|
||||
}
|
||||
|
||||
/// `EnableMouse(true)` on a shown frame inserts with flag -1 (`0x76af00`), which does not arm it.
|
||||
#[test]
|
||||
fn enabling_the_mouse_on_a_shown_frame_does_not_re_pick() {
|
||||
let mut s = mover_under_a_still_cursor();
|
||||
s.run("Shown:EnableMouse(true)").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(entered(&mut s), "");
|
||||
}
|
||||
|
||||
/// A shown frame with its mouse bit set enters the index with flag 0 and arms the re-pick.
|
||||
#[test]
|
||||
fn showing_a_mouse_enabled_frame_re_picks() {
|
||||
let mut s = mover_under_a_still_cursor();
|
||||
s.run("Taker:Show()").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(entered(&mut s), "mover");
|
||||
}
|
||||
|
||||
/// A shown parent's children go through their own transitions (`0x76aed5`, slot `+0x88`), so a
|
||||
/// mouse-enabled child arms the re-pick whatever its parent takes.
|
||||
#[test]
|
||||
fn showing_a_parent_re_picks_through_its_mouse_enabled_child() {
|
||||
let mut s = mover_under_a_still_cursor();
|
||||
s.run("MouseParent:Show()").unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(entered(&mut s), "mover");
|
||||
}
|
||||
|
||||
@@ -31,8 +31,14 @@ pub(super) fn show(lua: &Lua, h: FrameHandle) {
|
||||
if !model.arena.show_step(h) {
|
||||
return;
|
||||
}
|
||||
// Entering the mouse index schedules a hover re-pick (`0x764b8d`).
|
||||
model.hover_repick = true;
|
||||
// Only a frame entering the mouse index schedules a hover re-pick: the show's insert
|
||||
// (`0x764780` → `0x76afa0`, flag 0) reaches `0x764aa0` for a kind whose bit is set, and
|
||||
// its tail arms `[root+0x1100]` for the mouse kind alone (`0x764b84`, `0x764b8d`). A
|
||||
// frame that takes no mouse arms nothing; a parent's mouse-enabled children arm it
|
||||
// through their own transitions below.
|
||||
if takes_mouse(&model, h) {
|
||||
model.hover_repick = true;
|
||||
}
|
||||
}
|
||||
walk_children(lua, h, show);
|
||||
super::object::toplevel::raise_on_show(&mut model_mut(lua), h);
|
||||
@@ -47,6 +53,7 @@ pub(super) fn hide(lua: &Lua, h: FrameHandle) {
|
||||
if !model.arena.hide_step(h) {
|
||||
return;
|
||||
}
|
||||
drop_capture(&mut model, h);
|
||||
leave_if_hovered(&mut model, h)
|
||||
};
|
||||
if let Some(id) = left {
|
||||
@@ -75,6 +82,32 @@ fn walk_children(lua: &Lua, parent: FrameHandle, step: fn(&Lua, FrameHandle)) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the frame is in the mouse input index when visible: its mouse bit (`[+0xcc]` bit 2),
|
||||
/// or a hyperlink span, which the reference gives a mouse-enabled child of its own (`0x7a3240`).
|
||||
fn takes_mouse(model: &Model, h: FrameHandle) -> bool {
|
||||
model.arena.is_mouse_enabled(h)
|
||||
|| model
|
||||
.link_spans
|
||||
.get(&h)
|
||||
.is_some_and(|spans| !spans.is_empty())
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// world click, since the world hook `0x492b50` runs only in freelook. The button stays held
|
||||
/// until its release, which then finds nothing.
|
||||
fn drop_capture(model: &mut Model, h: FrameHandle) {
|
||||
if model.mouse_capture == Some(h) {
|
||||
model.mouse_capture = None;
|
||||
}
|
||||
for pressed in model.mouse_down_on.values_mut() {
|
||||
if *pressed == Some(h) {
|
||||
*pressed = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The hide's de-registration (`0x764920`, at `0x76ad9d`, before the child walk) ends a hover on
|
||||
/// this frame: the removal tail (`0x764ba0`) clears the hover, schedules a re-pick (`0x764cbb`)
|
||||
/// and calls the leave notify with 1 (`0x764cce`), whose base `0x76b6f0` clears the drag-arm
|
||||
|
||||
Reference in New Issue
Block a user