From 4101cef8b431cd28e4aef5066f4d5ba196ad496d Mon Sep 17 00:00:00 2001 From: Egon 5000 <210198019+elZanco@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:03:39 +0200 Subject: [PATCH] UI: add an Effects submenu to the layer context menu The layer context menu gains an Effects submenu between Blending Mode and Arrange, mirroring the After Effects layer menu: the effect registry grouped by category (sorted), synced with the Effect menu; picking one applies it to the selected layers. --- crates/ui-egui/src/panels/timeline.rs | 26 +++++++++++++++ .../ui-egui/tests/ui_blank_context_menus.rs | 33 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 72f10429..1b253e47 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -3579,6 +3579,7 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], vector: bool, action } }) }); + ui.menu_button(crate::i18n::tr("Effects"), |ui| effects_submenu(ui, layers, &mut item)); ui.menu_button(crate::i18n::tr("Arrange"), |ui| { item(ui, "Bring Layer to Front", "layer.arrange", json!({"layers": layers, "to": "front"})); item(ui, "Bring Layer Forward", "layer.arrange", json!({"layers": layers, "to": "forward"})); @@ -3592,6 +3593,31 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], vector: bool, action item(ui, "Delete", "edit.clear", json!({"layers": layers})); } +/// The layer context menu's Effects submenu: every effect from the registry grouped by category, +/// in the same order as the Effect menu's palette, so the two stay in sync automatically. +fn effects_submenu(ui: &mut egui::Ui, layers: &[u64], item: &mut dyn FnMut(&mut egui::Ui, &str, &str, serde_json::Value)) { + let mut cats: Vec<(&str, Vec<&'static effectcraft_engine::effects::EffectSpec>)> = Vec::new(); + for e in effectcraft_engine::effects::all() { + match cats.iter_mut().find(|(c, _)| *c == e.category) { + Some((_, v)) => v.push(e), + None => cats.push((e.category, vec![e])), + } + } + cats.sort_by_key(|(c, _)| c.to_ascii_lowercase()); + widgets::menu_scroll(ui, |ui| { + for (cat, mut fx) in cats { + fx.sort_by_key(|e| e.name.to_ascii_lowercase()); + ui.menu_button(cat, |ui| { + widgets::menu_scroll(ui, |ui| { + for e in fx { + item(ui, e.name, "effect.apply", json!({"effect": e.id, "layers": layers})); + } + }); + }); + } + }); +} + fn fmt_num(v: f64, d: usize) -> String { format!("{v:.d$}") } diff --git a/crates/ui-egui/tests/ui_blank_context_menus.rs b/crates/ui-egui/tests/ui_blank_context_menus.rs index fcbf3d09..c1533fa3 100644 --- a/crates/ui-egui/tests/ui_blank_context_menus.rs +++ b/crates/ui-egui/tests/ui_blank_context_menus.rs @@ -226,3 +226,36 @@ fn context_menu_rows_leave_no_gap_between_them() { } snapshot(&mut h, "project-rows"); } + +/// The layer context menu offers the whole effect palette as an "Effects" submenu (synced with +/// the Effect menu), between Blending Mode and Arrange; picking an effect applies it. +#[test] +fn timeline_layer_context_menu_has_the_effects_submenu_and_applies_an_effect() { + let mut h = harness(); + let layer = h.state_mut().session.execute("layer.newNull", json!({"name": "Null"})).unwrap()["layer"].as_u64().unwrap(); + h.run_steps(3); + let r = rect(&h, &format!("timeline.layer.{layer}.row")); + right_click(&mut h, pos2(r.min.x + 210.0, r.center().y)); + // The palette sits between Blending Mode and Arrange, as After Effects' layer menu does. + let blending = h.get_by_label("Blending Mode ⏵").rect(); + let effects = h.get_by_label("Effects ⏵").rect(); + let arrange = h.get_by_label("Arrange ⏵").rect(); + assert!(blending.bottom() <= effects.top() && effects.bottom() <= arrange.top(), "Effects between Blending Mode and Arrange"); + h.get_by_label("Effects ⏵").click(); + h.run_steps(3); + h.get_by_label("Blur & Sharpen ⏵").click(); + h.run_steps(3); + h.get_by_label("Gaussian Blur").click(); + h.run_steps(3); + let names: Vec = h + .state() + .session + .active_comp() + .unwrap() + .layer(effectcraft_engine::project::LayerId(layer)) + .unwrap() + .effects() + .map(|f| f.groups().map(|g| g.name.clone()).collect()) + .unwrap_or_default(); + assert_eq!(names, ["Gaussian Blur"]); +}