From e228f712a9838915e7b5c68e61d7dd2222303c4f Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 16 Oct 2026 10:23:00 +0200 Subject: [PATCH] Window menu: choosing a checked panel closes it, as in After Effects (fixes #1630) --- crates/ui-egui/src/menus.rs | 15 ++++++++++++++- crates/ui-egui/src/native_menu.rs | 18 ++++++++++++++++++ crates/ui-egui/tests/ui_panels.rs | 20 ++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 8b5fd1c30..ba765739c 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1521,6 +1521,19 @@ pub(crate) fn panel_shown(app: &EffectcraftApp, p: PanelKind) -> bool { } } +/// The command a menu entry runs when chosen from a menu: Window ▸ closes a panel whose +/// entry is checked, as in After Effects (#1630). Shortcuts and agents calling `window.panel` +/// still only show the panel. +pub(crate) fn menu_choice(app: &EffectcraftApp, id: String, params: Value) -> (String, Value) { + if id == "window.panel" + && let Some(p) = params.get("panel").and_then(Value::as_str).and_then(PanelKind::from_name) + && panel_shown(app, p) + { + return ("window.closePanel".into(), params); + } + (id, params) +} + /// Check-mark state of frontend toggles (engine state is answered by the engine). pub(crate) fn entry_checked(app: &EffectcraftApp, e: &MenuEntry) -> Option { let v = &app.ui.viewer; @@ -1990,7 +2003,7 @@ pub fn menu_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui) { }); }); crate::menu_keys::handle(&ctx, &nav); - if let Some((id, params)) = clicked + if let Some((id, params)) = clicked.map(|(id, params)| menu_choice(app, id, params)) && let Err(e) = invoke(app, &ctx, &id, if params.is_null() { json!({}) } else { params }) { app.ui.status = e; diff --git a/crates/ui-egui/src/native_menu.rs b/crates/ui-egui/src/native_menu.rs index 3e0984494..ad30ded58 100644 --- a/crates/ui-egui/src/native_menu.rs +++ b/crates/ui-egui/src/native_menu.rs @@ -376,6 +376,7 @@ pub fn activate(app: &mut EffectcraftApp, ctx: &egui::Context, menu: &NativeMenu } } let params = if params.is_null() { json!({}) } else { params }; + let (command, params) = crate::menus::menu_choice(app, command, params); let r = crate::menus::invoke(app, ctx, &command, params); if let Err(e) = &r { app.ui.status = e.clone(); @@ -612,6 +613,23 @@ mod tests { assert!(spec2.items().iter().any(|i| i.command == "file.openRecent" && i.label == "Recent Test.ecproj")); } + /// #1630: a checked Window menu entry closes its panel; an unchecked one opens it. + #[test] + fn window_menu_entry_toggles_its_panel() { + use crate::dock::PanelKind; + let mut app = app(); + let ctx = egui::Context::default(); + let choose = |app: &mut EffectcraftApp| { + let spec = build(app); + let id = spec.items().into_iter().find(|i| i.command == "window.panel" && i.params["panel"] == "align").unwrap().id.clone(); + activate(app, &ctx, &spec, &id).unwrap(); + }; + choose(&mut app); + assert!(app.ui.dock.contains(PanelKind::Align)); + choose(&mut app); + assert!(!app.ui.dock.contains(PanelKind::Align)); + } + #[test] fn activation_dispatches_the_menu_command() { let mut app = app(); diff --git a/crates/ui-egui/tests/ui_panels.rs b/crates/ui-egui/tests/ui_panels.rs index 15439a02b..3cadafce2 100644 --- a/crates/ui-egui/tests/ui_panels.rs +++ b/crates/ui-egui/tests/ui_panels.rs @@ -67,6 +67,26 @@ fn window_menu_opens_the_new_panels() { } } +/// #1630: choosing a checked panel in the Window menu closes it, as in After Effects (it only +/// brought the panel to the front, so the check mark never went away); choosing it again opens it. +#[test] +fn window_menu_closes_a_checked_panel() { + let mut h = harness(); + let shown = |h: &Harness<'_, EffectcraftApp>| { + h.state().ui.dock.contains(PanelKind::Align) || h.state().ui.floating.iter().any(|f| f.panels.contains(&PanelKind::Align)) + }; + open(&mut h, "align"); + assert!(shown(&h)); + click(&mut h, "menu.Window"); + let at = h.query_by_label("✔ Align").expect("Window ▸ Align, checked").rect().center(); + click_at(&mut h, at); + assert!(!shown(&h), "Align closed"); + click(&mut h, "menu.Window"); + let at = h.query_by_label(" Align").expect("Window ▸ Align, unchecked").rect().center(); + click_at(&mut h, at); + assert!(shown(&h), "Align open again"); +} + #[test] fn scopes_switch_kind() { let mut h = harness();