collective

Browse / github.com/storytold/cadcraft / patch

Canvas: a click made while the mouse is moving still selects (fixes #1546)

[Bug Report] [CadCraft] Cross selection doesnt work when mouse button clicked with slightest movement. ### Description During cross polygon selection the window is not formed if the mouse click is done when the mouse is in motion. When working with CAD we make swift movements and click on the go. So it would be nice if it is fixed. ### Operating system and version Windows 11 Pro ### Ticket details | | | |---|---| | **App** | CadCraft | | **Type** | Bug Report | | **Reported by** | Sriraam_architect (Discord username: sriraam_architect_47723) — Discord user ID `1560356819489136824` | | **Submitted** | 2026-10-18 05:48:27 UTC | | **Discord server** | ArtCraft (`1359579021108842617`) | | **Su

Status
open
Score
0.00
Adopters
0 other operators
Author
SusieeTheLinuxUser (imported from pull request #2032, open)
Base
7bf5905ac97316a7896de8cbc12d243f57b09bd8
Size
+146 −1 in 3 files
Review
pending
Shared
2026-10-28
Id
1217101b809d2643e048c9e3

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/canvas.rs crates/ui-egui/tests/it/main.rs crates/ui-egui/tests/it/moving_clicks.rs

diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs--- a/crates/ui-egui/src/canvas.rs+++ b/crates/ui-egui/src/canvas.rs@@ -121,6 +121,9 @@ pub struct CanvasState {     pub param: crate::parametric::Cache,     /// A selection window opened by a press-and-drag, to close where the button is let go.     drag_window: bool,+    /// The primary drag began on a grip (or a dynamic block's grip): its release is the grip's+    /// own gesture, not a click made while the mouse was moving (#1546).+    drag_from_grip: bool,     /// The right-click that opened the shortcut menu held Shift or Ctrl: it is the object snap     /// menu ([`crate::context_menu`]).     pub snap_menu: bool,@@ -1538,7 +1541,37 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {     let point_prompt = app.session.current_prompt().is_some_and(|p| cadcraft_engine::pointmod::accepts(&p))         && app.session.pending_window.is_none()         && !app.session.realtime_zoom_active();-    let pressed_primary = pressed_primary || (point_prompt && resp.drag_stopped_by(egui::PointerButton::Primary));+    // A drag that began on a grip (or a dynamic block's grip) belongs to that grip: remembered so+    // its release is never taken for a click made while the mouse was moving (#1546). Decided before+    // the release is classified, since a slow frame can start and stop a drag at once (the press is+    // then only in this frame's events).+    if resp.drag_started_by(egui::PointerButton::Primary) {+        let origin = ui.input(|i| {+            i.pointer.press_origin().or_else(|| {+                i.events.iter().find_map(|e| match e {+                    egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, .. } => Some(*pos),+                    _ => None,+                })+            })+        });+        app.canvas.drag_from_grip =+            origin.is_some_and(|origin| grip_at(app, &xf, Some(origin)).is_some() || crate::dyngrips::at(app, &xf, Some(origin)).is_some());+    }+    // Selecting works the same way (#1546): idle, or a command at "Select objects", a click made+    // while the mouse is moving still picks, or opens a window at the corner it lands on. A window+    // already pending is only ever closed by real clicks (PICKDRAG = 0 leaves it for the second+    // click), a PICKDRAG press-drag closes below where its drag ends, and a drag that began on a+    // grip belongs to the grip.+    let selecting = app.session.running.is_none() || app.session.current_prompt().is_some_and(|p| p.accept.select);+    let pressed_primary = pressed_primary+        || (point_prompt && resp.drag_stopped_by(egui::PointerButton::Primary))+        || (selecting+            && !point_prompt+            && !app.canvas.drag_window+            && !app.canvas.drag_from_grip+            && app.session.pending_window.is_none()+            && !app.session.realtime_zoom_active()+            && resp.drag_stopped_by(egui::PointerButton::Primary));     if point_prompt && inside && pressed_primary && !middle_down && app.cmd.options.is_none() && app.cmd.buffer.is_empty() {         let releases: Vec<egui::Pos2> = ui.input(|i| {             i.events@@ -1671,6 +1704,9 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {             app.session.echo(e.to_string());         }     }+    if resp.drag_stopped_by(egui::PointerButton::Primary) {+        app.canvas.drag_from_grip = false;+    }     // Press-and-drag selection window (PICKDRAG = 2; click-click still works): a drag     // that starts while objects are being selected opens a window where the button went down,     // and letting go closes it. Drags on grips and drags while drawing are left alone.diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs--- a/crates/ui-egui/tests/it/main.rs+++ b/crates/ui-egui/tests/it/main.rs@@ -15,6 +15,7 @@ mod hidden_command_line; mod lisp_files; mod measure_distance_snapping; mod menubar_hover;+mod moving_clicks; mod mspace_viewport_view; mod options_dialog; mod parameters_rename;diff --git a/crates/ui-egui/tests/it/moving_clicks.rs b/crates/ui-egui/tests/it/moving_clicks.rsnew file mode 100644--- /dev/null+++ b/crates/ui-egui/tests/it/moving_clicks.rs@@ -0,0 +1,108 @@+//! A click made while the mouse is moving still selects (#1546): egui drops a click whose button+//! comes up away from where it went down, and the pick or the window's first corner never+//! happened. As with placing points (#815), the click counts — landing where the button came up.++use cadcraft_engine::Session;+use cadcraft_engine::geom::Vec2;+use cadcraft_ui_egui::{CadApp, Services};+use egui::{Event, Modifiers, PointerButton, Pos2, vec2};+use egui_kittest::Harness;+use serde_json::json;++fn harness() -> Harness<'static, CadApp> {+    let mut app = CadApp::new(Session::new(), Services::default());+    app.canvas.zoom_pending = false;+    app.session.execute("line", &json!({"points":[[0,0],[10,0]]})).unwrap();+    app.session.execute("view.set", &json!({"center":[5,0],"height":30})).unwrap();+    app.session.settings.osmode = 0;+    app.session.settings.snapmode = false;+    app.session.settings.orthomode = false;+    // Two-click selection: a moving click must never be a press-and-drag window.+    app.session.execute("options", &json!({"PICKDRAG":0})).unwrap();+    let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(+        |ui, app: &mut CadApp| {+            app.logic(ui.ctx());+            app.ui(ui);+        },+        app,+    );+    h.run_steps(3);+    h+}++fn screen(h: &Harness<'_, CadApp>, p: Vec2) -> Pos2 {+    h.state().canvas.xf.unwrap().to_screen(p)+}++fn button(h: &mut Harness<'_, CadApp>, pos: Pos2, pressed: bool) {+    h.event(Event::PointerMoved(pos));+    h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });+    h.run_steps(1);+}++#[test]+fn a_click_made_while_the_mouse_moves_still_picks() {+    let mut h = harness();+    // The press starts well above the line; the hand is on its way down when the button comes up,+    // landing inside the pick box around the line.+    let (down, up) = (screen(&h, Vec2::new(5.0, 1.0)), screen(&h, Vec2::new(5.0, 0.1)));+    button(&mut h, down, true);+    button(&mut h, up, false);+    h.run_steps(2);+    assert_eq!(h.state().session.selection().len(), 1);+}++#[test]+fn a_click_made_while_the_mouse_moves_still_opens_a_selection_window() {+    let mut h = harness();+    // The first corner lands while the hand is moving, to the upper right of the line.+    let (down, up) = (screen(&h, Vec2::new(12.0, 3.0)), screen(&h, Vec2::new(12.6, 2.4)));+    button(&mut h, down, true);+    button(&mut h, up, false);+    h.run_steps(2);+    assert!(h.state().session.pending_window.is_some(), "the moving click must open the window");+    // The second corner, deliberately: lower and to the left, so the window crosses the line.+    h.run_steps(30); // Separate the deliberate click from egui's double-click interval.+    let b = screen(&h, Vec2::new(-2.0, -3.0));+    button(&mut h, b, true);+    button(&mut h, b, false);+    h.run_steps(2);+    assert!(h.state().session.pending_window.is_none());+    assert_eq!(h.state().session.selection().len(), 1, "the crossing window must take the line");+}++#[test]+fn a_click_made_while_the_mouse_moves_still_picks_at_a_select_prompt() {+    let mut h = harness();+    h.state_mut().session.start("select").unwrap();+    h.run_steps(2);+    let (down, up) = (screen(&h, Vec2::new(5.0, 1.0)), screen(&h, Vec2::new(5.0, 0.1)));+    button(&mut h, down, true);+    button(&mut h, up, false);+    h.run_steps(2);+    assert_eq!(h.state().session.selection().len(), 1);+}++#[test]+fn a_moving_release_never_consumes_a_pending_corner() {+    let mut h = harness();+    let a = screen(&h, Vec2::new(5.0, -5.0));+    button(&mut h, a, true);+    button(&mut h, a, false);+    h.run_steps(2);+    assert!(h.state().session.pending_window.is_some(), "the corner click must open the window");+    // A held-button movement after the corner is not the second click, however short: the window+    // stays open for the deliberate click that follows.+    button(&mut h, a + vec2(0.0, 20.0), true);+    button(&mut h, a + vec2(12.0, 20.0), false);+    h.run_steps(2);+    assert!(h.state().session.pending_window.is_some(), "the movement must not consume the corner");+    assert!(h.state().session.selection().is_empty());+    h.run_steps(30);+    let b = screen(&h, Vec2::new(-2.0, 2.0));+    button(&mut h, b, true);+    button(&mut h, b, false);+    h.run_steps(2);+    assert!(h.state().session.pending_window.is_none());+    assert_eq!(h.state().session.selection().len(), 1, "the deliberate click completes the window");+}