From aa4f85c8e1de4e0905b37f8057d7875defaa519e Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 12 Oct 2026 03:59:05 +0000 Subject: [PATCH] Threading: a frame dragged with the loaded text cursor snaps like frame drawing The press point and the moving edges snap to guides and smart guides, and the drag shows the snapped frame and its snap guides. --- crates/tools/src/select.rs | 9 +++++++-- crates/tools/src/tests.rs | 20 ++++++++++++++++++++ crates/tools/src/thread.rs | 24 +++++++++++++++++++----- 3 files changed, 46 insertions(+), 7 deletions(-) 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 => {