A second button pressed mid-drag re-arms or keeps it, as in 1.12.1 (#930)

Any press during a drag abandoned it with `OnDragStop`, so touching the right button while dragging an item or a window with the left stopped the drag.

In the reference, each press on the capture runs the drag arm: a button in the frame's drag mask re-arms it, started back to 0, with no `OnDragStop`, and any other button leaves the drag running. The release of a button in the mask ends a started drag with `OnDragStop`, or disarms an unstarted one. `OnDragStart` names the lowest held button.

Presses now re-arm or leave the drag as the reference does, the release tests the released button against the mask, and `OnDragStart` names the lowest held button. A release the client lost still stops the drag.

Tests cover a right press on a left-only drag, a re-arm, the first release after it, an item drag and the start's button.

Fixes #924

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 02:33:54 +07:00
committed by GitHub
co-authored by Claude Sonnet 5.5
parent 56f7fa003a
commit a2825a185f
3 changed files with 268 additions and 32 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ mod pet;
pub(crate) use bar::place_action;
pub(crate) use drag::{
abandon_drag, arm_drag, maybe_start_drag, take_drag, DragGesture, DragRelease,
abandon_drag, maybe_start_drag, press_drag, take_drag, DragGesture, DragRelease,
};
/// Sentinel bag id for the player's equipped slots, whose `slot` is a 1-based inventory slot id
+258 -26
View File
@@ -7,6 +7,15 @@ use crate::widget::FrameHandle;
/// or more from the press (`0x81c468`, squared by `0x76b300`).
pub(crate) const DRAG_START_THRESHOLD: f32 = 4.0;
/// The event button bits in order, `1`..`0x10` (`0x76bf70`'s name table).
const HELD_ORDER: [&str; 5] = [
"LeftButton",
"MiddleButton",
"RightButton",
"Button4",
"Button5",
];
/// A drag armed at a press on a [`Model::drag_registered`] frame, `started` past the threshold.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct DragGesture {
@@ -16,29 +25,57 @@ pub(crate) struct DragGesture {
pub(crate) started: bool,
}
/// A gesture whose button matched the release; [`Model::drag`] is cleared either way.
/// A gesture whose frame's drag mask held the released button; [`Model::drag`] is cleared either way.
pub(crate) struct DragRelease {
/// `OnDragStart` fired; an unstarted release fires nothing and the click proceeds.
pub(crate) started: bool,
pub(crate) source: FrameHandle,
}
/// Arm or clear the gesture on a press, replacing any leftover one; the caller runs
/// [`abandon_drag`] first so a started gesture still gets its `OnDragStop`.
pub(crate) fn arm_drag(model: &mut Model, hit: Option<FrameHandle>, button: &str, pos: (f32, f32)) {
model.drag = hit
.filter(|&h| {
model
.drag_registered
.get(&h)
.is_some_and(|set| set.iter().any(|b| b.eq_ignore_ascii_case(button)))
})
.map(|h| DragGesture {
/// The press arm `0x76bf70`, run on the frame the press lands on (the capture, else the hover). A
/// button in the frame's drag mask (`0x76bf7d`) re-arms: armed, not started, this button, this
/// point (`0x76bf82`..`0x76bfab`), with no `OnDragStop` for a started drag it replaces. Any other
/// button skips the arm (`0x76bf80` je `0x76bfb1`) and leaves the armed or started drag alone.
///
/// The reference keeps this state per frame and benilla has the one [`Model::drag`], which holds
/// the same thing because a press always lands on the capture, the frame of any live gesture. A
/// gesture on another frame, or one whose button's release was lost (`release_lost`; the
/// reference cannot lose one), can no longer be moved or released there, so it is abandoned and
/// the returned source of a started one must get `OnDragStop`, or a frame it is moving follows the
/// cursor.
pub(crate) fn press_drag(
model: &mut Model,
target: Option<FrameHandle>,
button: &str,
pos: (f32, f32),
release_lost: bool,
) -> Option<FrameHandle> {
let elsewhere = model
.drag
.as_ref()
.is_some_and(|g| Some(g.source) != target);
let displaced = if release_lost || elsewhere {
abandon_drag(model)
} else {
None
};
if let Some(h) = target.filter(|h| is_drag_button(model, *h, button)) {
model.drag = Some(DragGesture {
button: button.to_string(),
source: h,
start: pos,
started: false,
});
}
displaced
}
/// Whether `button` is in the frame's `RegisterForDrag` mask (`[+0xfc]`).
fn is_drag_button(model: &Model, frame: FrameHandle, button: &str) -> bool {
model
.drag_registered
.get(&frame)
.is_some_and(|set| set.iter().any(|b| b.eq_ignore_ascii_case(button)))
}
/// Start the armed gesture once the cursor passes the threshold, returning the `OnDragStart`
@@ -54,7 +91,19 @@ pub(crate) fn maybe_start_drag(model: &mut Model, pos: (f32, f32)) -> Option<(u3
if dist <= DRAG_START_THRESHOLD {
return None;
}
(g.source, g.button.clone())
// `OnDragStart`'s argument is the lowest button of the move event's held mask
// (`0x76c1e0` over `[evt+0x18]`), not the armed one: a chord re-armed by the later button
// starts as the earlier. The held set is the buttons whose press took a frame.
let held = HELD_ORDER.iter().find(|name| {
model
.mouse_down_on
.keys()
.any(|k| k.eq_ignore_ascii_case(name))
});
(
g.source,
held.map_or_else(|| g.button.clone(), |name| (*name).to_string()),
)
};
model.drag.as_mut().expect("checked Some above").started = true;
// A dragged button un-presses at the threshold, not on leaving its rect: `CSimpleButton`'s
@@ -68,18 +117,22 @@ pub(crate) fn maybe_start_drag(model: &mut Model, pos: (f32, f32)) -> Option<(u3
Some((id, button))
}
/// Drop a gesture whose release will never come, because the pointer left the window or a second
/// button was pressed; the reference cannot lose one, as the OS captures the pointer. Returns a
/// started one's source, which must get `OnDragStop` or a frame it is moving follows the cursor.
/// Drop a gesture whose release will never come, because the pointer left the window; the
/// reference cannot lose one, as the OS captures the pointer. Returns a started one's source,
/// which must get `OnDragStop` or a frame it is moving follows the cursor.
pub(crate) fn abandon_drag(model: &mut Model) -> Option<FrameHandle> {
model.drag.take().filter(|g| g.started).map(|g| g.source)
}
/// Take the gesture whose button matches the release.
/// Take the gesture a release ends. `0x76c050` tests the released button against the frame's
/// drag mask, not the armed button `[+0x108]`, so any mask button's release clears the arm
/// (`0x76c05d`) and, with the drag started, is the drag release (`0x76c06e`).
pub(crate) fn take_drag(model: &mut Model, button: &str) -> Option<DragRelease> {
let g = model
.drag
.take_if(|g| g.button.eq_ignore_ascii_case(button))?;
let source = model.drag.as_ref()?.source;
if !is_drag_button(model, source, button) {
return None;
}
let g = model.drag.take()?;
Some(DragRelease {
started: g.started,
source: g.source,
@@ -693,18 +746,22 @@ mod tests {
assert_eq!(s.eval::<i64>("return stops").unwrap(), 1);
}
/// Each press runs the arm `0x76bf70` on the capture: a button outside the frame's drag mask
/// (`0x76bf7d` je `0x76bfb1`) skips the arm and touches neither `[+0x100]` nor `[+0x104]`, so
/// the started left drag runs on and only its own release (`0x76c040`) fires `OnDragStop`.
#[test]
fn a_press_that_replaces_an_in_flight_gesture_stops_it_first() {
fn a_press_of_a_button_the_frame_does_not_drag_leaves_the_drag_running() {
let mut s = drag_script();
s.run(
r#"
stops, starts = 0, 0
stops, starts, ups = 0, 0, 0
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 100, 100); a:SetWidth(200); a:SetHeight(100)
a:EnableMouse(true); a:SetMovable(true)
a:RegisterForDrag("LeftButton")
a:SetScript("OnDragStart", function() starts = starts + 1; this:StartMoving() end)
a:SetScript("OnDragStop", function() stops = stops + 1; this:StopMovingOrSizing() end)
a:SetScript("OnMouseUp", function() ups = ups + 1 end)
"#,
)
.unwrap();
@@ -717,13 +774,188 @@ mod tests {
s.mouse_button(200.0, 200.0, "RightButton", true);
assert_eq!(
s.eval::<i64>("return stops").unwrap(),
0,
"the right press does not touch the left drag"
);
s.mouse_move(250.0, 250.0);
s.mouse_move(300.0, 300.0);
s.resolve();
assert_eq!(
s.eval::<f32>("return A:GetLeft()").unwrap(),
200.0,
"the frame still follows the cursor"
);
assert_eq!(
s.eval::<i64>("return starts").unwrap(),
1,
"the left drag the right press displaced was stopped, not forgotten"
"no second start"
);
// Replacing an unstarted press fires nothing, as releasing it would not.
s.mouse_button(150.0, 150.0, "LeftButton", true);
s.mouse_button(150.0, 150.0, "MiddleButton", true);
// The right release is no drag release (`0x76c050` je `0x76c099`): its `OnMouseUp` fires.
s.mouse_button(300.0, 300.0, "RightButton", false);
assert_eq!(s.eval::<i64>("return stops").unwrap(), 0);
assert_eq!(s.eval::<i64>("return ups").unwrap(), 1);
s.mouse_button(300.0, 300.0, "LeftButton", false);
assert_eq!(
s.eval::<i64>("return stops").unwrap(),
1,
"the left release ends the drag"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A frame registered for both buttons.
fn logged_two_button_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", "RightButton")
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
}
/// A press of a registered button re-arms (`0x76bf82`..`0x76bfab`: armed 1, started 0, the new
/// button and start point) and fires no `OnDragStop`. `0x76b320` then starts again from the new
/// point (`0x76b34c`), a second `OnDragStart`; the release of a mask button ends a started
/// drag whichever button armed it (`0x76c050`..`0x76c06e` read the released button only).
#[test]
fn a_press_of_a_registered_button_re_arms_without_a_stop() {
let mut s = logged_two_button_drag_frame();
s.mouse_button(100.0, 300.0, "LeftButton", true);
s.mouse_move(130.0, 300.0);
assert_eq!(log(&s), "down dragstart");
s.mouse_button(130.0, 300.0, "RightButton", true);
assert_eq!(log(&s), "down dragstart down", "no OnDragStop");
s.mouse_move(132.0, 300.0); // 2px from the new arm point: under the threshold
assert_eq!(log(&s), "down dragstart down");
s.mouse_move(160.0, 300.0); // past it: started again
assert_eq!(log(&s), "down dragstart down dragstart");
s.mouse_button(160.0, 300.0, "RightButton", false);
assert_eq!(
log(&s),
"down dragstart down dragstart dragstop receive",
"the release ends the started drag, no OnMouseUp"
);
// The drag is over: the left release finds it unarmed and is a plain release.
s.mouse_button(160.0, 300.0, "LeftButton", false);
assert_eq!(log(&s), "down dragstart down dragstart dragstop receive up");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `0x76c050` tests the released button against the mask, not `[+0x108]`: the first button's
/// release after the re-arm clears the arm (`0x76c05d`), and with the drag not restarted
/// (`[+0x104]` 0, `0x76c067` je `0x76c099`) is a plain `OnMouseUp`. Nothing starts afterwards.
#[test]
fn the_first_buttons_release_after_a_re_arm_disarms_it() {
let mut s = logged_two_button_drag_frame();
s.mouse_button(100.0, 300.0, "LeftButton", true);
s.mouse_move(130.0, 300.0);
s.mouse_button(130.0, 300.0, "RightButton", true);
s.mouse_button(130.0, 300.0, "LeftButton", false);
assert_eq!(log(&s), "down dragstart down up", "no OnDragStop");
s.mouse_move(200.0, 300.0); // the arm is cleared: the right button drags nothing
assert_eq!(log(&s), "down dragstart down up");
s.mouse_button(200.0, 300.0, "RightButton", false);
assert_eq!(log(&s), "down dragstart down up up");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `OnDragStart`'s button is not the armed one: `0x76c1e0` names the lowest bit of the move
/// event's held mask (`[evt+0x18]`, the producer `0x423fe0` stores `ds:0x883f40` there), so
/// with Left still held a drag re-armed by Right starts as `LeftButton`.
#[test]
fn on_drag_start_names_the_lowest_held_button() {
let mut s = drag_script();
s.run(
r#"
names = {}
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
a:RegisterForDrag("LeftButton", "RightButton")
a:SetScript("OnDragStart", function() table.insert(names, arg1) end)
"#,
)
.unwrap();
s.resolve();
s.mouse_button(100.0, 300.0, "LeftButton", true);
s.mouse_move(130.0, 300.0);
s.mouse_button(130.0, 300.0, "RightButton", true); // re-arms for Right, Left still held
s.mouse_move(160.0, 300.0);
s.mouse_button(160.0, 300.0, "RightButton", false);
s.mouse_button(160.0, 300.0, "LeftButton", false);
s.mouse_button(160.0, 300.0, "RightButton", true); // Right alone
s.mouse_move(200.0, 300.0);
assert_eq!(
s.eval::<String>("return table.concat(names, ' ')").unwrap(),
"LeftButton LeftButton RightButton"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The cursor payload is no part of the arm: a right click during an item drag leaves the
/// carried item and the drag, and the left release still drops on the frame under the cursor.
#[test]
fn an_item_drag_survives_a_right_click() {
let mut s = drag_script();
s.run(
r#"
stops, receives = 0, 0
local a = CreateFrame("Frame", "A")
a:SetPoint("BOTTOMLEFT", 0, 0); a:SetWidth(400); a:SetHeight(600); a:EnableMouse(true)
a:RegisterForDrag("LeftButton")
a:SetScript("OnDragStop", function() stops = stops + 1 end)
local b = CreateFrame("Frame", "B")
b:SetPoint("BOTTOMLEFT", 400, 0); b:SetWidth(400); b:SetHeight(600); b:EnableMouse(true)
b:SetScript("OnReceiveDrag", function() receives = receives + 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);
s.mouse_move(110.0, 300.0); // the drag starts
s.mouse_button(110.0, 300.0, "RightButton", true);
s.mouse_button(110.0, 300.0, "RightButton", false);
assert_eq!(s.eval::<i64>("return stops").unwrap(), 0);
assert!(s.cursor_item().is_some(), "the item is still carried");
s.mouse_move(600.0, 300.0);
s.mouse_button(600.0, 300.0, "LeftButton", false);
assert_eq!(s.eval::<i64>("return stops").unwrap(), 1);
assert_eq!(
s.eval::<i64>("return receives").unwrap(),
1,
"the drop reaches the frame under the cursor"
);
assert!(s.cursor_item().is_some(), "a drag release never drops");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
}
+9 -5
View File
@@ -269,14 +269,18 @@ impl UiScript {
// 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 mut release_lost = false;
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 let Some(entry) = model.mouse_down_on.remove(button) {
release_lost = true;
if let Some(stale) = entry {
button::edge(&mut model, stale, ButtonState::on_mouse_up);
}
}
if model.mouse_down_on.is_empty() {
model.mouse_capture = None;
@@ -388,10 +392,10 @@ impl UiScript {
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`
// The drag arm `0x76bf70`; a started gesture it displaces gets its `OnDragStop`
// below, outside this borrow.
let abandoned = cursor::abandon_drag(&mut model);
cursor::arm_drag(&mut model, press_target, button, (x, y));
let abandoned =
cursor::press_drag(&mut model, press_target, button, (x, y), release_lost);
// 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" {