From 9fc73122ea2c9e7ce5a9fb433a75cc534e3c7ede Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Fri, 6 Nov 2026 17:19:21 +0100 Subject: [PATCH] Home screen: leave it when a menu command opens a composition (#3092) * Home screen: leave it when a menu command opens a composition File > Import > After Effects Project... from the Home screen imported the project (the toast said so), but the Home screen stayed on top, so it looked as if the import had done nothing. Only the Home screen's own buttons left it. run_engine now leaves the Home screen when a command succeeds and changes the active composition to one that is open, the rule scheduled script tasks already follow. Frontend events run afterwards, so Help > In-App Tutorials and New Project from Template still show it. A command that opens no composition (or fails) keeps it. Co-Authored-By: Claude Opus 5.5 * Home screen: also leave it for a project that replaces one with the same composition id, a picked file and the New Composition dialog Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/menus/frontend.rs | 27 +++++++++++++ crates/ui-egui/src/menus/tests.rs | 45 ++++++++++++++++++++++ crates/ui-egui/src/panels/dialogs.rs | 2 + crates/ui-egui/tests/it/ui_comp_presets.rs | 20 ++++++++++ 4 files changed, 94 insertions(+) diff --git a/crates/ui-egui/src/menus/frontend.rs b/crates/ui-egui/src/menus/frontend.rs index ddd08c2a9..bd1294330 100644 --- a/crates/ui-egui/src/menus/frontend.rs +++ b/crates/ui-egui/src/menus/frontend.rs @@ -30,15 +30,40 @@ fn clipboard_text(s: &effectcraft_engine::Session) -> String { } } +/// What a command is checked against for leaving the Home screen: the active composition and +/// the project (its epoch, which a replaced project steps even when its composition has the same +/// id). +type HomeMark = (Option, u64); + +/// The [`HomeMark`] of the session now. +fn home_mark(app: &EffectcraftApp) -> HomeMark { + (app.session.active_comp_id(), app.session.script_ui.project_epoch) +} + +/// A command that opened a composition (an imported After Effects project, a new or opened +/// project) leaves the Home screen, as the Home screen's own buttons do: it otherwise stayed +/// over the opened project. +fn leave_home_if_opened(app: &mut EffectcraftApp, before: HomeMark, ok: bool) { + if ok && app.session.active_comp_id().is_some() && home_mark(app) != before { + app.ui.start_screen = false; + } +} + +/// Runs engine command `id` from a menu, key or the control channel: the status line shows a +/// failure, Copy commands fill the clipboard, the frontend events it queued run, and a command +/// that opened a composition leaves the Home screen. pub(crate) fn run_engine(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result { // Events already queued belong to earlier invocations (`engine.execute` defers them). let queued = app.session.events.len(); + let before = home_mark(app); let mut r = app.session.execute(id, params).map_err(|e| e.to_string()); if let Err(e) = &r { app.ui.status = e.clone(); } else if COPY_COMMANDS.contains(&id) && app.session.state.text_edit.is_none() { ctx.copy_text(clipboard_text(&app.session)); } + // Frontend events below may show the Home screen again (Help ▸ In-App Tutorials). + leave_home_if_opened(app, before, r.is_ok()); let events = app.session.drain_events(); for (index, ev) in events.into_iter().enumerate() { match ev { @@ -770,10 +795,12 @@ fn run_with_picked(app: &mut EffectcraftApp, id: &str, key: &str, params: &Value } // Save (untitled) becomes Save As. let id = if id == "file.save" { "file.saveAs" } else { id }; + let before = home_mark(app); let r = app.session.execute(id, Value::Object(p)).map_err(|e| e.to_string()); if let Err(e) = &r { app.ui.status = e.clone(); } + leave_home_if_opened(app, before, r.is_ok()); r } diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index 0eaa957ac..b12483b0a 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -638,6 +638,51 @@ fn menu_items_cover_the_tree() { assert!(effectcraft_engine::menus::entries().len() > 500); } +/// A command run from the menu that opens a composition (File ▸ Import ▸ After Effects +/// Project… after Import Anyway, a new composition) leaves the Home screen: it stayed over the +/// imported project, which looked as if nothing was imported. A command that opens none keeps it. +#[test] +fn a_menu_command_that_opens_a_composition_leaves_the_home_screen() { + let ctx = egui::Context::default(); + let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); + app.ui.start_screen = true; + run_engine(&mut app, &ctx, "project.summary", json!({})).unwrap(); + assert!(app.ui.start_screen); + assert!(run_engine(&mut app, &ctx, "file.importAep", json!({"path": "/no/such/file.aep", "allowLosses": true})).is_err()); + assert!(app.ui.start_screen); + run_engine(&mut app, &ctx, "comp.new", json!({"name": "Opened", "width": 64, "height": 36, "duration": 1})).unwrap(); + assert!(!app.ui.start_screen); + // Asked for again (Home button, Help ▸ In-App Tutorials) it stays while the comp is open. + app.ui.start_screen = true; + run_engine(&mut app, &ctx, "layer.newSolid", json!({"color": "#ff0000"})).unwrap(); + assert!(app.ui.start_screen); +} + +/// Opening a project leaves the Home screen also when its composition has the id of the one open +/// before (the project changed, the id did not), from the menu and from a file dialog's answer. +#[test] +fn opening_a_project_leaves_the_home_screen_whatever_its_composition_id() { + let ctx = egui::Context::default(); + let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); + app.session.execute("comp.new", json!({"name": "Saved", "width": 64, "height": 36, "duration": 1})).unwrap(); + let dir = std::env::temp_dir().join(format!("ec-home-open-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("Home.ecproj").to_string_lossy().to_string(); + app.session.execute("file.saveAs", json!({"path": path})).unwrap(); + let comp = app.session.active_comp_id(); + // File ▸ Open with the path given (the menu path, `run_engine`). + app.ui.start_screen = true; + run_engine(&mut app, &ctx, "file.open", json!({"path": path})).unwrap(); + assert_eq!(app.session.active_comp_id(), comp, "the same composition id"); + assert!(!app.ui.start_screen, "File ▸ Open leaves the Home screen"); + // The path picked in a file dialog (`run_with_picked`). + app.ui.start_screen = true; + super::frontend::dialog_answered(&mut app, crate::DialogAnswer { command: "file.open".into(), key: "path", params: json!({}), paths: vec![path.clone()] }); + assert_eq!(app.session.active_comp_id(), comp); + assert!(!app.ui.start_screen, "a picked project leaves the Home screen"); + let _ = std::fs::remove_dir_all(&dir); +} + /// A successful command that reports per-item problems (`errors`) shows a compact, readable /// report in the status line: `file: error; …`, first three and a count, never raw JSON. #[test] diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 05b84ca2d..08f416623 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -503,6 +503,8 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis }; if !existing && r.is_ok() { app.dialog_state.last_new_comp = Some(app.dialog_state.comp.clone()); + // The new composition opens: it leaves the Home screen, as `run_engine` does. + app.ui.start_screen = false; } if let Err(e) = r { app.ui.status = e.to_string(); diff --git a/crates/ui-egui/tests/it/ui_comp_presets.rs b/crates/ui-egui/tests/it/ui_comp_presets.rs index 262d2183b..3fda8a8cc 100644 --- a/crates/ui-egui/tests/it/ui_comp_presets.rs +++ b/crates/ui-egui/tests/it/ui_comp_presets.rs @@ -196,3 +196,23 @@ fn composition_settings_refits_when_the_language_changes() { assert_eq!(view.width() > 600.0 - 36.0, wider, "{language}: the dialog is {} wide", view.width()); } } + +/// OK in the New Composition dialog opened over the Home screen (the native menu shows it there) +/// leaves the Home screen for the new composition; Cancel keeps it. +#[test] +fn new_composition_dialog_ok_leaves_the_home_screen() { + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(|_| EffectcraftApp::new(Session::default())); + h.run_steps(3); + h.state_mut().ui.start_screen = true; + h.state_mut().dialog = Some(Dialog::NewComp); + h.run_steps(3); + click(&mut h, "dialog.comp.cancel"); + assert_eq!(h.state().dialog, None); + assert!(h.state().ui.start_screen, "Cancel keeps the Home screen"); + h.state_mut().dialog = Some(Dialog::NewComp); + h.run_steps(3); + click(&mut h, "dialog.comp.ok"); + assert_eq!(h.state().dialog, None); + assert!(h.state().session.active_comp_id().is_some(), "a composition was made"); + assert!(!h.state().ui.start_screen, "OK leaves the Home screen"); +}