diff --git a/crates/engine/src/tests/text_edit.rs b/crates/engine/src/tests/text_edit.rs index 1713cd459..f129479f6 100644 --- a/crates/engine/src/tests/text_edit.rs +++ b/crates/engine/src/tests/text_edit.rs @@ -7,6 +7,7 @@ use serde_json::json; use crate::Session; use crate::commands::text_edit::{MAX_IME_TEXT_BYTES, ime_stamp_revision, ime_surrounding, layer_doc}; +use crate::tests::three_d::layer_count; fn session(text: &str) -> (Session, u64) { let mut s = Session::default(); @@ -278,7 +279,7 @@ fn type_tool_layers_point_box_and_empty_cleanup() { let pos = l.props.prop("transform/position").unwrap().value.as_vec3(); assert_eq!([pos[0], pos[1]], [190.0, 110.0]); s.execute("text.endEdit", json!({})).unwrap(); - assert_eq!(s.active_comp().unwrap().layers.len(), 1); + assert_eq!(layer_count(&s), 1); // Resizing the box (layer space) is an attribute too. s.execute("layer.setText", json!({"layer": b, "box": [-100, -60, 200, 120]})).unwrap(); assert_eq!(doc(&s, b).box_size, Some([200.0, 120.0])); @@ -1209,14 +1210,13 @@ fn deselect_all_discards_an_empty_type_tool_layer() { s.state.text_edit.as_ref().is_some_and(|e| e.created && e.layer == effectcraft_project::LayerId(new)), "the Type tool opens the edit on its new, empty layer" ); - let count = |s: &Session| s.active_comp().map_or(0, |c| c.layers.len()); - let before = count(&s); + let before = layer_count(&s); s.execute("edit.deselectAll", json!({})).unwrap(); assert!(s.state.text_edit.is_none()); assert!(s.state.selected_layers.is_empty()); - assert_eq!(count(&s), before - 1, "a Type-tool layer left empty is removed, as text.endEdit does"); + assert_eq!(layer_count(&s), before - 1, "a Type-tool layer left empty is removed, as text.endEdit does"); s.execute("edit.undo", json!({})).unwrap(); - assert_eq!(count(&s), before, "that removal is one undo step of its own, as on text.endEdit"); + assert_eq!(layer_count(&s), before, "that removal is one undo step of its own, as on text.endEdit"); } #[test] @@ -1230,7 +1230,7 @@ fn duplicate_ends_an_edit_on_already_committed_text() { assert_eq!(doc(&s, t).text, "Hello", "the committed text is kept on the original"); assert_eq!(doc(&s, copy).text, "Hello", "and carried by the copy"); s.execute("edit.undo", json!({})).unwrap(); - assert_eq!(s.active_comp().map_or(0, |c| c.layers.len()), 1, "undo removes the copy"); + assert_eq!(layer_count(&s), 1, "undo removes the copy"); assert!(s.state.text_edit.is_none(), "undo does not reopen the edit"); } @@ -1256,25 +1256,24 @@ fn duplicate_during_an_edit_copies_the_layer_not_a_selected_property_group() { |s: &Session| s.active_comp().unwrap().layer(LayerId(owner)).unwrap().props.group(path).unwrap().groups().map(|g| g.uid).collect::>(); let item = items(&s)[0]; let n = items(&s).len(); - let layers = |s: &Session| s.active_comp().map_or(0, |c| c.layers.len()); - let before = layers(&s); + let before = layer_count(&s); s.execute("prop.select", json!({"layer": owner, "prop": item})).unwrap(); begin_edit_at(&mut s, t, 5); let r = s.execute("edit.duplicate", json!({})).unwrap(); assert!(s.state.text_edit.is_none(), "{what}: Duplicate ends the open edit"); assert_eq!(items(&s).len(), n, "{what}: the selected {what} is not duplicated"); - assert_eq!(layers(&s), before + 1, "{what}: the edited layer is"); + assert_eq!(layer_count(&s), before + 1, "{what}: the edited layer is"); assert_eq!(s.state.selected_layers, vec![LayerId(r[0].as_u64().unwrap())], "{what}: and its copy is selected"); // The selection the edit passed over goes with it: the next Duplicate copies the selected // copy rather than reaching back to the original's {what}. s.execute("edit.duplicate", json!({})).unwrap(); assert_eq!(items(&s).len(), n, "{what}: a second Duplicate leaves the original's {what} alone"); - assert_eq!(layers(&s), before + 2, "{what}: and copies the copy"); + assert_eq!(layer_count(&s), before + 2, "{what}: and copies the copy"); // With no edit open, the selected item keeps its precedence over the layers. s.execute("prop.select", json!({"layer": owner, "prop": item})).unwrap(); s.execute("edit.duplicate", json!({})).unwrap(); assert_eq!(items(&s).len(), n + 1, "{what}: outside an edit Duplicate copies the selected {what}"); - assert_eq!(layers(&s), before + 2, "{what}: and no layer"); + assert_eq!(layer_count(&s), before + 2, "{what}: and no layer"); } } @@ -1294,12 +1293,12 @@ fn duplicate_ends_the_open_edit_and_selects_the_copy() { // The edit ending is no undoable step of its own (as with text.endEdit): undo takes the // duplicate, it does not reopen the edit or undo the typing. s.execute("edit.undo", json!({})).unwrap(); - assert_eq!(s.active_comp().map_or(0, |c| c.layers.len()), 1, "undo removed the copy"); + assert_eq!(layer_count(&s), 1, "undo removed the copy"); assert_eq!(doc(&s, t).text, "Hello!", "the typed text stays committed"); assert!(s.state.text_edit.is_none(), "undo does not reopen the edit"); // With no open edit Duplicate is untouched by all of this: same behavior, no extra steps. s.execute("edit.duplicate", json!({"layers": [t]})).unwrap(); - assert_eq!(s.active_comp().map_or(0, |c| c.layers.len()), 2, "a plain duplicate still duplicates"); + assert_eq!(layer_count(&s), 2, "a plain duplicate still duplicates"); assert!(s.state.text_edit.is_none()); } @@ -1311,18 +1310,17 @@ fn duplicate_of_an_empty_type_tool_layer_discards_it_as_endedit_does() { s.state.text_edit.as_ref().is_some_and(|e| e.created && e.layer == effectcraft_project::LayerId(new)), "the Type tool opens the edit on its new, empty layer" ); - let count = |s: &Session| s.active_comp().map_or(0, |c| c.layers.len()); - let before = count(&s); + let before = layer_count(&s); s.execute("edit.duplicate", json!({})).unwrap(); assert!(s.state.text_edit.is_none(), "the edit ends the way text.endEdit ends it"); - assert_eq!(count(&s), before - 1, "a Type-tool layer left empty is discarded, not duplicated"); + assert_eq!(layer_count(&s), before - 1, "a Type-tool layer left empty is discarded, not duplicated"); assert!(s.state.selected_layers.is_empty(), "nothing is selected: the layer that would be copied is gone"); // The edit is committed before Duplicate resolves its layers, so naming the very layer // the empty edit was on addresses nothing: the commit still happened, the duplicate errors. let new2 = s.execute("layer.newText", json!({"text": "", "position": [90.0, 90.0], "edit": true})).unwrap()["layer"].as_u64().unwrap(); assert!(s.execute("edit.duplicate", json!({"layers": [new2]})).is_err(), "the named layer is gone once the edit commits"); assert!(s.state.text_edit.is_none(), "and the edit ended all the same"); - assert_eq!(count(&s), before - 1, "with the empty layer discarded, nothing left to duplicate"); + assert_eq!(layer_count(&s), before - 1, "with the empty layer discarded, nothing left to duplicate"); s.execute("edit.undo", json!({})).unwrap(); - assert_eq!(count(&s), before, "that removal is one undo step of its own, as on text.endEdit"); + assert_eq!(layer_count(&s), before, "that removal is one undo step of its own, as on text.endEdit"); } diff --git a/crates/engine/src/tests/three_d.rs b/crates/engine/src/tests/three_d.rs index 07a48af1c..dc28ae42a 100644 --- a/crates/engine/src/tests/three_d.rs +++ b/crates/engine/src/tests/three_d.rs @@ -26,7 +26,7 @@ fn auto_orient_command() { assert!(!effectcraft_render::three_d::camera::is_two_node(&s.active_comp().unwrap().layers[0])); } -fn layer_count(s: &Session) -> usize { +pub(crate) fn layer_count(s: &Session) -> usize { s.active_comp().unwrap().layers.len() } diff --git a/crates/ui-egui/src/menus/shortcuts.rs b/crates/ui-egui/src/menus/shortcuts.rs index 7e3dfcec2..f931667ea 100644 --- a/crates/ui-egui/src/menus/shortcuts.rs +++ b/crates/ui-egui/src/menus/shortcuts.rs @@ -136,8 +136,8 @@ pub fn chord_matches(session: &effectcraft_engine::Session, command: &str, key: bindings(session).into_iter().any(|(m, k, id, _)| id == command && k == key && mods_match(m, mods)) } -/// Whether a Deselect All chord matched by [`chord_matches`] may run while a text editor holds -/// the keyboard. Chords carrying Cmd/Ctrl/Alt are command chords (the editor treats them as +/// Whether a Deselect All or Duplicate chord matched by [`chord_matches`] may run while a text +/// editor holds the keyboard. Chords carrying Cmd/Ctrl/Alt are command chords (the editor treats them as /// such for undo and the rest), and function keys never type, so those forward from the editor; /// a chord that could instead produce a character — a bare or Shift-only letter, digit or /// punctuation key, as from a pathological bare-letter rebinding — stays with the editor so diff --git a/crates/ui-egui/tests/it/ui_text_edit.rs b/crates/ui-egui/tests/it/ui_text_edit.rs index a63631e8a..4fe647e54 100644 --- a/crates/ui-egui/tests/it/ui_text_edit.rs +++ b/crates/ui-egui/tests/it/ui_text_edit.rs @@ -580,7 +580,7 @@ fn type_tool_selected_text_preference_skips_ineligible_layers_and_falls_back_to_ } /// Opens a fresh Type-tool edit holding `text` and returns the harness with the new layer id, -/// so the Deselect All tests start from the same state. +/// so the Deselect All and Duplicate tests start from the same state. fn type_tool_edit(a: EffectcraftApp, text: &str) -> (Harness<'static, EffectcraftApp>, u64) { let mut h = harness(a); h.run_steps(3);