From 69b25e68d02918ea84b7cb066e42bee87b88e5d9 Mon Sep 17 00:00:00 2001 From: SMOSTAFAH1 <1.318674e+08+SMOSTAFAH1@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:10:18 +0000 Subject: [PATCH 1/2] fix(selection): update marquee selection while dragging Select objects inside the live marquee on pointer moves instead of waiting for pointerup. Add regression coverage for the held-pointer state.\n\nCo-authored-by: Manus --- crates/draw-engine/src/engine/pointer_move.rs | 20 ++++++++++++++- crates/draw-engine/tests/ci_pointer.rs | 25 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/crates/draw-engine/src/engine/pointer_move.rs b/crates/draw-engine/src/engine/pointer_move.rs index 53b89e8..1dd0232 100644 --- a/crates/draw-engine/src/engine/pointer_move.rs +++ b/crates/draw-engine/src/engine/pointer_move.rs @@ -4,7 +4,9 @@ use crate::interaction::constrain_to_angle; use crate::interaction::{linear_from_drag, rect_from_drag, snap_move}; use crate::scene::binding::{anchor, anchor_for_drop, set_anchor, Anchor, End, EndDrop}; use crate::scene::{scene_bounds, DrawElement, DrawElementType}; -use crate::selection::{resize_element_within, rotate_element, HandleKind}; +use crate::selection::{elements_in_marquee_among, resize_element_within, rotate_element, HandleKind}; + +const MARQUEE_MIN_DRAG: f64 = 2.0; impl DrawEngine { /// `invert_snap` is the host's Ctrl/Cmd: it flips object snapping for this move, on @@ -313,6 +315,22 @@ impl DrawEngine { Some(Interaction::Laser) } Interaction::Marquee { start, base, .. } => { + // Resolve the band continuously, not only in `end_pointer`. This makes the + // selection visible while the button is still held, which is important for + // large boards: the user must be able to see which objects the live band will + // capture before deciding where to release. Keep the original selection as + // the additive base, so Shift-drag remains stable while the caught set changes. + let rect = crate::selection::marquee_rect(start.x, start.y, world.x, world.y); + let dragged = (rect.max_x - rect.min_x).abs() > MARQUEE_MIN_DRAG + || (rect.max_y - rect.min_y).abs() > MARQUEE_MIN_DRAG; + let mut ids = base.clone(); + if dragged { + ids.extend(elements_in_marquee_among( + self.scene.iter_ordered().filter(|el| !self.untouchable(el)), + rect, + )); + } + self.select_caught(ids); // The rubber band is chrome: nothing in the scene changes as it grows, so // nothing else will mark the frame dirty. Without this the rectangle was // never painted at all — the state followed the pointer perfectly and the diff --git a/crates/draw-engine/tests/ci_pointer.rs b/crates/draw-engine/tests/ci_pointer.rs index aefbc8d..0c8c307 100644 --- a/crates/draw-engine/tests/ci_pointer.rs +++ b/crates/draw-engine/tests/ci_pointer.rs @@ -201,6 +201,31 @@ fn pointer_select_tool_click_empty_canvas_clears_selection() { assert!(engine.get_selection().is_empty()); } +#[test] +fn pointer_marquee_selects_objects_before_pointerup() { + let first = filled(box_at(40.0, 40.0, 40.0, 40.0)); + let second = filled(box_at(120.0, 40.0, 40.0, 40.0)); + let outside = filled(box_at(260.0, 40.0, 40.0, 40.0)); + let mut engine = engine_with_scene(vec![first.clone(), second.clone(), outside]); + engine.set_tool(DrawTool::Select); + + engine.begin_pointer(20.0, 20.0, false, false); + engine.move_pointer(180.0, 100.0, false, false); + + // The button is still held here. The live band must already have selected both objects + // inside it; waiting for `end_pointer` is the regression this test prevents. + let mut selected = engine.get_selection(); + selected.sort(); + let mut expected = vec![first.id, second.id]; + expected.sort(); + assert_eq!(selected, expected); + + engine.end_pointer(); + let mut after_release = engine.get_selection(); + after_release.sort(); + assert_eq!(after_release, expected); +} + #[test] fn pointer_eraser_tool_deletes_clicked_element() { // Filled, so a click in the middle lands on it: a transparent shape is hit only on From c5bbf726bab7843da447b82cf6f4166f4dd4d421 Mon Sep 17 00:00:00 2001 From: SMOSTAFAH1 <1.318674e+08+SMOSTAFAH1@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:12:48 +0000 Subject: [PATCH 2/2] style: format marquee selection imports Co-authored-by: Manus --- crates/draw-engine/src/engine/pointer_move.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/draw-engine/src/engine/pointer_move.rs b/crates/draw-engine/src/engine/pointer_move.rs index 1dd0232..68e40f8 100644 --- a/crates/draw-engine/src/engine/pointer_move.rs +++ b/crates/draw-engine/src/engine/pointer_move.rs @@ -4,7 +4,9 @@ use crate::interaction::constrain_to_angle; use crate::interaction::{linear_from_drag, rect_from_drag, snap_move}; use crate::scene::binding::{anchor, anchor_for_drop, set_anchor, Anchor, End, EndDrop}; use crate::scene::{scene_bounds, DrawElement, DrawElementType}; -use crate::selection::{elements_in_marquee_among, resize_element_within, rotate_element, HandleKind}; +use crate::selection::{ + elements_in_marquee_among, resize_element_within, rotate_element, HandleKind, +}; const MARQUEE_MIN_DRAG: f64 = 2.0;