diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index a131da7de..8f74a43aa 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1108,6 +1108,8 @@ pub struct Session { /// While the active tool's actions run: their commands aren't "a command from outside the /// tool" ([`Session::after_command`]). pub(crate) in_tool_actions: bool, + /// While [`Session::preview`] runs its command: its edits are the interaction's own. + previewing: bool, /// Envelope Options with no envelope selected: the options and fidelity new envelopes get /// (`None`: the reference app's defaults, fidelity 50); not saved. pub(crate) envelope_defaults: Option<(vectorcraft_doc::live::EnvelopeOptions, f64)>, @@ -1183,6 +1185,7 @@ impl Session { batch_stash: None, recovery: Default::default(), in_tool_actions: false, + previewing: false, envelope_defaults: None, liquify_stroke: None, plane_widget_press: false, @@ -1479,6 +1482,11 @@ impl Session { /// Run `f` on a mutable copy of the active document. Outside an interaction this records one /// undo step labelled `label`; inside one it just mutates (the interaction commits later). pub fn edit(&mut self, label: &str, f: impl FnOnce(&mut Document, &mut Selection) -> Result) -> Result { + // An edit made while typing (the Transform panel) is a step of its own: inside the typing + // session, the next keystroke's preview would start again from before it (#1881). + if !self.previewing && cmd::typing_in_progress(self) { + cmd::settle_interaction(self)?; + } let st = self.doc_mut()?; let before = st.doc.clone(); let before_sel = st.selection.clone(); @@ -1579,6 +1587,11 @@ impl Session { // ---------- interactions (live drags) ---------- pub fn begin_interaction(&mut self, label: &str) -> Result<()> { + // Typing in progress is kept first: a drag or a dialog's preview would otherwise start + // from the snapshot taken before the typing, and drop it (#1881). + if label != "Typing" && cmd::typing_in_progress(self) { + cmd::settle_interaction(self)?; + } let st = self.doc_mut()?; if st.interaction.is_some() { return Ok(()); @@ -1618,7 +1631,9 @@ impl Session { self.pending_paint = None; let rw = cmd::distortcmds::perspective_rewrite(self, cmd, params); let (cmd, params) = rw.as_ref().map_or((cmd, params), |(c, p)| (c.as_str(), p)); + let outer = std::mem::replace(&mut self.previewing, true); let r = self.execute(cmd, params); + self.previewing = outer; let params = self.noted(params); let st = self.doc_mut()?; if let Some(it) = &mut st.interaction { diff --git a/crates/engine/src/tests_textedit.rs b/crates/engine/src/tests_textedit.rs index 68293247f..0458dcbf5 100644 --- a/crates/engine/src/tests_textedit.rs +++ b/crates/engine/src/tests_textedit.rs @@ -653,3 +653,61 @@ fn a_fill_the_selected_characters_have_records_no_undo_step() { s.execute("paint.setStroke", &json!({"color": "#ff0000"})).unwrap(); assert_eq!(history(&s), before); } + +fn left(s: &Session, id: NodeId) -> f64 { + s.doc().unwrap().doc.bounds_of(&[id], false).unwrap().x0 +} + +/// #1881: the Transform panel moving or scaling the type while it is being typed keeps both the +/// typing and the transformation, however the editing goes on. +#[test] +fn a_transform_panel_edit_while_typing_keeps_the_typing_and_the_transform() { + let mut s = session(); + let v = ViewInfo::default(); + let id = text(&mut s, "Hello"); + s.select_tool("type", v).unwrap(); + let b = s.doc().unwrap().doc.bounds_of(&[id], false).unwrap(); + click(&mut s, b.x1 - 1.0, b.center().y); + s.tool_text(" world", v).unwrap(); + s.execute("object.setBounds", &json!({"x": 400, "reference": 0})).unwrap(); + assert!((left(&s, id) - 400.0).abs() < 1e-6); + // Typing on. + s.tool_text("!", v).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "Hello world!"); + assert!((left(&s, id) - 400.0).abs() < 1e-6, "the move stays: {}", left(&s, id)); + s.execute("object.setBounds", &json!({"width": 300, "reference": 0, "proportional": true})).unwrap(); + let w = |s: &Session| s.doc().unwrap().doc.bounds_of(&[id], false).unwrap().width(); + assert!((w(&s) - 300.0).abs() < 1e-6); + key(&mut s, ToolKey::Escape, Mods::default()); + assert_eq!(obj(&s, id).plain_text(), "Hello world!"); + assert!((w(&s) - 300.0).abs() < 1e-6, "the scale stays: {}", w(&s)); + assert!((left(&s, id) - 400.0).abs() < 1e-6); + // Each its own step, the typing first. + let labels = undo_labels(&s); + let n = labels.len(); + assert_eq!(labels[n - 4], "Typing", "{labels:?}"); + assert_eq!(labels[n - 2], "Typing", "{labels:?}"); +} + +/// #1881: a dialog previewing a transformation while the type is being typed (Object › Transform +/// with Preview) starts from the typed text, and keeps it when it commits. +#[test] +fn a_previewed_transform_while_typing_keeps_the_typing() { + let mut s = session(); + let v = ViewInfo::default(); + let id = text(&mut s, "Hello"); + s.select_tool("type", v).unwrap(); + let b = s.doc().unwrap().doc.bounds_of(&[id], false).unwrap(); + click(&mut s, b.x1 - 1.0, b.center().y); + s.tool_text(" world", v).unwrap(); + s.begin_interaction("Move").unwrap(); + s.preview("object.setBounds", &json!({"x": 400, "reference": 0})).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "Hello world", "the preview starts from the typed text"); + s.commit_interaction().unwrap(); + assert_eq!(obj(&s, id).plain_text(), "Hello world"); + assert!((left(&s, id) - 400.0).abs() < 1e-6); + // Typing on starts a new session from the moved text. + s.tool_text("!", v).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "Hello world!"); + assert!((left(&s, id) - 400.0).abs() < 1e-6); +}