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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin KleinandClaude Opus 5.5 authored and GitHub committed 2026-11-06 17:19:21 +01:00
1 parent 549a4d0540
commit 9fc73122ea
4 files changed
+94

No files matched your search

+27
View File
@@ -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<effectcraft_project::ItemId>, 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<Value, String> {
// 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
}
+45
View File
@@ -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]
+2
View File
@@ -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();
@@ -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");
}