diff --git a/crates/ui-egui/src/menus/entries.rs b/crates/ui-egui/src/menus/entries.rs index 8fee44a72..d7b5d56ca 100644 --- a/crates/ui-egui/src/menus/entries.rs +++ b/crates/ui-egui/src/menus/entries.rs @@ -119,7 +119,8 @@ pub(crate) fn entry_enabled(app: &EffectcraftApp, e: &MenuEntry) -> bool { } } if e.command == "edit.clear" && app.ui.focused == PanelKind::Project { - return app.session.is_enabled("project.delete"); + // An open text edit stands down: Clear must not delete the comp being typed into (#2227). + return app.session.state.text_edit.is_none() && app.session.is_enabled("project.delete"); } if e.command == "edit.clear" && app.ui.focused == PanelKind::RenderQueue { return !app.session.is_rendering() && !app.session.project.render_queue.is_empty(); diff --git a/crates/ui-egui/src/menus/invoke.rs b/crates/ui-egui/src/menus/invoke.rs index 6c4863033..23065067f 100644 --- a/crates/ui-egui/src/menus/invoke.rs +++ b/crates/ui-egui/src/menus/invoke.rs @@ -232,8 +232,13 @@ pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: V return run_engine(app, ctx, "edit.pasteSvg", json!({"svg": svg})); } } - // Clear follows panel focus, including the Edit menu and keyboard shortcut. + // Clear follows panel focus, including the Edit menu and keyboard shortcut. An open text + // edit stands down: the user is working in a composition, and the Project-panel redirect + // would delete it without asking when nothing uses it (#2227). if id == "edit.clear" && params.as_object().is_some_and(|p| p.is_empty()) && app.ui.focused == PanelKind::Project { + if app.session.state.text_edit.is_some() { + return Ok(Value::Null); + } // Through `invoke`, so deleting items that compositions use still asks first (M3.16). return invoke(app, ctx, "project.delete", json!({})); } diff --git a/crates/ui-egui/tests/it/ui_project_delete.rs b/crates/ui-egui/tests/it/ui_project_delete.rs index f217c29db..2a395cf4b 100644 --- a/crates/ui-egui/tests/it/ui_project_delete.rs +++ b/crates/ui-egui/tests/it/ui_project_delete.rs @@ -364,3 +364,29 @@ fn a_cut_is_read_with_the_modifiers_where_it_is_in_the_frame() { assert_eq!(h.state().session.project.item(solid).is_none(), deletes, "case {n}: {events:?} after {before:?}"); } } + +/// An open text edit stands down the Project-panel Clear redirect: the composition being typed +/// into must not be deleted out from under it, with no confirmation when nothing uses it (#2227). +#[test] +fn clear_with_the_project_focused_stands_down_during_a_text_edit() { + let (mut h, _, _) = harness(); + let working = h.state().session.active_comp_id().unwrap(); + // the composition being typed into is also the Project panel's selection, as when it was opened + click(&mut h, &format!("project.item.{}.name", working.0), Modifiers::NONE); + let text = h.state_mut().session.execute("layer.newText", json!({"text": "Title", "size": 80})).unwrap()["layer"].as_u64().unwrap(); + h.state_mut().session.execute("text.edit", json!({"layer": text, "created": true})).unwrap(); + h.state_mut().ui.focused = PanelKind::Project; + h.run_steps(2); + assert!(h.state().session.state.project_selection.contains(&working), "the comp is the project selection"); + let project = h.state().session.project.clone(); + + // Edit > Clear (the menu, or the control channel) dispatches through `invoke` + let ctx = h.ctx.clone(); + let cleared = effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "edit.clear", json!({})); + h.run_steps(3); + + cleared.expect("Clear stands down during a text edit"); + assert!(h.state().session.project.item(working).is_some(), "the composition being typed into survives"); + assert_eq!(h.state().session.project, project, "nothing was deleted"); + assert!(h.state().session.state.text_edit.is_some(), "the text edit stays open"); +}