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:
co-authored by
Claude Sonnet 5.5
parent
a2825a185f
commit
af281a05f7
@@ -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(),
|
||||
|
||||
@@ -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:?}");
|
||||
|
||||
Reference in New Issue
Block a user