A drag release fires OnDragStop and the drop, not OnMouseUp (#928)

A release that ended a started drag fired the frame's `OnMouseUp` before `OnDragStop`, so a frame that both drags and acts on mouse-up ran its mouse-up action at the end of every drag.

In the reference, the mouse-up first checks the frame's drag registration for the released button and its started drag: if both hold it fires `OnDragStop` and the hover frame's `OnReceiveDrag`, and returns before `OnMouseUp`, which only a release without a started drag reaches.

The release now follows that: a started drag fires `OnDragStop` and the drop in `OnMouseUp`'s place, and any other release fires `OnMouseUp` as before.

Tests cover a drag, a plain click, a button the frame is not registered to drag, and a resize grip.

Fixes #920

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 02:16:00 +07:00
committed by GitHub
co-authored by Claude Sonnet 5.5
parent eec1804d37
commit b0ad656aaa
2 changed files with 124 additions and 14 deletions
@@ -183,6 +183,101 @@ mod tests {
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A frame whose every mouse script appends to `log`, registered for a LeftButton drag.
fn logged_drag_frame() -> UiScript {
let mut s = drag_script();
s.run(
r#"
log = {}
local function note(name) return function() table.insert(log, name) end end
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
a:RegisterForDrag("LeftButton")
a:SetScript("OnMouseDown", note("down"))
a:SetScript("OnMouseUp", note("up"))
a:SetScript("OnDragStart", note("dragstart"))
a:SetScript("OnDragStop", note("dragstop"))
a:SetScript("OnReceiveDrag", note("receive"))
"#,
)
.unwrap();
s.resolve();
s
}
fn log(s: &UiScript) -> String {
s.eval::<String>("return table.concat(log, ' ')").unwrap()
}
/// `0x76c040` on a release for a drag-registered button whose drag has started clears the arm,
/// fires `OnDragStop` (`0x76c06e`) and the hover frame's drop (`0x76c081`) and returns 1
/// (`0x76c08f`), never reaching its `OnMouseUp` (`0x76c099`).
#[test]
fn a_drag_release_fires_dragstop_and_no_mouseup() {
let mut s = logged_drag_frame();
s.mouse_button(100.0, 300.0, "LeftButton", true);
s.mouse_move(130.0, 300.0); // 30 units: past the threshold
s.mouse_button(130.0, 300.0, "LeftButton", false);
assert_eq!(log(&s), "down dragstart dragstop receive");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A press that never crossed the threshold falls through to `OnMouseUp` (`0x76c067` je).
#[test]
fn a_plain_click_on_a_drag_frame_fires_mouseup() {
let mut s = logged_drag_frame();
s.mouse_button(100.0, 300.0, "LeftButton", true);
s.mouse_button(100.0, 300.0, "LeftButton", false);
assert_eq!(log(&s), "down up");
}
/// The gate `0x76c050` tests the released button against the frame's registered buttons: a
/// frame registered for LeftButton drag still gets `OnMouseUp` for a RightButton release.
#[test]
fn a_button_not_registered_for_drag_still_fires_mouseup() {
let mut s = logged_drag_frame();
s.mouse_button(100.0, 300.0, "RightButton", true);
s.mouse_move(130.0, 300.0);
s.mouse_button(130.0, 300.0, "RightButton", false);
assert_eq!(log(&s), "down up");
}
/// A stock resize grip starts sizing in `OnMouseDown` and is not drag-registered, so its
/// `OnMouseUp` (which stops the sizing) fires even after the cursor left it.
#[test]
fn a_resize_grip_still_gets_mouseup() {
let mut s = drag_script();
s.run(
r#"
ups = 0
local f = CreateFrame("Frame", "Sized", UIParent)
f:SetResizable(true)
f:SetPoint("BOTTOMLEFT", 100, 100); f:SetWidth(200); f:SetHeight(100)
local g = CreateFrame("Button", "Grip", f)
g:SetPoint("BOTTOMRIGHT", 0, 0); g:SetWidth(16); g:SetHeight(16)
g:EnableMouse(true)
g:SetScript("OnMouseDown", function() Sized:StartSizing("BOTTOMRIGHT") end)
g:SetScript("OnMouseUp", function() ups = ups + 1; Sized:StopMovingOrSizing() end)
"#,
)
.unwrap();
s.resolve();
s.mouse_button(295.0, 105.0, "LeftButton", true);
s.mouse_move(395.0, 55.0); // dragged 100 right, 50 down, off the grip
s.mouse_button(395.0, 55.0, "LeftButton", false);
s.resolve();
assert_eq!(s.eval::<i64>("return ups").unwrap(), 1);
assert_eq!(s.eval::<f32>("return Sized:GetWidth()").unwrap(), 300.0);
s.mouse_move(500.0, 300.0);
s.resolve();
assert_eq!(
s.eval::<f32>("return Sized:GetWidth()").unwrap(),
300.0,
"the sizing ended on the release"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn drag_not_started_leaves_the_ordinary_click_path_untouched() {
let mut s = drag_script();
+29 -14
View File
@@ -464,9 +464,34 @@ impl UiScript {
// the hover frame, so a press on A released over B fires `OnMouseUp` on A, and a press
// 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 { press_id } else { captured_id } {
if let Err(e) = event::fire_widget_handler(&self.lua, id, script, vec![btn.clone()]) {
//
// The frame's mouse-up `0x76c040` runs the drag leg first: a release of a button the frame
// is registered to drag (`0x76c050`), with the drag started (`0x76c055`), fires
// `OnDragStop` (`0x76c06e`) and the hover frame's `OnReceiveDrag` (`0x76c081`) and returns
// 1 (`0x76c08f`) before the `OnMouseUp` leg (`0x76c099`). So a drag release fires no
// `OnMouseUp`; a release of a button the frame does not drag, or of a drag that never
// started, still does.
if down {
if let Some(id) = press_id {
if let Err(e) =
event::fire_widget_handler(&self.lua, id, "OnMouseDown", vec![btn.clone()])
{
self.push_error(e);
}
}
} else if let Some(release) = drag_release.as_ref().filter(|r| r.started) {
self.fire_drag_stop(release.source);
if let Some(id) = hit_id {
if let Err(e) =
event::fire_widget_handler(&self.lua, id, "OnReceiveDrag", Vec::new())
{
self.push_error(e);
}
}
} else if let Some(id) = captured_id {
if let Err(e) =
event::fire_widget_handler(&self.lua, id, "OnMouseUp", vec![btn.clone()])
{
self.push_error(e);
}
}
@@ -523,16 +548,6 @@ impl UiScript {
if let Some(source) = abandoned {
self.fire_drag_stop(source);
}
if let Some(release) = drag_release.filter(|r| r.started) {
self.fire_drag_stop(release.source);
if let Some(id) = hit_id {
if let Err(e) =
event::fire_widget_handler(&self.lua, id, "OnReceiveDrag", Vec::new())
{
self.push_error(e);
}
}
}
if let Some(id) = click_id {
// Shared with `Click()`: gated on the Button being enabled; a CheckButton toggles
// before `OnClick` fires.
@@ -554,7 +569,7 @@ impl UiScript {
}
/// The Button release slot `0x7792d0` past its base (`0x76c040`: `OnMouseUp`, which the caller
/// has fired), on the frame that took the press: a PUSHED button (`0x7792fc`) with the cursor
/// has fired; a started drag returns 1 there and never reaches this), on the frame that took the press: a PUSHED button (`0x7792fc`) with the cursor
/// inside its own rect (`0x779335`), not "the topmost frame under the cursor", and registered
/// for this button's Up (`0x77934e`) fires `OnDoubleClick` (`0x77938d`) or `OnClick`
/// (`0x7793a9`); only then does the tail set NORMAL (`0x7793de`) if the button is unlocked and