From 3174e68d0eeb093fa9d6d6c9af97131d1c0d50f9 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 14:43:18 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Timeline:=20Markers=20=E2=96=B8=20Add=20Mar?= =?UTF-8?q?ker=20in=20the=20layer=20context=20menu=20(#976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-clicking a layer offered no way to mark it. The layer context menu now has After Effects' Markers submenu with Add Marker (layer.addMarker, as Layer ▸ Markers ▸ Add Marker), which marks the right-clicked layer, or the selection it belongs to, at the current time. The submenu title reuses the menu catalogs' "Markers" translation in the panel catalogs. --- crates/ui-egui/src/i18n/ui.rs | 6 ++++ crates/ui-egui/src/panels/timeline.rs | 3 ++ .../ui-egui/tests/ui_blank_context_menus.rs | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index f31d0951..3a49acad 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -295,6 +295,7 @@ const JAPANESE: &[(&str, &str)] = &[ ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OR Apache-2.0 • フォント:Inter、JetBrains Mono (OFL)"), ("Magnification ratio popup", "拡大率のポップアップ"), ("Make Comp Size", "コンポジションサイズにする"), + ("Markers", "マーカー"), ("Masks", "マスク"), ("Maximize Panel", "パネルを最大化"), ("Media Replacement", "メディアの置き換え"), @@ -742,6 +743,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT 或 Apache-2.0 • 字体:Inter、JetBrains Mono(OFL)"), ("Magnification ratio popup", "放大倍率弹出菜单"), ("Make Comp Size", "设为合成长宽"), + ("Markers", "标记"), ("Masks", "蒙版"), ("Maximize Panel", "最大化面板"), ("Media Replacement", "媒体替换"), @@ -1277,6 +1279,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT 或 Apache-2.0 • 字型:Inter、JetBrains Mono(OFL)"), ("Magnification ratio popup", "放大倍率快顯選單"), ("Make Comp Size", "設為合成長寬"), + ("Markers", "標記"), ("Masks", "遮色片"), ("Maximize Panel", "最大化面板"), ("Media Replacement", "媒體替換"), @@ -1815,6 +1818,7 @@ const UKRAINIAN: &[(&str, &str)] = &[ ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OR Apache-2.0 • Шрифти: Inter, JetBrains Mono (OFL)"), ("Magnification ratio popup", "Меню коефіцієнта збільшення"), ("Make Comp Size", "За розміром композиції"), + ("Markers", "Маркери"), ("Masks", "Маски"), ("Maximize Panel", "Розгорнути панель"), ("Media Replacement", "Заміна матеріалу"), @@ -2351,6 +2355,7 @@ const PORTUGUESE: &[(&str, &str)] = &[ ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OU Apache-2.0 • Fontes: Inter, JetBrains Mono (OFL)"), ("Magnification ratio popup", "Menu de proporção de ampliação"), ("Make Comp Size", "Ajustar ao tamanho da composição"), + ("Markers", "Marcadores"), ("Masks", "Máscaras"), ("Maximize Panel", "Maximizar painel"), ("Media Replacement", "Substituição de mídia"), @@ -2795,6 +2800,7 @@ const CZECH: &[(&str, &str)] = &[ ("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OR Apache-2.0 • Písma: Inter, JetBrains Mono (OFL)"), ("Magnification ratio popup", "Nabídka míry přiblížení"), ("Make Comp Size", "Nastavit velikost kompozice"), + ("Markers", "Značky"), ("Masks", "Masky"), ("Maximize Panel", "Maximalizovat panel"), ("Media Replacement", "Nahrazení médií"), diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 72f10429..8f4a6be7 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -3569,6 +3569,9 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], vector: bool, action item(ui, "Freeze Frame", "layer.freezeFrame", json!({"layers": layers})); item(ui, "Freeze on Last Frame", "layer.freezeOnLastFrame", json!({"layers": layers})); }); + ui.menu_button(crate::i18n::tr("Markers"), |ui| { + item(ui, "Add Marker", "layer.addMarker", json!({"layers": layers})); + }); ui.menu_button(crate::i18n::tr("Blending Mode"), |ui| { widgets::menu_scroll(ui, |ui| { for m in BlendMode::ALL { diff --git a/crates/ui-egui/tests/ui_blank_context_menus.rs b/crates/ui-egui/tests/ui_blank_context_menus.rs index fcbf3d09..e6fc87b2 100644 --- a/crates/ui-egui/tests/ui_blank_context_menus.rs +++ b/crates/ui-egui/tests/ui_blank_context_menus.rs @@ -226,3 +226,32 @@ fn context_menu_rows_leave_no_gap_between_them() { } snapshot(&mut h, "project-rows"); } + +/// #976: a layer's context menu has After Effects' Markers ▸ Add Marker, which marks the layer that +/// was right-clicked (not the one selected before, and not the composition) at the current time. +#[test] +fn layer_context_menu_adds_a_layer_marker() { + let mut h = harness(); + let solid = |h: &mut Harness<'_, EffectcraftApp>, name: &str| -> u64 { + h.state_mut().session.execute("layer.newSolid", json!({"name": name, "color": "#406080"})).unwrap()["layer"].as_u64().unwrap() + }; + let (below, top) = (solid(&mut h, "Below"), solid(&mut h, "Top")); + h.state_mut().session.execute("layer.select", json!({"layers": [below]})).unwrap(); + h.state_mut().session.execute("time.set", json!({"time": 1.5})).unwrap(); + h.run_steps(3); + let r = rect(&h, &format!("timeline.layer.{top}.row")); + right_click(&mut h, pos2(r.min.x + 210.0, r.center().y)); + h.get_by_label("Markers ⏵").click(); + h.run_steps(3); + snapshot(&mut h, "layer-markers"); + h.get_by_label("Add Marker").click(); + h.run_steps(3); + let now = h.state().session.time().seconds(); + let comp = h.state().session.active_comp().unwrap(); + let markers = |id: u64| -> Vec { comp.layer(effectcraft_engine::project::LayerId(id)).unwrap().markers.iter().map(|m| m.time.seconds()).collect() }; + assert_eq!(markers(top), vec![now]); + assert!((now - 1.5).abs() < 0.01, "at the current time: {now}"); + assert!(markers(below).is_empty(), "{:?}", markers(below)); + assert!(comp.markers.is_empty(), "no composition marker"); + assert_eq!(h.state().session.history.undo.last().map(|u| u.0.as_str()), Some("Add Marker")); +} From 424dcab40b8e5119f91ae40f02a0395bb498e3e8 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Mon, 12 Oct 2026 14:43:18 +0200 Subject: [PATCH 2/2] Shortcuts: Numpad * marks the selected layers (#976) Numpad * always added a composition marker. As in After Effects, it now adds a layer marker to the selected layers at the current time, and a composition marker only when no layer is selected. --- crates/ui-egui/src/menus.rs | 5 +++++ crates/ui-egui/tests/ui_timeline_zoom.rs | 27 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index b10b0e31..dda1b617 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1744,6 +1744,11 @@ fn handle_shortcuts_impl(app: &mut EffectcraftApp, ctx: &egui::Context, tab_only // comes as text. if !tab_only && clipboard && app.dialog.is_none() && ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(t) if t == "*"))) { let bound = app.session.shortcuts().bindings().into_iter().find(|(k, _)| *k == "Num*").map(|(_, b)| (b.command.clone(), b.params.clone())); + // As in After Effects, it marks the selected layers; only with none selected does it add + // a composition marker (#976). + let bound = bound.map(|(id, params)| { + if id == "comp.addMarker" && !app.session.state.selected_layers.is_empty() { ("layer.addMarker".to_owned(), params) } else { (id, params) } + }); if let Some((id, params)) = bound && let Err(e) = invoke(app, ctx, &id, if params.is_null() { json!({}) } else { params }) { diff --git a/crates/ui-egui/tests/ui_timeline_zoom.rs b/crates/ui-egui/tests/ui_timeline_zoom.rs index 2c802600..601bcd6f 100644 --- a/crates/ui-egui/tests/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/ui_timeline_zoom.rs @@ -333,13 +333,38 @@ fn dragging_out_of_the_marker_bin_adds_a_comp_marker() { assert_eq!(markers(&h), vec![13.0]); let history = &h.state().session.history.undo; assert_eq!((history.len(), history.last().map(|u| u.0.as_str())), (undo + 1, Some("Add Marker")), "one undo step"); - // Numpad * (it arrives as text): Add Marker at the current time. + // Numpad * (it arrives as text) with no layer selected: Add Marker at the current time. + h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap(); h.state_mut().session.execute("time.set", json!({"time": 20.0})).unwrap(); h.event(Event::Text("*".into())); h.run_steps(2); assert_eq!(markers(&h), vec![13.0, 20.0]); } +/// #976: as in After Effects, Numpad * marks the selected layers at the current time; only with +/// no layer selected does it add a composition marker. +#[test] +fn numpad_star_marks_the_selected_layer() { + let (mut h, layer) = harness(); + let comp_markers = + |h: &Harness<'_, EffectcraftApp>| -> Vec { h.state().session.active_comp().unwrap().markers.iter().map(|m| m.time.seconds()).collect() }; + let layer_markers = |h: &Harness<'_, EffectcraftApp>| -> Vec { + let comp = h.state().session.active_comp().unwrap(); + comp.layer(effectcraft_engine::project::LayerId(layer)).unwrap().markers.iter().map(|m| m.time.seconds()).collect() + }; + let star = |h: &mut Harness<'_, EffectcraftApp>, time: f64| { + h.state_mut().session.execute("time.set", json!({"time": time})).unwrap(); + h.event(Event::Text("*".into())); + h.run_steps(2); + }; + h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap(); + star(&mut h, 2.0); + assert_eq!((layer_markers(&h), comp_markers(&h)), (vec![2.0], vec![])); + h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap(); + star(&mut h, 3.0); + assert_eq!((layer_markers(&h), comp_markers(&h)), (vec![2.0], vec![3.0])); +} + /// #468: zoomed in, a CTI scrub held past the right end of the ruler scrolls the timeline (the /// CTI rides the edge), past the left end scrolls it back; during playback the view pages to /// keep the CTI in sight.