diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 84e12d3b3..63f46f553 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -800,9 +800,14 @@ impl Tool for SelectionTool { } } - fn overlays(&self, _cx: &ToolContext) -> Vec { + fn overlays(&self, cx: &ToolContext) -> Vec { match &self.drag { - Drag::Marquee { start, cur } | Drag::Place { start, cur } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))], + Drag::Place { start, cur } if crate::thread::is_drag(cx, *start, *cur) => { + let Some((sr, r, guides)) = crate::thread::drag_frame(cx, *start, *cur) else { return vec![] }; + let r = Rect::from_points(cx.layout.to_canvas(sr, Point::new(r.x0, r.y0)), cx.layout.to_canvas(sr, Point::new(r.x1, r.y1))); + std::iter::once(Overlay::Marquee(r)).chain(guides).collect() + } + Drag::Marquee { start, cur } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))], Drag::Move { .. } | Drag::Resize { .. } | Drag::Rotate { .. } | Drag::Anchor { .. } => self.guides.clone(), _ => vec![], } diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index f898755bf..17fc4a9c2 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -822,6 +822,26 @@ fn loaded_drag_makes_a_frame_of_that_size_and_shift_autoflows() { assert_eq!(t.cursor(&cx, td.at(500.0, 700.0), Mods::default()), Cursor::Arrow, "autoflow unloads"); } +#[test] +fn loaded_drag_snaps_like_frame_drawing() { + let td = thread_doc(); + let cx = ToolContext { snap: SnapView::FACTORY, ..td.cx() }; + let mut t = create("selection"); + click(&mut t, &cx, td.port(td.a, true), Mods::default()); + let col = td.d.spreads[0].pages[0].column_rects()[0]; + // Press and release a point or two inside the column's edges: both sides snap to them. + let (p0, p1) = (td.at(col.x0 + 1.5, 560.0), td.at(col.x1 - 2.0, 650.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, p0.x, p0.y)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, p1.x, p1.y)); + let Some(Overlay::Marquee(m)) = t.overlays(&cx).first().cloned() else { panic!("no marquee") }; + assert!((m.x0 - td.at(col.x0, 0.0).x).abs() < 1e-9 && (m.x1 - td.at(col.x1, 0.0).x).abs() < 1e-9, "{m:?}"); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, p1.x, p1.y)); + let [Action::Exec(id, p)] = a.as_slice() else { panic!("{a:?}") }; + assert_eq!(id, "text.thread"); + let r: Vec = p["rect"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect(); + assert_eq!((r[0], r[2]), (col.x0, col.x1), "{p}"); +} + #[test] fn alt_click_stays_loaded() { let td = thread_doc(); diff --git a/crates/tools/src/thread.rs b/crates/tools/src/thread.rs index 7e6500fbb..001fd154e 100644 --- a/crates/tools/src/thread.rs +++ b/crates/tools/src/thread.rs @@ -10,7 +10,7 @@ use designcraft_doc::{Document, Item, ItemId, SpreadRef}; use designcraft_geom::{Point, Rect}; use serde_json::{Value, json}; -use crate::{CanvasLayout, Cursor, Mods, ToolContext, rect_json, spread_json}; +use crate::{CanvasLayout, Cursor, Mods, Overlay, ToolContext, rect_json, spread_json}; /// Port square size, in screen pixels. pub const PORT_SIZE_PX: f64 = 8.5; @@ -87,6 +87,21 @@ pub fn click_frame_rect(doc: &Document, sr: SpreadRef, p: Point) -> Option [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()).then_some(r) } +/// Is a press at `start` released at `p` (canvas) a drag rather than a click? +pub(crate) fn is_drag(cx: &ToolContext, start: Point, p: Point) -> bool { + (p - start).hypot() >= cx.tol(3.0) +} + +/// The frame a drag with the loaded cursor from `start` to `p` (canvas) makes, in spread +/// coordinates, and the snap guides to draw. It is drawn like the frame tools (the press point +/// and the moving edges snap) without their modifiers: Shift and Alt autoflow here. +pub(crate) fn drag_frame(cx: &ToolContext, start: Point, p: Point) -> Option<(SpreadRef, Rect, Vec)> { + let (sr, sa) = cx.layout.spread_at(start)?; + let sa = if cx.snap.any() { crate::snap::snap_point(cx, sr, sa).0 } else { sa }; + let (r, guides) = crate::frame::snapped_drag_rect(cx, sr, sa, cx.layout.to_spread(sr, p), Mods::default()); + Some((sr, r, guides)) +} + /// The loaded text cursor. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct Loaded { @@ -156,10 +171,9 @@ impl Loaded { /// The command for a click at `p`, or a drag from `start` to `p` (canvas). None: nothing to do. pub fn click(self, cx: &ToolContext, start: Point, p: Point, mods: Mods) -> Option<(&'static str, Value)> { - let dragged = (p - start).hypot() >= cx.tol(3.0); - let (pair, new_rect) = if dragged { - let (sr, a) = cx.layout.spread_at(start)?; - (self.pair(cx.doc, None), Some((sr, Rect::from_points(a, cx.layout.to_spread(sr, p))))) + let (pair, new_rect) = if is_drag(cx, start, p) { + let (sr, r, _) = drag_frame(cx, start, p)?; + (self.pair(cx.doc, None), Some((sr, r))) } else { match self.target(cx, p) { Target::Unthread => {