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 }