diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 27bd8290..fd67109f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -274,6 +274,12 @@ fn no_params(p: &Value) -> bool { /// Execute a UI or engine command by id. pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result { + // Layer ▸ New ▸ Text (menu, shortcut, context menus) puts an insertion point in the centre of + // the comp, as After Effects does, so typing goes into Source Text rather than running + // shortcuts (#866). `engine.execute` keeps the headless default. + if id == "layer.newText" && no_params(¶ms) { + return run_engine(app, ctx, id, json!({"text": "", "edit": true})); + } if no_params(¶ms) && app.ui.focused == PanelKind::Project { match id { "edit.duplicate" => return run_engine(app, ctx, "project.duplicate", json!({})), diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index a008b5be..e216176a 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -2672,7 +2672,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { crate::widgets::context_menu(&response, |ui| { let menu = ui.menu_button(crate::i18n::tr("New"), |ui| { for (key, label, command, params) in [ - ("text", "Text", "layer.newText", json!({"text": "Text"})), + ("text", "Text", "layer.newText", json!({})), ("solid", "Solid…", "layer.newSolid", json!({})), ("light", "Light…", "layer.newLight", json!({})), ("camera", "Camera…", "layer.newCamera", json!({})), @@ -3473,7 +3473,7 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], actions: &mut Vec<(S } }; ui.menu_button(crate::i18n::tr("New"), |ui| { - item(ui, "Text", "layer.newText", json!({"text": "Text"})); + item(ui, "Text", "layer.newText", json!({})); item(ui, "Solid…", "app.solidSettings", json!({})); item(ui, "Light…", "layer.newLight", json!({})); item(ui, "Camera…", "layer.newCamera", json!({})); diff --git a/crates/ui-egui/tests/ui_text_edit.rs b/crates/ui-egui/tests/ui_text_edit.rs index 1c2c7355..86734b0b 100644 --- a/crates/ui-egui/tests/ui_text_edit.rs +++ b/crates/ui-egui/tests/ui_text_edit.rs @@ -277,3 +277,33 @@ fn type_tool_drag_makes_paragraph_text_with_box_handles() { assert!(doc2.box_size.unwrap()[0] > bs[0] + 20.0, "{:?}", doc2.box_size); assert!(h.state().session.state.text_edit.is_some(), "still editing after resizing"); } + +/// #866: Layer ▸ New ▸ Text (menu or its shortcut) puts an insertion point in the comp, as After +/// Effects does, so typed keys reach Source Text (and the layer name follows) instead of running +/// shortcuts while the layer keeps the placeholder "Text". +#[test] +fn layer_new_text_menu_and_shortcut_start_typing_into_source_text() { + for via_shortcut in [false, true] { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Type", "width": 640, "height": 360, "duration": 2})).unwrap(); + s.execute("layer.newSolid", json!({"color": "#ffffff"})).unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(move |_| EffectcraftApp::new(s)); + h.state_mut().show_panel(PanelKind::Composition); + h.run_steps(3); + if via_shortcut { + key(&mut h, Key::T, Modifiers::COMMAND | Modifiers::ALT | Modifiers::SHIFT); + } else { + let ctx = h.ctx.clone(); + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "layer.newText", json!({})).unwrap(); + h.run_steps(2); + } + let (lid, text, _, _) = edited(&h); + assert_eq!(text, "", "no placeholder to type over (shortcut: {via_shortcut})"); + type_text(&mut h, "My First Project"); + key(&mut h, Key::Escape, Modifiers::NONE); + assert!(h.state().session.state.text_edit.is_none(), "Escape commits"); + let s = &h.state().session; + assert_eq!(layer_doc(s, LayerId(lid)).unwrap().text, "My First Project", "typed text is the Source Text"); + assert_eq!(s.active_comp().unwrap().layer(LayerId(lid)).unwrap().name, "My First Project", "the name follows the text"); + } +}