From 4c5ea73d98c28e585c84829d44fe2045ba8cf4d2 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:51:35 -0400 Subject: [PATCH] refactor(tools): fewer parameters and simpler pointer handling (Sonar S107, S3776) (#1725) Co-authored-by: Claude Sonnet 5.5 --- crates/tools/src/select.rs | 5 +- crates/tools/src/snap.rs | 57 +++++---- crates/tools/src/text.rs | 235 +++++++++++++++++++------------------ 3 files changed, 154 insertions(+), 143 deletions(-) diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index c437484c2..cba5fc317 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -570,8 +570,7 @@ fn finish_snapped_resize( proposed: Rect, mods: Mods, driver_x: bool, - x_edges: [bool; 3], - y_edges: [bool; 3], + (x_edges, y_edges): ([bool; 3], [bool; 3]), hit: &crate::Snap, ) -> Rect { if !mods.alt { @@ -1021,7 +1020,7 @@ impl Tool for SelectionTool { pointer, }, ); - to = finish_snapped_resize(from, handle, proposed, ev.mods, driver_x, x_edges, y_edges, &hit); + to = finish_snapped_resize(from, handle, proposed, ev.mods, driver_x, (x_edges, y_edges), &hit); crate::snap::lay_dimension_guides(&mut hit.guides, cx.layout.xf(spread), proposed, to, x_edges, y_edges, hit.length_delta); self.guides = hit.guides; } diff --git a/crates/tools/src/snap.rs b/crates/tools/src/snap.rs index 11da46178..693ca5906 100644 --- a/crates/tools/src/snap.rs +++ b/crates/tools/src/snap.rs @@ -204,7 +204,7 @@ fn match_rotation(cx: &ToolContext, req: &SnapRequest<'_>, zone_length: f64) -> let mut seen = HashSet::new(); if let Some(spread) = cx.doc.spread(req.spread) { for item in &spread.items { - push_rotation(&mut best, cx.selection, req, item, Affine::IDENTITY, false, 0, &mut seen, proposed, tol); + push_rotation(&mut best, cx.selection, req, item, Walk::ROOT, &mut seen, (proposed, tol)); } } if shows_parent_items(cx, req) { @@ -223,7 +223,24 @@ fn push_parent_rotations( tol: f64, ) { for (item, _) in shown_parent_items(cx.doc, req.spread) { - push_rotation(best, cx.selection, req, item, Affine::IDENTITY, false, 0, seen_items, proposed, tol); + push_rotation(best, cx.selection, req, item, Walk::ROOT, seen_items, (proposed, tol)); + } +} + +/// Where a recursive walk over an item tree stands: the parent's absolute transform, whether an +/// ancestor was skipped, and how deep it is. +#[derive(Clone, Copy)] +struct Walk { + ancestor: Affine, + skipped: bool, + depth: usize, +} + +impl Walk { + const ROOT: Walk = Walk { ancestor: Affine::IDENTITY, skipped: false, depth: 0 }; + + fn child(self, ancestor: Affine, skipped: bool) -> Walk { + Walk { ancestor, skipped, depth: self.depth + 1 } } } @@ -232,13 +249,11 @@ fn push_rotation( selection: &Selection, req: &SnapRequest<'_>, item: &Item, - ancestor: Affine, - skipped: bool, - depth: usize, + walk: Walk, seen: &mut HashSet, - proposed: f64, - tol: f64, + (proposed, tol): (f64, f64), ) { + let Walk { ancestor, skipped, depth } = walk; if depth >= ALIGN_GROUP_DEPTH || !seen.insert(item.id) || item.hidden { return; } @@ -254,7 +269,7 @@ fn push_rotation( return; } for child in item.children() { - push_rotation(best, selection, req, child, abs, skip, depth + 1, seen, proposed, tol); + push_rotation(best, selection, req, child, walk.child(abs, skip), seen, (proposed, tol)); } } @@ -907,7 +922,7 @@ fn dimension_lengths(cx: &ToolContext, req: &SnapRequest<'_>) -> Vec { let mut seen = HashSet::new(); if let Some(sp) = cx.doc.spread(req.spread) { for item in &sp.items { - push_own_lengths(&mut out, cx.selection, req, item, Affine::IDENTITY, false, 0, &mut seen); + push_own_lengths(&mut out, cx.selection, req, item, Walk::ROOT, &mut seen); } } if shows_parent_items(cx, req) { @@ -916,16 +931,8 @@ fn dimension_lengths(cx: &ToolContext, req: &SnapRequest<'_>) -> Vec { out } -fn push_own_lengths( - out: &mut Vec, - selection: &Selection, - req: &SnapRequest<'_>, - item: &Item, - ancestor: Affine, - skipped: bool, - depth: usize, - seen: &mut HashSet, -) { +fn push_own_lengths(out: &mut Vec, selection: &Selection, req: &SnapRequest<'_>, item: &Item, walk: Walk, seen: &mut HashSet) { + let Walk { ancestor, skipped, depth } = walk; if depth >= ALIGN_GROUP_DEPTH || !seen.insert(item.id) || item.hidden { return; } @@ -936,7 +943,7 @@ fn push_own_lengths( let skip = skipped || align_skips_item(selection, req, item.id); if item.is_group() && !item.children().is_empty() { for child in item.children() { - push_own_lengths(out, selection, req, child, abs, skip, depth + 1, seen); + push_own_lengths(out, selection, req, child, walk.child(abs, skip), seen); } return; } @@ -994,7 +1001,7 @@ fn stroke_outset(item: &Item) -> f64 { fn push_parent_lengths(out: &mut Vec, cx: &ToolContext, req: &SnapRequest<'_>, seen_items: &mut HashSet) { for (item, _) in shown_parent_items(cx.doc, req.spread) { - push_own_lengths(out, cx.selection, req, item, Affine::IDENTITY, false, 0, seen_items); + push_own_lengths(out, cx.selection, req, item, Walk::ROOT, seen_items); } } @@ -1084,11 +1091,11 @@ fn pass_align(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) -> let mut best: Option = None; if let Some(sp) = cx.doc.spread(req.spread) { for page in &sp.pages { - offer_box(&mut best, req.rect, flags, axis, page.bounds(), tol, cx.snap.align_edges, cx.snap.align_centers); + offer_box(&mut best, req.rect, flags, axis, page.bounds(), tol, (cx.snap.align_edges, cx.snap.align_centers)); } for item in &sp.items { let Some(rect) = item_target_rect(cx.selection, req, item) else { continue }; - offer_box(&mut best, req.rect, flags, axis, rect, tol, cx.snap.align_edges, cx.snap.align_centers); + offer_box(&mut best, req.rect, flags, axis, rect, tol, (cx.snap.align_edges, cx.snap.align_centers)); } } if shows_parent_items(cx, req) { @@ -1186,7 +1193,7 @@ fn group_box(item: &Item, seen: &mut HashSet, depth: usize, filter: Opti fn offer_parent_items(best: &mut Option, cx: &ToolContext, req: &SnapRequest<'_>, flags: [bool; 3], axis: Axis, tol: f64) { for (item, dx) in shown_parent_items(cx.doc, req.spread) { let Some(rect) = item_target_rect(cx.selection, req, item) else { continue }; - offer_box(best, req.rect, flags, axis, shift_x(rect, dx), tol, cx.snap.align_edges, cx.snap.align_centers); + offer_box(best, req.rect, flags, axis, shift_x(rect, dx), tol, (cx.snap.align_edges, cx.snap.align_centers)); } } @@ -1280,7 +1287,7 @@ fn offer_line(best: &mut Option, moving: Rect, flags: [bool; 3], axis: Axi } } -fn offer_box(best: &mut Option, moving: Rect, flags: [bool; 3], axis: Axis, target: Rect, tol: f64, edges: bool, centers: bool) { +fn offer_box(best: &mut Option, moving: Rect, flags: [bool; 3], axis: Axis, target: Rect, tol: f64, (edges, centers): (bool, bool)) { let (m0, m_mid, m1) = axis_triple(moving, axis); let (t0, t_mid, t1) = axis_triple(target, axis); let [use_left, use_mid, use_right] = flags; diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index 56658f41e..f0503e912 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -212,6 +212,123 @@ fn underlines(cx: &ToolContext, ts: TextSel, r: Range) -> Vec<(Point, Poi out } +impl TypeTool { + fn pointer_down(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { + self.start = Some(ev.pos); + self.drawing = false; + self.selecting = None; + self.cell_drag = None; + self.unit = None; + self.border = None; + if ev.kind == PointerKind::Down + && let Some(b) = border_at(cx, ev.pos) + { + self.border = Some(b); + return vec![]; + } + // Text frames take the caret (locked ones too, as in InDesign); an empty frame + // becomes one, unless it's locked: then the press draws a new text frame over it. + if let Some((_, id)) = cx.hit(ev.pos) + && let Some(it) = cx.doc.item(id) + && !matches!(it.content, designcraft_doc::Content::Graphic(_) | designcraft_doc::Content::Group { .. }) + && (it.is_text_frame() || !it.locked) + { + let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); + let clicks = ev.click_count(); + // Whole rows/columns selected: wait to see whether this is a drag. + if clicks == 1 && !ev.mods.shift && whole_rows_or_cols(cx) { + self.cell_drag = Some((id.0, sp)); + return vec![]; + } + self.selecting = Some(id.0); + // Two presses select a word (or a run of spaces), three a line, four a + // paragraph, five the story. + if clicks >= 2 { + let unit = match clicks { + 2 => "word", + 3 => "line", + 4 => "paragraph", + _ => "story", + }; + self.unit = Some(unit); + return vec![Action::Exec("text.selectAt".into(), json!({"frame": id.0, "point": [sp.x, sp.y], "unit": unit}))]; + } + let cmd = if ev.mods.shift { "text.extendTo" } else { "text.placeCaret" }; + return vec![Action::Exec(cmd.into(), json!({"frame": id.0, "point": [sp.x, sp.y]}))]; + } + vec![] + } + + fn pointer_drag(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { + let Some(a) = self.start else { return vec![] }; + if let Some(b) = self.border.as_mut() { + // A click with a little wobble must not resize. + if !b.begun && (ev.pos - a).hypot() < cx.tol(3.0) { + return vec![]; + } + return b.drag(ev.pos, ev.mods.shift); + } + if self.cell_drag.is_some() { + return vec![]; + } + if let Some(fid) = self.selecting { + let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); + let mut p = json!({"frame": fid, "point": [sp.x, sp.y]}); + // After several clicks the drag grows the selection by whole units. + if let Some(unit) = self.unit { + p["unit"] = json!(unit); + } + return vec![Action::Exec("text.extendTo".into(), p)]; + } + if (ev.pos - a).hypot() < cx.tol(3.0) && !self.drawing { + return vec![]; + } + let Some((sr, sa)) = cx.layout.spread_at(a) else { return vec![] }; + // Drawn like the frame tools: the press point and the moving edges snap. + let sa = if cx.snap.any() { crate::snap::snap_point(cx, sr, sa).0 } else { sa }; + let (r, guides) = snapped_drag_rect(cx, sr, sa, cx.layout.to_spread(sr, ev.pos), ev.mods); + self.guides = guides; + let mut out = vec![]; + if !self.drawing { + self.drawing = true; + out.push(Action::Begin("Create Text Frame".into())); + } + out.push(Action::Preview( + "frame.create".into(), + json!({"spread": spread_json(sr), "shape": "rectangle", "content": "text", "rect": rect_json(r), "caret": true, "vertical": self.vertical}), + )); + out + } + + fn pointer_up(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { + self.guides.clear(); + let start = self.start.take(); + if let Some(b) = self.border.take() { + return if b.begun { vec![Action::Commit] } else { vec![] }; + } + if let Some((fid, from)) = self.cell_drag.take() { + let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); + let moved = start.is_some_and(|a| (ev.pos - a).hypot() >= cx.tol(3.0)); + return if moved { + vec![Action::Exec("table.dropCells".into(), json!({"frame": fid, "from": [from.x, from.y], "to": [sp.x, sp.y]}))] + } else { + vec![Action::Exec("text.placeCaret".into(), json!({"frame": fid, "point": [from.x, from.y]}))] + }; + } + if std::mem::take(&mut self.drawing) { + self.selecting = None; + return vec![Action::Commit]; + } + // Ends a press in text: drops dragged text, or is ignored by the engine. + if let Some(fid) = self.selecting.take() { + let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); + let moved = start.is_some_and(|a| (ev.pos - a).hypot() >= cx.tol(3.0)); + return vec![Action::Exec("text.release".into(), json!({"frame": fid, "point": [sp.x, sp.y], "moved": moved, "copy": ev.mods.alt}))]; + } + vec![] + } +} + impl Tool for TypeTool { fn id(&self) -> &'static str { if self.vertical { "verticalType" } else { "type" } @@ -219,121 +336,9 @@ impl Tool for TypeTool { fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { match ev.kind { - PointerKind::Down | PointerKind::DoubleClick => { - self.start = Some(ev.pos); - self.drawing = false; - self.selecting = None; - self.cell_drag = None; - self.unit = None; - self.border = None; - if ev.kind == PointerKind::Down - && let Some(b) = border_at(cx, ev.pos) - { - self.border = Some(b); - return vec![]; - } - // Text frames take the caret (locked ones too, as in InDesign); an empty frame - // becomes one, unless it's locked: then the press draws a new text frame over it. - if let Some((_, id)) = cx.hit(ev.pos) - && let Some(it) = cx.doc.item(id) - && !matches!(it.content, designcraft_doc::Content::Graphic(_) | designcraft_doc::Content::Group { .. }) - && (it.is_text_frame() || !it.locked) - { - let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); - let clicks = ev.click_count(); - // Whole rows/columns selected: wait to see whether this is a drag. - if clicks == 1 && !ev.mods.shift && whole_rows_or_cols(cx) { - self.cell_drag = Some((id.0, sp)); - return vec![]; - } - self.selecting = Some(id.0); - // Two presses select a word (or a run of spaces), three a line, four a - // paragraph, five the story. - if clicks >= 2 { - let unit = match clicks { - 2 => "word", - 3 => "line", - 4 => "paragraph", - _ => "story", - }; - self.unit = Some(unit); - return vec![Action::Exec("text.selectAt".into(), json!({"frame": id.0, "point": [sp.x, sp.y], "unit": unit}))]; - } - let cmd = if ev.mods.shift { "text.extendTo" } else { "text.placeCaret" }; - return vec![Action::Exec(cmd.into(), json!({"frame": id.0, "point": [sp.x, sp.y]}))]; - } - vec![] - } - PointerKind::Drag => { - let Some(a) = self.start else { return vec![] }; - if let Some(b) = self.border.as_mut() { - // A click with a little wobble must not resize. - if !b.begun && (ev.pos - a).hypot() < cx.tol(3.0) { - return vec![]; - } - return b.drag(ev.pos, ev.mods.shift); - } - if self.cell_drag.is_some() { - return vec![]; - } - if let Some(fid) = self.selecting { - let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); - let mut p = json!({"frame": fid, "point": [sp.x, sp.y]}); - // After several clicks the drag grows the selection by whole units. - if let Some(unit) = self.unit { - p["unit"] = json!(unit); - } - return vec![Action::Exec("text.extendTo".into(), p)]; - } - if (ev.pos - a).hypot() < cx.tol(3.0) && !self.drawing { - return vec![]; - } - let Some((sr, sa)) = cx.layout.spread_at(a) else { return vec![] }; - // Drawn like the frame tools: the press point and the moving edges snap. - let sa = if cx.snap.any() { crate::snap::snap_point(cx, sr, sa).0 } else { sa }; - let (r, guides) = snapped_drag_rect(cx, sr, sa, cx.layout.to_spread(sr, ev.pos), ev.mods); - self.guides = guides; - let mut out = vec![]; - if !self.drawing { - self.drawing = true; - out.push(Action::Begin("Create Text Frame".into())); - } - out.push(Action::Preview( - "frame.create".into(), - json!({"spread": spread_json(sr), "shape": "rectangle", "content": "text", "rect": rect_json(r), "caret": true, "vertical": self.vertical}), - )); - out - } - PointerKind::Up => { - self.guides.clear(); - let start = self.start.take(); - if let Some(b) = self.border.take() { - return if b.begun { vec![Action::Commit] } else { vec![] }; - } - if let Some((fid, from)) = self.cell_drag.take() { - let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); - let moved = start.is_some_and(|a| (ev.pos - a).hypot() >= cx.tol(3.0)); - return if moved { - vec![Action::Exec("table.dropCells".into(), json!({"frame": fid, "from": [from.x, from.y], "to": [sp.x, sp.y]}))] - } else { - vec![Action::Exec("text.placeCaret".into(), json!({"frame": fid, "point": [from.x, from.y]}))] - }; - } - if std::mem::take(&mut self.drawing) { - self.selecting = None; - return vec![Action::Commit]; - } - // Ends a press in text: drops dragged text, or is ignored by the engine. - if let Some(fid) = self.selecting.take() { - let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); - let moved = start.is_some_and(|a| (ev.pos - a).hypot() >= cx.tol(3.0)); - return vec![Action::Exec( - "text.release".into(), - json!({"frame": fid, "point": [sp.x, sp.y], "moved": moved, "copy": ev.mods.alt}), - )]; - } - vec![] - } + PointerKind::Down | PointerKind::DoubleClick => self.pointer_down(cx, ev), + PointerKind::Drag => self.pointer_drag(cx, ev), + PointerKind::Up => self.pointer_up(cx, ev), PointerKind::Move => vec![], } }