#866: Layer > New > Text starts typing into Source Text

Problem: Layer > New > Text (menu, Ctrl+Alt+Shift+T, Timeline context menus)
created a layer with the placeholder "Text" and no edit session, so typed keys
ran shortcuts; users then renamed the layer (Enter) and the name, not the
Source Text, got their text.

Fix: as in After Effects, the UI dispatch runs layer.newText with an empty
text and an edit session (insertion point in the comp centre); the name
follows the typed text. engine.execute keeps the headless default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
vangel76andClaude Opus 5.5 committed 2026-10-11 17:46:01 +02:00
1 parent 9a7d3e7699
commit 1226576a92
3 files changed
+38 -2

No files matched your search

+6
View File
@@ -273,6 +273,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<Value, String> {
// 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(&params) {
return run_engine(app, ctx, id, json!({"text": "", "edit": true}));
}
if no_params(&params) && app.ui.focused == PanelKind::Project {
match id {
"edit.duplicate" => return run_engine(app, ctx, "project.duplicate", json!({})),
+2 -2
View File
@@ -2661,7 +2661,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
response.context_menu(|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!({})),
@@ -3457,7 +3457,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!({}));
+30
View File
@@ -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");
}
}