diff --git a/crates/benilla-app/src/addon_harness/use_probe.rs b/crates/benilla-app/src/addon_harness/use_probe.rs index 92f0a304..82378194 100644 --- a/crates/benilla-app/src/addon_harness/use_probe.rs +++ b/crates/benilla-app/src/addon_harness/use_probe.rs @@ -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); diff --git a/crates/benilla-app/src/addon_harness/use_probe_tests.rs b/crates/benilla-app/src/addon_harness/use_probe_tests.rs index baf4b953..2b1447d5 100644 --- a/crates/benilla-app/src/addon_harness/use_probe_tests.rs +++ b/crates/benilla-app/src/addon_harness/use_probe_tests.rs @@ -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( diff --git a/crates/benilla-app/src/ui_script/action_bar_tests.rs b/crates/benilla-app/src/ui_script/action_bar_tests.rs index ef05f764..398c24b4 100644 --- a/crates/benilla-app/src/ui_script/action_bar_tests.rs +++ b/crates/benilla-app/src/ui_script/action_bar_tests.rs @@ -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"); diff --git a/crates/benilla-app/src/ui_script/bagnon_render_tests.rs b/crates/benilla-app/src/ui_script/bagnon_render_tests.rs index 76cd5553..8cf89622 100644 --- a/crates/benilla-app/src/ui_script/bagnon_render_tests.rs +++ b/crates/benilla-app/src/ui_script/bagnon_render_tests.rs @@ -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); diff --git a/crates/benilla-app/src/ui_script/chat_resize_tests.rs b/crates/benilla-app/src/ui_script/chat_resize_tests.rs index 77301637..214b418e 100644 --- a/crates/benilla-app/src/ui_script/chat_resize_tests.rs +++ b/crates/benilla-app/src/ui_script/chat_resize_tests.rs @@ -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::("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(); diff --git a/crates/benilla-app/src/ui_script/chat_tests.rs b/crates/benilla-app/src/ui_script/chat_tests.rs index 7103db07..6da1b88b 100644 --- a/crates/benilla-app/src/ui_script/chat_tests.rs +++ b/crates/benilla-app/src/ui_script/chat_tests.rs @@ -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::("return SpellButton1:GetLeft()").unwrap(), s.eval::("return SpellButton1:GetRight()").unwrap(), diff --git a/crates/benilla-ui/src/script/cursor/drag.rs b/crates/benilla-ui/src/script/cursor/drag.rs index 2c54c91e..a812ca27 100644 --- a/crates/benilla-ui/src/script/cursor/drag.rs +++ b/crates/benilla-ui/src/script/cursor/drag.rs @@ -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::("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::("return drag_starts").unwrap(), 1); assert_eq!( s.eval::("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::("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::("return clicks").unwrap(), 0); assert!(s.errors().is_empty(), "{:?}", s.errors()); diff --git a/crates/benilla-ui/src/script/tests/drag_threshold.rs b/crates/benilla-ui/src/script/tests/drag_threshold.rs new file mode 100644 index 00000000..2bfa5c4c --- /dev/null +++ b/crates/benilla-ui/src/script/tests/drag_threshold.rs @@ -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) -> 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::("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}"); + } + } +} diff --git a/crates/benilla-ui/src/script/tests/mod.rs b/crates/benilla-ui/src/script/tests/mod.rs index 13befd49..ad0fcb60 100644 --- a/crates/benilla-ui/src/script/tests/mod.rs +++ b/crates/benilla-ui/src/script/tests/mod.rs @@ -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; diff --git a/crates/benilla-ui/src/script/tests/movable.rs b/crates/benilla-ui/src/script/tests/movable.rs index cfaf53e1..d3022547 100644 --- a/crates/benilla-ui/src/script/tests/movable.rs +++ b/crates/benilla-ui/src/script/tests/movable.rs @@ -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::("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::("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::("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:?}");