forked from 1b-agent/opencadstudio-upstream
Merge pull request #1856 from gianlucafiore/fix/selection-click-first-lasso-delay
fix(selection): prioritize quick click-click boxes over eager lasso
This commit is contained in:
2 files changed
+111
-14
No files matched your search
+37
-1
@@ -3552,7 +3552,17 @@ mod tests {
|
|||||||
path[0].0, path[0].1,
|
path[0].0, path[0].1,
|
||||||
)));
|
)));
|
||||||
let _ = app.update(Message::ViewportLeftPress);
|
let _ = app.update(Message::ViewportLeftPress);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(180));
|
// Deterministically simulate an intentional hold-to-lasso.
|
||||||
|
// A fast click+move must no longer activate a lasso just
|
||||||
|
// because the cursor crossed the old 24 px shortcut.
|
||||||
|
let held_since =
|
||||||
|
iced::time::Instant::now() - std::time::Duration::from_millis(260);
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selection
|
||||||
|
.borrow_mut()
|
||||||
|
.input
|
||||||
|
.left_press_time = Some(held_since);
|
||||||
for &(x, y) in &path {
|
for &(x, y) in &path {
|
||||||
let _ = app.update(Message::ViewportMove(iced::Point::new(x, y)));
|
let _ = app.update(Message::ViewportMove(iced::Point::new(x, y)));
|
||||||
}
|
}
|
||||||
@@ -3586,6 +3596,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_click_move_release_arms_a_box_at_the_actual_press_location() {
|
||||||
|
use crate::app::Message;
|
||||||
|
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.pick_drag_rect = false;
|
||||||
|
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
|
||||||
|
|
||||||
|
let pressed = iced::Point::new(100.0, 180.0);
|
||||||
|
let moved = iced::Point::new(450.0, 350.0);
|
||||||
|
let _ = app.update(Message::ViewportMove(pressed));
|
||||||
|
let _ = app.update(Message::ViewportLeftPress);
|
||||||
|
let _ = app.update(Message::ViewportMove(moved));
|
||||||
|
{
|
||||||
|
let sel = app.tabs[i].scene.selection.borrow();
|
||||||
|
assert!(!sel.input.left_dragging, "fast first click must win over lasso");
|
||||||
|
assert!(!sel.gesture.poly_active, "the lasso must not arm");
|
||||||
|
}
|
||||||
|
let _ = app.update(Message::ViewportLeftRelease);
|
||||||
|
let sel = app.tabs[i].scene.selection.borrow();
|
||||||
|
assert_eq!(sel.gesture.box_anchor, Some(pressed));
|
||||||
|
assert!(!sel.gesture.poly_active);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pickadd_command_flips_flag() {
|
fn pickadd_command_flips_flag() {
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|||||||
+74
-13
@@ -31,6 +31,65 @@ const UCS_ICON_PICK_PX: f32 = 10.0;
|
|||||||
/// Pixel half-width for clicking an icon arm (line segment).
|
/// Pixel half-width for clicking an icon arm (line segment).
|
||||||
const UCS_ICON_ARM_PX: f32 = 6.0;
|
const UCS_ICON_ARM_PX: f32 = 6.0;
|
||||||
|
|
||||||
|
/// Decide whether a selection press has become a drag. A fast mouse
|
||||||
|
/// movement alone must not turn the first click of a click-click rectangle
|
||||||
|
/// into a lasso. Hold-to-lasso gets the normal dwell; PICKDRAG=1 keeps its
|
||||||
|
/// deliberate immediate rectangular-drag shortcut.
|
||||||
|
fn selection_drag_should_start(dist2: f32, held_ms: u128, pick_drag_rect: bool) -> bool {
|
||||||
|
const MIN_DIST2: f32 = 12.0 * 12.0;
|
||||||
|
const RECT_FAST_DIST2: f32 = 24.0 * 24.0;
|
||||||
|
dist2 > MIN_DIST2
|
||||||
|
&& (held_ms >= POLY_START_DELAY_MS || (pick_drag_rect && dist2 > RECT_FAST_DIST2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For a short selection click, use the actual mouse-down position even if
|
||||||
|
/// the user starts moving immediately. Commands and active drag gestures
|
||||||
|
/// continue using the live cursor position.
|
||||||
|
fn selection_click_position(
|
||||||
|
last: Point,
|
||||||
|
pressed: Option<Point>,
|
||||||
|
is_click: bool,
|
||||||
|
no_command: bool,
|
||||||
|
) -> Point {
|
||||||
|
if is_click && no_command {
|
||||||
|
pressed.unwrap_or(last)
|
||||||
|
} else {
|
||||||
|
last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod selection_gesture_priority_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_first_click_never_becomes_a_lasso_solely_from_cursor_speed() {
|
||||||
|
assert!(!selection_drag_should_start(25.0 * 25.0, 20, false));
|
||||||
|
assert!(!selection_drag_should_start(120.0 * 120.0, 80, false));
|
||||||
|
assert!(!selection_drag_should_start(500.0 * 500.0, POLY_START_DELAY_MS - 1, false));
|
||||||
|
assert!(selection_drag_should_start(25.0 * 25.0, POLY_START_DELAY_MS, false));
|
||||||
|
assert!(!selection_drag_should_start(11.0 * 11.0, POLY_START_DELAY_MS + 500, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pickdrag_rectangle_keeps_its_fast_drag_shortcut() {
|
||||||
|
assert!(selection_drag_should_start(25.0 * 25.0, 20, true));
|
||||||
|
assert!(!selection_drag_should_start(23.0 * 23.0, 20, true));
|
||||||
|
assert!(selection_drag_should_start(13.0 * 13.0, POLY_START_DELAY_MS, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quick_selection_click_anchors_where_the_button_went_down() {
|
||||||
|
let start = Point::new(100.0, 100.0);
|
||||||
|
let moved = Point::new(190.0, 135.0);
|
||||||
|
assert_eq!(selection_click_position(moved, Some(start), true, true), start);
|
||||||
|
// During a command or a genuine press-drag, use the moving cursor.
|
||||||
|
assert_eq!(selection_click_position(moved, Some(start), true, false), moved);
|
||||||
|
assert_eq!(selection_click_position(moved, Some(start), false, true), moved);
|
||||||
|
assert_eq!(selection_click_position(moved, None, true, true), moved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapper coordinates are local to the active pane, while the overlay
|
/// Snapper coordinates are local to the active pane, while the overlay
|
||||||
/// consumes canvas coordinates. Apply the same translation to the visible
|
/// consumes canvas coordinates. Apply the same translation to the visible
|
||||||
/// snap glyph and both Extension guide origins.
|
/// snap glyph and both Extension guide origins.
|
||||||
@@ -1383,17 +1442,12 @@ impl OpenCADStudio {
|
|||||||
.left_press_time
|
.left_press_time
|
||||||
.map(|t| Instant::now().duration_since(t).as_millis())
|
.map(|t| Instant::now().duration_since(t).as_millis())
|
||||||
.unwrap_or(u128::MAX);
|
.unwrap_or(u128::MAX);
|
||||||
// Selection gestures need a wider dead zone than ordinary
|
// In default PICKDRAG=0, cursor speed must never promote a
|
||||||
// pointer clicks. In particular, a quick press/release with
|
// quick click into a lasso. The 250 ms press dwell distinguishes
|
||||||
// residual mouse motion must not start a freeform lasso.
|
// an intentional hold-drag from a fast click-move-click box.
|
||||||
// Keep the existing delay and reserve immediate dragging for
|
// PICKDRAG=1 retains fast press-drag rectangular selection;
|
||||||
// an unmistakable movement (24 px); after the delay, require
|
// grip dragging is handled separately.
|
||||||
// at least 12 px. Grip editing follows its separate path.
|
let dragged = selection_drag_should_start(dist2, elapsed_ms, self.pick_drag_rect);
|
||||||
const SELECTION_DRAG_MIN_PX2: f32 = 12.0 * 12.0;
|
|
||||||
const SELECTION_DRAG_FAST_PX2: f32 = 24.0 * 24.0;
|
|
||||||
let dragged = dist2 > SELECTION_DRAG_MIN_PX2
|
|
||||||
&& (elapsed_ms >= POLY_START_DELAY_MS
|
|
||||||
|| dist2 > SELECTION_DRAG_FAST_PX2);
|
|
||||||
if !sel.input.left_dragging && dragged {
|
if !sel.input.left_dragging && dragged {
|
||||||
sel.input.left_dragging = true;
|
sel.input.left_dragging = true;
|
||||||
if self.pick_drag_rect {
|
if self.pick_drag_rect {
|
||||||
@@ -4303,11 +4357,18 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
let (p, is_click, is_down) = {
|
let (p, is_click, is_down) = {
|
||||||
let sel = self.tabs[i].scene.selection.borrow();
|
let sel = self.tabs[i].scene.selection.borrow();
|
||||||
let p = match sel.input.last_move_pos {
|
let last = match sel.input.last_move_pos {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => return Task::none(),
|
None => return Task::none(),
|
||||||
};
|
};
|
||||||
(p, !sel.input.left_dragging, sel.input.left_down)
|
let is_click = !sel.input.left_dragging;
|
||||||
|
let p = selection_click_position(
|
||||||
|
last,
|
||||||
|
sel.input.left_press_pos,
|
||||||
|
is_click,
|
||||||
|
self.tabs[i].active_cmd.is_none(),
|
||||||
|
);
|
||||||
|
(p, is_click, sel.input.left_down)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Grip editing: click-move-click (plus legacy press-drag).
|
// Grip editing: click-move-click (plus legacy press-drag).
|
||||||
|
|||||||
Reference in new issue
Block a user