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.
This commit is contained in:
aiongg
2026-10-12 03:59:05 +00:00
parent 710ab70abe
commit aa4f85c8e1
3 changed files with 46 additions and 7 deletions
+7 -2
View File
@@ -800,9 +800,14 @@ impl Tool for SelectionTool {
}
}
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
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![],
}
+20
View File
@@ -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<f64> = 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();
+19 -5
View File
@@ -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<Rect>
[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<Overlay>)> {
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 => {