From 9932096122b9c56fb0aed9eb57859df55098164e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 00:37:29 -0800 Subject: [PATCH] Canvas: a dropped press keeps Type editing and the Crop Image box A dropped press on the Type tool no longer ends editing and switches tools: the type being edited stays in editing, with the caret and selection it had before the press, and the press does not count toward a double or triple click. A dropped handle or move press with Crop Image puts the box back as it was at the press and keeps earlier adjustments. --- crates/tools/src/cropimage.rs | 31 ++++++++++++++++++++++++++++++ crates/tools/src/text.rs | 3 +++ crates/tools/src/text/tests.rs | 35 ++++++++++++++++++++++++++++++++++ crates/tools/src/text/tool.rs | 17 +++++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/crates/tools/src/cropimage.rs b/crates/tools/src/cropimage.rs index 52f347e91..ff716bf17 100644 --- a/crates/tools/src/cropimage.rs +++ b/crates/tools/src/cropimage.rs @@ -184,6 +184,16 @@ impl Tool for CropImageTool { } } + /// A dropped press puts the box back as it was at the press; earlier changes to it stay. + fn drop_press(&mut self, cx: &ToolContext) -> Vec { + if let Some(Drag::Handle(_, from) | Drag::Move(_, from)) = self.drag.take() + && let Some((id, _, _)) = self.current(cx) + { + self.rect = Some((Some(id), from)); + } + vec![] + } + fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool { matches!(key, ToolKey::Enter | ToolKey::Escape) && self.current(cx).is_some() } @@ -368,6 +378,27 @@ mod tests { assert!(t.overlays(&cx).is_empty() && !t.claims_key(&cx, ToolKey::Enter)); } + #[test] + fn a_dropped_press_puts_the_box_back_as_it_was_at_the_press() { + let (d, s) = doc_with_image((50.0, 60.0)); + let p = paint(); + let cx = cx(&d, &s, &p); + let mut t = CropImageTool::default(); + drag(&mut t, &cx, (50.0, 60.0), (80.0, 70.0)); + assert_eq!(t.options()["rect"], json!([80.0, 70.0, 170.0, 90.0])); + // A handle pressed and dragged, then the press dropped: the box is the adjusted one. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 250.0, 160.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 130.0)); + assert!(t.drop_press(&cx).is_empty()); + assert!(!t.busy()); + assert_eq!(t.options()["rect"], json!([80.0, 70.0, 170.0, 90.0])); + // So is a moved box. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 110.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0)); + assert!(t.drop_press(&cx).is_empty()); + assert_eq!(t.options()["rect"], json!([80.0, 70.0, 170.0, 90.0])); + } + #[test] fn crop_box_option_requires_exactly_four_finite_numeric_values() { let mut tool = CropImageTool::default(); diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index 3e854231d..b1394c81d 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -189,6 +189,9 @@ pub struct TypeTool { snap: DrawSnap, /// The press landed in the edited text: dragging selects. selecting: bool, + /// The caret and anchor from before a press in type, kept until its release; a dropped press + /// puts them back. + before_press: Option<(usize, usize)>, /// Click counting for double/triple click (position of the last click, count). clicks: (Option, u8), /// Styled copy of the last copied range (pasting the same plain text keeps its styles). diff --git a/crates/tools/src/text/tests.rs b/crates/tools/src/text/tests.rs index ab3229809..3c1741172 100644 --- a/crates/tools/src/text/tests.rs +++ b/crates/tools/src/text/tests.rs @@ -603,6 +603,41 @@ fn a_drag_across_type_not_being_edited_selects_its_text() { assert_eq!((tool.editing, tool.sel()), (Some(id), (7, 7))); } +#[test] +fn a_dropped_press_keeps_the_type_in_editing() { + let (d, id, mut tool) = editing("Hello world"); + let (sel, p) = (Selection::default(), paint()); + let c = cx(&d, &sel, &p); + let t = TypeTool::text(&c, id).unwrap().clone(); + let lay = vectorcraft_text::layout(FontDb::global(), &t); + let pt = |i: usize| { + let (a, b) = vectorcraft_text::caret_position(&lay, i); + t.xf * a.midpoint(b) + }; + let (at, to) = (pt(3), pt(7)); + tool.pointer(&c, &PointerEvent::new(PointerKind::Down, at.x + 0.1, at.y)); + tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, to.x + 0.1, to.y)); + assert!(tool.busy()); + assert_eq!(tool.sel(), (3, 7)); + // The press is dropped: the caret and selection are as they were before it. + let out = tool.drop_press(&c); + assert!(!out.iter().any(|a| matches!(a, Action::SwitchTool(_)))); + assert!(!tool.busy()); + assert_eq!((tool.editing, tool.sel()), (Some(id), (0, 0))); + // The dropped press does not count toward a double or triple click. + assert_eq!(tool.clicks, (None, 0)); + tool.pointer(&c, &PointerEvent::new(PointerKind::Up, at.x + 0.1, at.y)); + assert_eq!((tool.editing, tool.sel()), (Some(id), (0, 0))); + // A Shift-press that extends the selection, dropped, leaves the caret where a click put it. + tool.pointer(&c, &PointerEvent::new(PointerKind::Down, at.x + 0.1, at.y)); + tool.pointer(&c, &PointerEvent::new(PointerKind::Up, at.x + 0.1, at.y)); + assert_eq!(tool.sel(), (3, 3)); + tool.pointer(&c, &PointerEvent { mods: SHIFT, ..PointerEvent::new(PointerKind::Down, to.x + 0.1, to.y) }); + assert_eq!(tool.sel(), (3, 7)); + tool.drop_press(&c); + assert_eq!((tool.editing, tool.sel()), (Some(id), (3, 3))); +} + #[test] fn select_all_and_styled_paste() { let (d, _, mut tool) = editing("abc"); diff --git a/crates/tools/src/text/tool.rs b/crates/tools/src/text/tool.rs index c983205da..9014f540e 100644 --- a/crates/tools/src/text/tool.rs +++ b/crates/tools/src/text/tool.rs @@ -29,6 +29,20 @@ impl Tool for TypeTool { fn claims_key(&self, _: &ToolContext, key: ToolKey) -> bool { key == ToolKey::Escape && self.editing.is_some() } + /// A dropped press drops only the selection drag or the type it was placing: the type being + /// edited stays in editing, with the caret and selection it had before the press. + fn drop_press(&mut self, _cx: &ToolContext) -> Vec { + if let Some((caret, anchor)) = self.before_press.take() { + (self.caret, self.anchor) = (caret, anchor); + } + self.selecting = false; + self.clicks = (None, 0); + self.press = None; + self.held = None; + self.drag = None; + self.snap.clear(); + vec![] + } fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { match ev.kind { PointerKind::Down => { @@ -36,6 +50,7 @@ impl Tool for TypeTool { let same = self.clicks.0.is_some_and(|q| (q - ev.pos).hypot() <= cx.tol(4.0)); let n = if same { self.clicks.1 + 1 } else { 1 }; self.clicks = (Some(ev.pos), n); + self.before_press = Some((self.caret, self.anchor)); let out = self.move_to(byte, ev.mods.shift); self.goal_x = None; if n >= 3 { @@ -51,6 +66,7 @@ impl Tool for TypeTool { // as it does in the type being edited. if let Some(out) = self.edit_at(cx, ev.pos) { self.selecting = true; + self.before_press = Some((self.caret, self.anchor)); return out; } self.press = Some((ev.pos, self.snap.press(cx, ev.pos, self.editing.as_slice(), None))); @@ -71,6 +87,7 @@ impl Tool for TypeTool { vec![] } PointerKind::Up => { + self.before_press = None; if self.selecting { self.selecting = false; return vec![];