From 7f1ad31a7bd43b672d6c19be760c5efa12bdca39 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 14 Oct 2026 11:32:15 +0700 Subject: [PATCH] Fix #1881: a Ctrl/Cmd drag while typing keeps what was typed With the Type tool editing, a press with Ctrl (Cmd on the Mac) lends the Selection tool for the drag. Its drag joined the open typing session, so its preview started from the document before the typing: the type moved or scaled and lost what had just been typed. The typing is now kept as its own undo step before the lent tool takes the press, as Undo already does, and typing goes on after the drag. --- crates/engine/src/cmd/mod.rs | 1 + crates/engine/src/tests_textedit.rs | 24 ++++++++++++++++++++++++ crates/engine/src/tooling.rs | 6 ++++++ 3 files changed, 31 insertions(+) diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 5fb39922..dab8c0c7 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -457,6 +457,7 @@ pub(crate) fn unique_name(base: &str, taken: impl Fn(&str) -> bool) -> String { (2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string()) } +pub(crate) use edit::{settle_interaction, typing_in_progress}; pub(crate) use vectorcraft_doc::range::parse_range; /// A date param in Unix seconds that defaults to the clock: `None` when absent (now, see diff --git a/crates/engine/src/tests_textedit.rs b/crates/engine/src/tests_textedit.rs index a306f953..68293247 100644 --- a/crates/engine/src/tests_textedit.rs +++ b/crates/engine/src/tests_textedit.rs @@ -150,6 +150,30 @@ fn undo_while_typing_takes_back_only_the_typing_and_redo_returns_it() { assert_eq!(obj(&s, id).plain_text(), "abc!"); } +/// #1881: a Cmd/Ctrl drag while typing (the Selection tool lent to the Type tool) moves the type +/// and keeps what was typed, and typing goes on after it. +#[test] +fn a_cmd_drag_while_typing_moves_the_type_and_keeps_the_typing() { + let mut s = session(); + let v = ViewInfo::default(); + s.select_tool("type", v).unwrap(); + click(&mut s, 200.0, 200.0); + let id = s.doc().unwrap().selection.objects[0]; + s.tool_text("Hello", v).unwrap(); + let before = s.doc().unwrap().doc.bounds_of(&[id], false).unwrap(); + let (at, cmd) = (before.center(), Mods { cmd: true, ..Default::default() }); + for (kind, dx) in [(PointerKind::Down, 0.0), (PointerKind::Drag, 100.0), (PointerKind::Up, 100.0)] { + s.pointer(&PointerEvent::new(kind, at.x + dx, at.y).with_mods(cmd), v).unwrap(); + } + assert_eq!(obj(&s, id).plain_text(), "Hello", "the typing is kept"); + let moved = s.doc().unwrap().doc.bounds_of(&[id], false).unwrap(); + assert!(moved.x0 - before.x0 > 50.0, "the type moved: {before:?} → {moved:?}"); + s.tool_text("!", v).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "Hello!"); + let after = s.doc().unwrap().doc.bounds_of(&[id], false).unwrap(); + assert!((after.x0 - moved.x0).abs() < 1e-6, "and stays where it was moved: {after:?}"); +} + #[test] fn undo_during_a_drag_takes_back_only_the_drag() { let mut s = session(); diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index dcb6b3a2..d67edda7 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -115,6 +115,12 @@ impl Session { return *ev; } let Some(id) = self.lent_tool(self.tool.id()) else { return *ev }; + // Typing in progress is kept as its own step first: the lent tool's drag previews from the + // document its interaction started from, which would drop the typing (#1881). + if crate::cmd::typing_in_progress(self) { + // Fails only without a document, and then there is no typing to keep. + let _ = crate::cmd::settle_interaction(self); + } let lent = self.make_tool(id); self.lender = Some(std::mem::replace(&mut self.tool, lent)); PointerEvent { mods: Mods { cmd: false, ..ev.mods }, ..*ev }