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.rs
new 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");
+}
