From c220c500e92dbb0726e51311c1eadfdfe19db23a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 02:48:17 -0800 Subject: [PATCH] Type: Done during a Cmd press does not save empty point type Done (pattern editing), Stop Editing Opacity Mask and Exit Isolation Mode end the Type tool's edit first, so point type a click placed and left empty is discarded. While a Cmd/Ctrl press had lent a selection tool, the Type tool was not the active tool, its edit was not ended, and the empty point type was saved into the pattern tile or the mask. The lent tool is now given back first. A drag the lent tool started is kept as its own step, with its last position applied when Real-time Drawing and Editing is off. --- crates/engine/src/tests/opacitymask.rs | 27 +++++++++- crates/engine/src/tests/pattern.rs | 68 +++++++++++++++++++++++++- crates/engine/src/tooling.rs | 17 ++++++- 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/tests/opacitymask.rs b/crates/engine/src/tests/opacitymask.rs index c2db4f368..206ef7e66 100644 --- a/crates/engine/src/tests/opacitymask.rs +++ b/crates/engine/src/tests/opacitymask.rs @@ -6,7 +6,7 @@ use serde_json::{Value, json}; use vectorcraft_geom::Affine; use vectorcraft_render::{RenderOptions, Renderer}; -use vectorcraft_tools::{PointerEvent, PointerKind}; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind}; use crate::cmd::maskedit::MASK_EDIT_LAYER; use crate::tests::{history, select_with_a_step_to_redo}; @@ -234,6 +234,31 @@ fn empty_point_type_is_not_saved_into_the_mask() { } } +/// The same while a Cmd/Ctrl press has lent the Selection tool to the Type tool: the command before +/// the release gives the Type tool back first, so the empty point type is still discarded. +#[test] +fn empty_point_type_is_not_saved_into_the_mask_during_a_cmd_press() { + for cmd in ["transparency.stopEditingOpacityMask", "object.exitIsolation"] { + let mut s = session(); + s.prefs.placeholder_text = false; + let obj = editing_mask(&mut s); + let v = ViewInfo::default(); + s.select_tool("type", v).unwrap(); + for kind in [PointerKind::Down, PointerKind::Up] { + s.pointer(&PointerEvent::new(kind, 250.0, 250.0), v).unwrap(); + } + let press = Mods { cmd: true, ..Mods::default() }; + s.pointer(&PointerEvent::new(PointerKind::Down, 280.0, 280.0).with_mods(press), v).unwrap(); + assert_eq!(s.tool_id(), "selection", "{cmd}: the press lends the Selection tool"); + s.execute(cmd, &json!({})).unwrap(); + assert_eq!(s.tool_id(), "type", "{cmd}: the Type tool is back"); + assert!(!s.tool_wants_text(), "{cmd}"); + s.pointer(&PointerEvent::new(PointerKind::Up, 280.0, 280.0).with_mods(press), v).unwrap(); + let art = &mask(&s, obj).unwrap().art; + assert!(art.children().is_none() && !matches!(art.kind, vectorcraft_doc::NodeKind::Text(_)), "{cmd}: the mask keeps only the square"); + } +} + /// Releasing the masks of a group and of an object inside it puts each mask's art directly above /// its object, as releasing one mask at a time does. #[test] diff --git a/crates/engine/src/tests/pattern.rs b/crates/engine/src/tests/pattern.rs index a23e621b9..544dcce96 100644 --- a/crates/engine/src/tests/pattern.rs +++ b/crates/engine/src/tests/pattern.rs @@ -5,7 +5,7 @@ use vectorcraft_color::Paint; use vectorcraft_doc::pattern::{PATTERN_EDIT_LAYER, RepeatKind, TileType}; use vectorcraft_doc::{Node, NodeKind}; use vectorcraft_geom::{Point, Rect}; -use vectorcraft_tools::{PointerEvent, PointerKind}; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind}; use crate::tests_menucmds::place_undecodable_png; use crate::*; @@ -134,6 +134,72 @@ fn empty_point_type_is_not_saved_into_the_pattern() { } } +/// The same while a Cmd/Ctrl press has lent the Selection tool to the Type tool: Done or Exit +/// Isolation Mode before the release gives the Type tool back first, so the empty point type is +/// still discarded, and the release that follows leaves the tile as it is. +#[test] +fn empty_point_type_is_not_saved_into_the_pattern_during_a_cmd_press() { + for cmd in ["object.pattern.done", "object.exitIsolation"] { + let mut s = session(); + s.prefs.placeholder_text = false; + let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + sel(&mut s, &[r]); + s.execute("object.pattern.make", &json!({"name": "P"})).unwrap(); + let v = ViewInfo::default(); + s.select_tool("type", v).unwrap(); + for kind in [PointerKind::Down, PointerKind::Up] { + s.pointer(&PointerEvent::new(kind, 200.0, 200.0), v).unwrap(); + } + let press = Mods { cmd: true, ..Mods::default() }; + s.pointer(&PointerEvent::new(PointerKind::Down, 300.0, 300.0).with_mods(press), v).unwrap(); + assert_eq!(s.tool_id(), "selection", "{cmd}: the press lends the Selection tool"); + s.execute(cmd, &json!({})).unwrap(); + assert_eq!(s.tool_id(), "type", "{cmd}: the Type tool is back"); + assert!(!s.tool_wants_text(), "{cmd}"); + s.pointer(&PointerEvent::new(PointerKind::Up, 300.0, 300.0).with_mods(press), v).unwrap(); + let art = &doc(&s).pattern("P").unwrap().art; + assert_eq!(art.len(), 1, "{cmd}: the tile keeps only the square"); + assert!(!matches!(art[0].kind, NodeKind::Text(_)), "{cmd}"); + } +} + +/// Done during a drag of the lent Selection tool keeps the drag as its own step before the tile is +/// saved, so no drag is left open for the release to end. With Real-time Drawing and Editing off, +/// the drag's last position is applied first, as the release would apply it. +#[test] +fn done_during_a_lent_selection_drag_keeps_the_drag_as_its_own_step() { + for real_time in [true, false] { + let mut s = session(); + s.prefs.placeholder_text = false; + s.execute("prefs.set", &json!({"key": "realTimeDrawing", "value": real_time})).unwrap(); + let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + sel(&mut s, &[r]); + s.execute("object.pattern.make", &json!({"name": "P"})).unwrap(); + let st = s.doc().unwrap(); + let b = st.doc.bounds_of(&st.selection.objects, false).expect("the tile art is selected"); + let c = b.center(); + let v = ViewInfo::default(); + s.select_tool("type", v).unwrap(); + for kind in [PointerKind::Down, PointerKind::Up] { + s.pointer(&PointerEvent::new(kind, 200.0, 200.0), v).unwrap(); + } + let press = Mods { cmd: true, ..Mods::default() }; + s.pointer(&PointerEvent::new(PointerKind::Down, c.x, c.y).with_mods(press), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, c.x + 20.0, c.y).with_mods(press), v).unwrap(); + assert!(s.in_interaction(), "{real_time}: the lent Selection tool drags the square"); + s.execute("object.pattern.done", &json!({})).unwrap(); + assert!(!s.in_interaction(), "{real_time}: the drag is kept before the tile is saved"); + assert_eq!(s.tool_id(), "type", "{real_time}"); + s.pointer(&PointerEvent::new(PointerKind::Up, c.x + 20.0, c.y).with_mods(press), v).unwrap(); + let labels = s.doc().unwrap().history.undo.iter().map(|e| e.label.as_str()).collect::>(); + assert_eq!(labels.last(), Some(&"Done Editing Pattern"), "{real_time}: {labels:?}"); + assert!(labels.contains(&"Move"), "{real_time}: {labels:?}"); + let art = &doc(&s).pattern("P").unwrap().art; + assert_eq!(art.len(), 1, "{real_time}: the tile keeps only the square"); + assert!(!matches!(art[0].kind, NodeKind::Text(_)), "{real_time}"); + } +} + #[test] fn cancel_new_pattern_removes_it() { let mut s = session(); diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index ee83e72d6..18e8da3d5 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -861,9 +861,22 @@ impl Session { /// End the Type tool's text editing before a command saves the art of an editing layer (pattern /// editing's Done, Stop Editing Opacity Mask): point type its click placed that is still empty is - /// discarded first, so it is not saved into the pattern tile or the mask. + /// discarded first, so it is not saved into the pattern tile or the mask. A selection tool that a + /// Cmd press lent is given back first, so the Type tool behind it ends its edit too; a drag of the + /// lent tool is kept as its own step, as its release would keep it. pub(crate) fn end_text_edit(&mut self) { - if self.in_tool_actions || !self.tool.wants_text() { + if self.in_tool_actions { + return; + } + if self.lender.is_some() { + if let Err(e) = self.apply_actions(vec![Action::Commit]) { + log::warn!("keeping the lent tool's drag before ending the text edit: {e}"); + } + if let Err(e) = self.give_back_tool(self.last_view) { + log::warn!("giving back the lent tool before ending the text edit: {e}"); + } + } + if !self.tool.wants_text() { return; } let acts = self.with_tool_cx(self.last_view, |t, cx| t.deactivate(cx));