From eeedd6b540e229fdfba987a6b842e4e6cb48b2d1 Mon Sep 17 00:00:00 2001 From: Christopher Linnett Date: Wed, 28 Oct 2026 00:26:52 +0800 Subject: [PATCH] Text: duplicate during a text edit leaves no effect selected on the original (#2226) Duplicate passes over the property selection an open edit kept and copies the edited layer, but left that selection in place on the original. With the copy selected, the next Duplicate copied the original's effect, shape item or text animator, and Clear would have removed it. Ending the edit through Duplicate now clears that property selection along with it. --- crates/engine/src/cmd/edit.rs | 8 +++++++- crates/engine/src/tests/text_edit.rs | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 8d4bde60c..585e4459e 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -105,10 +105,11 @@ pub(crate) fn reid(layer: &mut Layer, next: &mut u64) { } fn duplicate(s: &mut Session, p: &Value) -> Result { + let editing = s.state.text_edit.is_some(); // Effects selected → duplicate them (Edit ▸ Duplicate in Effect Controls). An open text edit // comes first, as it does for Clear: opening the editor keeps the property selection, but // while typing, Duplicate means the edited layer, not an effect or item selected before. - if p.get("layers").is_none() && s.state.text_edit.is_none() { + if p.get("layers").is_none() && !editing { let fx = super::effect::selected_effects(s); if !fx.is_empty() { let mut out = vec![]; @@ -151,6 +152,11 @@ fn duplicate(s: &mut Session, p: &Value) -> Result { relink_copies(comp, &map); proj.next_id = next; st.selected_layers = created.clone(); + // The property selection the edit kept was passed over above and belongs to the + // originals: left in place, the next Duplicate or Clear would act on it, not the copies. + if editing { + st.selected_props.clear(); + } Ok(created) })?; Ok(json!(new.iter().map(|l| l.0).collect::>())) diff --git a/crates/engine/src/tests/text_edit.rs b/crates/engine/src/tests/text_edit.rs index 64ee2f0e3..1713cd459 100644 --- a/crates/engine/src/tests/text_edit.rs +++ b/crates/engine/src/tests/text_edit.rs @@ -1265,11 +1265,16 @@ fn duplicate_during_an_edit_copies_the_layer_not_a_selected_property_group() { 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!(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"); // 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 + 1, "{what}: and no layer"); + assert_eq!(layers(&s), before + 2, "{what}: and no layer"); } }