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"); +}