A drag starts at 1% of the screen diagonal, as in 1.12.1 (#931)

A drag started past a fixed 4 pixels from the press. In 1.12.1 the threshold is 0.01 layout units, where the screen diagonal is 1: `0x76b300` stores `0.01f × 0.01f` (`0x81c468`) as an f32, and `0x76b320` starts the drag when the squared distance from the press point is not below it, comparing at x87 double precision. The converters `0x425610`/`0x425710` scale the cursor into those units (`0x41ad80`).

The drag test now divides both points by the layout unit and compares the squared distance in f64 against the f32 threshold. At 1024×768 a drag needs 12.8 px, at 1920×1080 about 22 px; the UI scale does not change it.

Tests: just under and over 1% of the diagonal, 12 and 13 px at 1024×768, the exact f32 boundary, two screen sizes and three UI scales.

Fixes #157

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 02:51:42 +07:00
committed by GitHub
co-authored by Claude Sonnet 5.5
parent a2825a185f
commit af281a05f7
10 changed files with 173 additions and 33 deletions
@@ -24,9 +24,9 @@ use super::render::{Painted, RenderBaseline};
/// it, in painter order, are dropped, so a broken ninth widget reads clean.
pub const MAX_USE_TARGETS: usize = 8;
/// How far the drag travels: past `cursor::DRAG_START_THRESHOLD` (4 px, strict `>`) so
/// `OnDragStart` fires, and inside a 16 px item slot so the release lands on the same frame.
const DRAG_PX: f32 = 5.0;
/// How far the drag travels: past the drag-start distance (0.01 of the screen diagonal, 12.8 root
/// units at the 1024×768 these runs use) so `OnDragStart` fires.
const DRAG_PX: f32 = 14.0;
/// Where the cursor is parked at the end, off-screen so the last `OnLeave` always fires.
const PARKED: (f32, f32) = (-10_000.0, -10_000.0);
@@ -90,7 +90,7 @@ fn the_use_column_can_fail() {
"#,
),
);
// `OnDragStart` fires only past the 4 px threshold, so the drag must actually move.
// `OnDragStart` fires only past the drag-start distance, so the drag must actually move.
fx.addon(
"RaisesOnDrag",
&painted_button(
@@ -826,7 +826,7 @@ fn drag_drop_onto_another_button_hops_the_displaced_action() {
s.resolve();
s.mouse_button(26.0, 22.0, "LeftButton", true);
s.mouse_move(40.0, 22.0); // past the 4px drag-start threshold
s.mouse_move(40.0, 22.0); // past the drag-start distance
let consumed = s.mouse_button(68.0, 22.0, "LeftButton", false);
assert!(consumed, "OnReceiveDrag consumed the release");
@@ -576,7 +576,7 @@ fn dragging_a_bagnon_slot_picks_the_item_up() {
let (dx, dy) = centre_of(&mut s, &empty);
s.mouse_move(sx, sy);
s.mouse_button(sx, sy, "LeftButton", true);
s.mouse_move(sx + 12.0, sy + 12.0); // past DRAG_START_THRESHOLD ⇒ OnDragStart
s.mouse_move(sx + 12.0, sy + 12.0); // past the drag-start distance ⇒ OnDragStart
s.mouse_move(dx, dy);
s.mouse_button(dx, dy, "LeftButton", false);
@@ -91,11 +91,14 @@ fn reveal_window(s: &mut UiScript, window: &str) {
}
}
/// A move past the drag-start distance (0.01 of the 1600×900 diagonal, 15.7 root units).
const DRAG_STARTS_AT: f32 = 20.0;
fn drag_tab(s: &mut UiScript, dx: f32, dy: f32) {
reveal(s);
let (tx, ty) = centre(s, "ChatFrame1Tab");
s.mouse_button(tx, ty, "LeftButton", true);
s.mouse_move(tx + 5.0, ty); // past the 4px threshold: OnDragStart -> StartMoving
s.mouse_move(tx + DRAG_STARTS_AT, ty); // past the drag-start distance: OnDragStart -> StartMoving
s.mouse_move(tx + dx, ty + dy);
s.mouse_button(tx + dx, ty + dy, "LeftButton", false);
s.resolve();
@@ -284,18 +287,18 @@ fn the_tab_drag_moves_an_unlocked_window_and_a_locked_one_stays_put() {
reveal(&mut s);
let (tx, ty) = centre(&mut s, "ChatFrame1Tab");
s.mouse_button(tx, ty, "LeftButton", true);
s.mouse_move(tx + 5.0, ty);
s.mouse_move(tx + DRAG_STARTS_AT, ty);
s.mouse_move(tx + 80.0, ty);
assert_eq!(
left(&mut s),
start + 75.0,
start + 80.0 - DRAG_STARTS_AT,
"the window followed the cursor from where the drag started"
);
s.mouse_button(tx + 80.0, ty, "LeftButton", false);
s.mouse_move(tx + 400.0, ty);
assert_eq!(
left(&mut s),
start + 75.0,
start + 80.0 - DRAG_STARTS_AT,
"OnDragStop ended it — a move that outlived the button would keep going"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -318,8 +321,8 @@ fn a_moved_window_is_user_placed_and_the_managed_pass_skips_it() {
let moved = left(&mut s);
assert_eq!(
moved,
seated + 115.0,
"120 less the 5px spent on the threshold"
seated + 120.0 - DRAG_STARTS_AT,
"120 less the move spent on the drag-start distance"
);
assert!(
s.eval::<bool>("return ChatFrame1:IsUserPlaced()").unwrap(),
@@ -534,7 +537,7 @@ fn drag_named_tab(s: &mut UiScript, window: &str, dx: f32, dy: f32) {
"the tab is hit"
);
s.mouse_button(tx, ty, "LeftButton", true);
s.mouse_move(tx + 5.0, ty);
s.mouse_move(tx + DRAG_STARTS_AT, ty);
s.mouse_move(tx + dx, ty + dy);
s.mouse_button(tx + dx, ty + dy, "LeftButton", false);
s.resolve();
@@ -315,7 +315,7 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
s.run("ToggleSpellBook(BOOKTYPE_SPELL)").unwrap();
s.resolve();
// Drag Fireball off its book button (press → past the 4px threshold → the payload is up).
// Drag Fireball off its book button (press → past the drag-start distance → the payload is up).
let (l, r, t, b) = (
s.eval::<f32>("return SpellButton1:GetLeft()").unwrap(),
s.eval::<f32>("return SpellButton1:GetRight()").unwrap(),
+22 -10
View File
@@ -3,9 +3,23 @@
use crate::script::Model;
use crate::widget::FrameHandle;
/// The drag-start distance in pixels, exceeded strictly; the reference starts at 0.01 frame units
/// or more from the press (`0x81c468`, squared by `0x76b300`).
pub(crate) const DRAG_START_THRESHOLD: f32 = 4.0;
/// The drag-start threshold, 0.01 layout units squared: `0x76b300` stores `0.01f × 0.01f`
/// (`0x81c468` = `0.01f`) in `[0xcf0c14]` as an f32.
const DRAG_START_DISTANCE_SQ: f32 = 0.01 * 0.01;
/// Whether `pos` is far enough from the press point `start` to start the drag (`0x76b34c`–`0x76b372`).
/// The reference measures in layout units, where the screen diagonal is 1 (`0x41ad80`, from the
/// press and move converters `0x425666`/`0x425766`), so the pointer, in root units, is divided by
/// [`Model::layout_unit`]: 1% of the diagonal at every screen size and UI scale. The squared
/// distance is taken in `f64` against the f32 threshold (x87 at 53 bits: `WinMain`'s
/// `_controlfp(0x9001f)`, `D3DCREATE_FPU_PRESERVE`), and only a smaller one skips (`0x76b372`
/// `jne`), so a NaN, which also sets the carry flag, skips too.
fn past_start_distance(model: &Model, start: (f32, f32), pos: (f32, f32)) -> bool {
let unit = model.layout_unit();
let axis = |a: f32, b: f32| f64::from(a / unit) - f64::from(b / unit);
let (dx, dy) = (axis(pos.0, start.0), axis(pos.1, start.1));
dx * dx + dy * dy >= f64::from(DRAG_START_DISTANCE_SQ)
}
/// The event button bits in order, `1`..`0x10` (`0x76bf70`'s name table).
const HELD_ORDER: [&str; 5] = [
@@ -86,9 +100,7 @@ pub(crate) fn maybe_start_drag(model: &mut Model, pos: (f32, f32)) -> Option<(u3
if g.started {
return None;
}
let (sx, sy) = g.start;
let dist = ((pos.0 - sx).powi(2) + (pos.1 - sy).powi(2)).sqrt();
if dist <= DRAG_START_THRESHOLD {
if !past_start_distance(model, g.start, pos) {
return None;
}
// `OnDragStart`'s argument is the lowest button of the move event's held mask
@@ -174,17 +186,17 @@ mod tests {
s.resolve();
s.mouse_button(100.0, 300.0, "LeftButton", true); // press inside A
s.mouse_move(102.0, 300.0); // 2px: under the threshold
s.mouse_move(102.0, 300.0); // 2 units: under the threshold
assert_eq!(s.eval::<i64>("return drag_starts").unwrap(), 0);
s.mouse_move(110.0, 300.0); // 10px from the press: starts
s.mouse_move(120.0, 300.0); // 20 units from the press: starts
assert_eq!(s.eval::<i64>("return drag_starts").unwrap(), 1);
assert_eq!(
s.eval::<String>("return drag_button").unwrap(),
"LeftButton"
);
s.mouse_move(120.0, 300.0); // further movement doesn't re-fire
s.mouse_move(140.0, 300.0); // further movement doesn't re-fire
assert_eq!(s.eval::<i64>("return drag_starts").unwrap(), 1);
s.mouse_move(600.0, 300.0); // over B now
@@ -230,7 +242,7 @@ mod tests {
s.resolve();
s.mouse_button(100.0, 300.0, "LeftButton", true);
s.mouse_move(110.0, 300.0); // starts the drag
s.mouse_move(120.0, 300.0); // starts the drag
s.mouse_button(100.0, 300.0, "LeftButton", false); // released back on A
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 0);
assert!(s.errors().is_empty(), "{:?}", s.errors());
@@ -0,0 +1,124 @@
//! The drag-start distance: 0.01 of the screen diagonal (`0x76b320`, `0x76b300`, `0x81c468`).
use super::common::script;
use crate::script::UiScript;
/// A drag-registered frame covering `width`×`height` root units from `(-100, 0)`, under a
/// `UIParent` when `ui_scale` is given, with `OnDragStart` counting into `starts`.
fn drag_script(width: f32, height: f32, ui_scale: Option<f32>) -> UiScript {
let mut s = script();
s.set_screen_size(width, height);
s.run(
r#"
starts = 0
local up = CreateFrame("Frame", "UIParent")
up:SetPoint("BOTTOMLEFT", 0, 0); up:SetPoint("TOPRIGHT", 0, 0)
local a = CreateFrame("Frame", "A", up)
a:SetPoint("BOTTOMLEFT", -100, 0); a:SetWidth(5000); a:SetHeight(5000); a:EnableMouse(true)
a:RegisterForDrag("LeftButton")
a:SetScript("OnDragStart", function() starts = starts + 1 end)
"#,
)
.unwrap();
if let Some(v) = ui_scale {
s.set_ui_scale(v);
s.apply_ui_scale((1024, 768), &mut None);
}
s.resolve();
s
}
fn starts(s: &UiScript) -> i64 {
s.eval::<i64>("return starts").unwrap()
}
/// The layout unit, the screen diagonal in root units.
fn unit(s: &UiScript) -> f32 {
s.model_ref().layout_unit()
}
/// Press at the origin of a drag frame, move `frac` of the diagonal right, and say whether the
/// drag started.
fn starts_at(s: &mut UiScript, frac: f32) -> bool {
let d = frac * unit(s);
s.run("starts = 0").unwrap();
s.mouse_button(0.0, 300.0, "LeftButton", true);
s.mouse_move(d, 300.0);
let started = starts(s) == 1;
s.mouse_button(d, 300.0, "LeftButton", false);
started
}
/// 0.0099 of the diagonal is short of the start; 0.0101 is past it.
#[test]
fn a_move_under_one_percent_of_the_diagonal_starts_no_drag() {
let mut s = drag_script(1024.0, 768.0, None);
assert!(!starts_at(&mut s, 0.0099), "0.0099 of the diagonal");
assert!(starts_at(&mut s, 0.0101), "0.0101 of the diagonal");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// At 1024×768 the threshold is 12.8 px, not the old 4.
#[test]
fn at_1024_by_768_a_drag_needs_12_8_px() {
let mut s = drag_script(1024.0, 768.0, None);
let u = unit(&s);
assert!(!starts_at(&mut s, 12.0 / u), "12 px");
assert!(starts_at(&mut s, 13.0 / u), "13 px");
}
/// The boundary of `0x76b368`: the squared distance in x87 53-bit precision (`WinMain`'s
/// `_controlfp(0x9001f)`, `D3DCREATE_FPU_PRESERVE`) against the f32 `0.01f × 0.01f`, which rounds up
/// past the exact square of `0.01f`. A move of exactly `0.01f` in layout units is short of it and
/// the next f32 above starts the drag (`0x76b372`: only a smaller distance skips).
#[test]
fn the_boundary_is_the_f32_square_of_0_01_compared_as_a_double() {
let mut s = drag_script(1024.0, 768.0, None);
let u = unit(&s);
let at = 0.01_f32;
let above = f32::from_bits(at.to_bits() + 1);
// A root-unit x whose layout coordinate (`x / u`) is exactly `want`.
let root_x = |want: f32| -> f32 {
let guess = want * u;
(-16..=16_i32)
.map(|k| f32::from_bits((guess.to_bits() as i32 + k) as u32))
.find(|x| x / u == want)
.expect("a root x that lands on the layout coordinate")
};
for (want, expect) in [(at, false), (above, true)] {
let x = root_x(want);
s.mouse_button(0.0, 300.0, "LeftButton", true);
s.mouse_move(x, 300.0);
assert_eq!(starts(&s) == 1, expect, "a layout move of {want:e}");
s.mouse_button(x, 300.0, "LeftButton", false);
s.run("starts = 0").unwrap();
}
}
/// The threshold follows the window diagonal: 22 px at 1920×1080 (1% of 2203 px), not 12.8.
#[test]
fn the_threshold_scales_with_the_screen_diagonal() {
let root_w = 1920.0 * 768.0 / 1080.0;
let mut s = drag_script(root_w, 768.0, None);
let px = |root: f32| root * 1080.0 / 768.0;
let thr = 0.01 * unit(&s);
assert!((px(thr) - 22.03).abs() < 0.01, "{} px", px(thr));
assert!(!starts_at(&mut s, 0.0099));
assert!(starts_at(&mut s, 0.0101));
// 14 px passes at 1024×768 and not here.
let u = unit(&s);
assert!(!starts_at(&mut s, 14.0 * 768.0 / 1080.0 / u));
}
/// The UI scale is `UIParent`'s own, never the pointer's: the same fraction of the diagonal starts
/// a drag under any scale, on either screen.
#[test]
fn the_threshold_does_not_follow_the_ui_scale() {
for (w, h) in [(1024.0, 768.0), (1920.0 * 768.0 / 1080.0, 768.0)] {
for scale in [0.64, 1.0, 1.15] {
let mut s = drag_script(w, h, Some(scale));
assert!(!starts_at(&mut s, 0.0099), "{w}x{h} scale {scale}");
assert!(starts_at(&mut s, 0.0101), "{w}x{h} scale {scale}");
}
}
}
@@ -15,6 +15,7 @@ mod cooldown;
mod create_frame_template;
mod cvar_bindings;
mod dispatch_bench;
mod drag_threshold;
mod end_to_end;
mod engine_updates;
mod events;
@@ -41,20 +41,20 @@ fn the_canonical_addon_idiom_moves_a_frame_and_the_position_survives_the_stop()
assert_eq!(corner(&mut s), (100.0, 100.0), "born where SetPoint put it");
s.mouse_button(150.0, 140.0, "LeftButton", true); // press inside the panel
s.mouse_move(152.0, 140.0); // 2 px, under the drag threshold
s.mouse_move(152.0, 140.0); // 2 units, under the drag threshold
assert_eq!(s.eval::<i64>("return starts").unwrap(), 0);
assert_eq!(corner(&mut s), (100.0, 100.0));
s.mouse_move(160.0, 140.0); // crosses the threshold ⇒ OnDragStart ⇒ StartMoving
s.mouse_move(170.0, 140.0); // crosses the threshold ⇒ OnDragStart ⇒ StartMoving
assert_eq!(s.eval::<i64>("return starts").unwrap(), 1);
s.mouse_move(260.0, 190.0); // +100, +50 from the grab
s.mouse_move(270.0, 190.0); // +100, +50 from the grab
assert_eq!(
corner(&mut s),
(200.0, 150.0),
"the frame follows the cursor delta since StartMoving"
);
s.mouse_button(260.0, 190.0, "LeftButton", false); // OnDragStop ⇒ StopMovingOrSizing
s.mouse_button(270.0, 190.0, "LeftButton", false); // OnDragStop ⇒ StopMovingOrSizing
assert_eq!(s.eval::<i64>("return stops").unwrap(), 1);
assert_eq!(corner(&mut s), (200.0, 150.0), "the position survives");
@@ -74,17 +74,17 @@ fn the_canonical_addon_idiom_moves_a_frame_and_the_position_survives_the_stop()
fn a_moving_frame_follows_successive_mouse_moves() {
let mut s = movable_panel();
s.mouse_button(150.0, 140.0, "LeftButton", true);
s.mouse_move(160.0, 140.0); // the move that starts the drag
s.mouse_move(170.0, 140.0); // the move that starts the drag
assert_eq!(
corner(&mut s),
(100.0, 100.0),
"the frame follows from where StartMoving was called, so the starting move moves nothing"
);
for (step, want) in [
((170.0, 140.0), (110.0, 100.0)),
((170.0, 160.0), (110.0, 120.0)),
((120.0, 160.0), (60.0, 120.0)), // back left of where it started
((120.0, 160.0), (60.0, 120.0)), // a zero-delta move changes nothing
((180.0, 140.0), (110.0, 100.0)),
((180.0, 160.0), (110.0, 120.0)),
((130.0, 160.0), (60.0, 120.0)), // back left of where it started
((130.0, 160.0), (60.0, 120.0)), // a zero-delta move changes nothing
] {
s.mouse_move(step.0, step.1);
assert_eq!(corner(&mut s), want, "after moving to {step:?}");