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.
This commit is contained in:
Egon 5000 2026-10-12 23:03:39 +02:00
parent 470ea7a3af
commit 4101cef8b4
2 changed files with 59 additions and 0 deletions

View File

@ -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$}")
}

View File

@ -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<String> = 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"]);
}