diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 58cbe8349..9831092da 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -291,6 +291,7 @@ const JAPANESE: &[(&str, &str)] = &[ ("Layer {}", "レイヤー{}"), ("Layer:", "レイヤー:"), ("Layer: {}", "レイヤー:{}"), + ("Layer Styles", "レイヤースタイル"), ("Learn", "学習"), ("Leave all attributes in \"{}\"", "すべての属性を「{}」に残す"), ("Licence: {}", "ライセンス:{}"), @@ -836,6 +837,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[ ("Layer {}", "图层 {}"), ("Layer:", "图层:"), ("Layer: {}", "图层:{}"), + ("Layer Styles", "图层样式"), ("Learn", "学习"), ("Leave all attributes in \"{}\"", "将所有属性保留在“{}”中"), ("Licence: {}", "许可:{}"), @@ -1569,6 +1571,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[ ("Layer {}", "圖層 {}"), ("Layer:", "圖層:"), ("Layer: {}", "圖層:{}"), + ("Layer Styles", "圖層樣式"), ("Learn", "學習"), ("Leave all attributes in \"{}\"", "將所有屬性保留在“{}”中"), ("Licence: {}", "許可:{}"), @@ -2305,6 +2308,7 @@ const UKRAINIAN: &[(&str, &str)] = &[ ("Layer {}", "Шар {}"), ("Layer:", "Шар:"), ("Layer: {}", "Шар: {}"), + ("Layer Styles", "Стилі шару"), ("Learn", "Навчання"), ("Leave all attributes in \"{}\"", "Залишити всі атрибути в «{}»"), ("Licence: {}", "Ліцензія: {}"), @@ -2856,6 +2860,7 @@ const PORTUGUESE: &[(&str, &str)] = &[ ("Layer {}", "Camada {}"), ("Layer:", "Camada:"), ("Layer: {}", "Camada: {}"), + ("Layer Styles", "Estilos de camada"), ("Learn", "Aprender"), ("Leave all attributes in \"{}\"", "Manter todos os atributos em \"{}\""), ("Licence: {}", "Licença: {}"), @@ -3308,6 +3313,7 @@ const CZECH: &[(&str, &str)] = &[ ("Layer {}", "Vrstva {}"), ("Layer:", "Vrstva:"), ("Layer: {}", "Vrstva: {}"), + ("Layer Styles", "Styly vrstvy"), ("Learn", "Výuka"), ("Leave all attributes in \"{}\"", "Ponechat všechny atributy v „{}“"), ("Licence: {}", "Licence: {}"), @@ -3865,6 +3871,7 @@ const SPANISH: &[(&str, &str)] = &[ ("Layer {}", "Capa {}"), ("Layer:", "Capa:"), ("Layer: {}", "Capa: {}"), + ("Layer Styles", "Estilos de capa"), ("Learn", "Aprender"), ("Leave all attributes in \"{}\"", "Dejar todos los atributos en \"{}\""), ("Licence: {}", "Licencia: {}"), diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 63a174cda..aff4ef2c8 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -3958,6 +3958,7 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], caps: LayerMenuCaps, } }) }); + ui.menu_button(crate::i18n::tr("Layer Styles"), |ui| layer_styles_submenu(ui, layers, &mut item)); 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"})); @@ -3972,6 +3973,31 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], caps: LayerMenuCaps, item(ui, "Delete", "edit.clear", json!({"layers": layers})); } +/// The layer context menu's Layer Styles submenu, matching the Layer ▸ Layer Styles application menu. +fn layer_styles_submenu(ui: &mut egui::Ui, layers: &[u64], item: &mut dyn FnMut(&mut egui::Ui, &str, &str, serde_json::Value)) { + item(ui, "Layer Style Options...", "layer.style.options", json!({})); + ui.separator(); + item(ui, "Convert to Editable Styles", "layer.style.convertToEditable", json!({"layers": layers})); + item(ui, "Show All", "layer.style.showAll", json!({"layers": layers})); + item(ui, "Remove All", "layer.style.removeAll", json!({"layers": layers})); + ui.separator(); + for (id, label) in effectcraft_engine::project::styles::STYLES { + let cmd = match *id { + "dropShadow" => "layer.style.dropShadow", + "innerShadow" => "layer.style.innerShadow", + "outerGlow" => "layer.style.outerGlow", + "innerGlow" => "layer.style.innerGlow", + "bevelEmboss" => "layer.style.bevelEmboss", + "satin" => "layer.style.satin", + "colorOverlay" => "layer.style.colorOverlay", + "gradientOverlay" => "layer.style.gradientOverlay", + "stroke" => "layer.style.stroke", + _ => continue, + }; + item(ui, label, cmd, json!({})); + } +} + /// 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)) { diff --git a/crates/ui-egui/tests/ui_blank_context_menus.rs b/crates/ui-egui/tests/ui_blank_context_menus.rs index 3c03149a2..20f7467cc 100644 --- a/crates/ui-egui/tests/ui_blank_context_menus.rs +++ b/crates/ui-egui/tests/ui_blank_context_menus.rs @@ -274,6 +274,31 @@ fn timeline_layer_context_menu_has_the_effects_submenu_and_applies_an_effect() { assert_eq!(names, ["Gaussian Blur"]); } +/// #1611: the layer context menu has Layer Styles between Blending Mode and Effects. +#[test] +fn timeline_layer_context_menu_has_layer_styles_submenu() { + let mut h = harness(); + let solid = h.state_mut().session.execute("layer.newSolid", json!({"name": "Solid", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap(); + h.run_steps(3); + let r = rect(&h, &format!("timeline.layer.{solid}.row")); + right_click(&mut h, pos2(r.min.x + 210.0, r.center().y)); + let blending = h.get_by_label("Blending Mode ⏵").rect(); + let styles = h.get_by_label("Layer Styles ⏵").rect(); + let effects = h.get_by_label("Effects ⏵").rect(); + assert!(blending.bottom() <= styles.top() && styles.bottom() <= effects.top(), "Layer Styles between Blending Mode and Effects"); + h.get_by_label("Layer Styles ⏵").click(); + h.run_steps(3); + assert!(h.query_by_label("Layer Style Options...").is_some()); + assert!(h.query_by_label("Drop Shadow").is_some()); + assert!(h.query_by_label("Stroke").is_some()); + h.get_by_label("Drop Shadow").click(); + h.run_steps(3); + assert_eq!(h.state().dialog, Some(Dialog::LayerStyles)); + let comp = h.state().session.active_comp().unwrap(); + let l = comp.layer(effectcraft_engine::project::LayerId(solid)).unwrap(); + assert!(l.layer_styles().is_some_and(|g| g.sub("dropShadow").is_some())); +} + /// #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] diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 6d35db827..5173e7691 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -23,7 +23,7 @@ feel and platform differences, and the 26.x interface changes are not in yet. | Nudging and modifiers | Arrow nudge (1 px, Shift 10 px), Alt/Option duplicates and slips, Shift constrains | 75% | #290 nudging fixed; #491 (Linux multi-layer edits) is closed | | Numeric entry | Scrubbable values, typed entry with Tab / Shift+Tab, angle revolutions, Escape cancels, non-finite rejected | 85% | `ui_angles`, `ui_inline_edit` (#93, #141, #361); rename arrows on Linux (#488) | | Effect Controls | After Effects' twirl-downs, angle dial, point crosshair, eyedropper, curves/levels editors, effect menu on right-click | 70% | `ui_effect_controls`, `ui_fx_editors`; 26.5 rebuilt this panel with icon controls and effect colour labels: labels exist (a colour bar on the effect header and on the effect's Timeline row, right-click Label menu, `effect.setLabel`), the rebuilt controls are not matched | -| Context menus | Layer, property, Project, Timeline empty area, work area, labels with colours | 80% | `ui_blank_context_menus`, `ui_project_rows`; #227, #290, #407 fixed; a comp's Project row menu has Composition Settings… (#1058) | +| Context menus | Layer, property, Project, Timeline empty area, work area, labels with colours | 80% | `ui_blank_context_menus`, `ui_project_rows`; #227, #290, #407 fixed; a comp's Project row menu has Composition Settings… (#1058); layer context menu has Layer Styles (#1611) | | Keyboard shortcuts | After Effects' defaults; Keyboard Shortcuts editor that rebinds and saves sets | 75% | Shortcut editor in Settings; #467 (adding shortcuts on Linux), #428 and #336 are closed; no shortcuts for scripts | | Drag and drop | Project → Timeline / viewer / New Comp / Render Queue; effects onto layers; files from the OS | 65% | `ui_drag_drop`; OS drops fail on Wayland (winit) and some Linux setups (#268); #552, #351 (multi-file drops) and #511 (crash dragging over the import dialog) are closed | | Text editing on canvas | Point and paragraph text, per-character styles, Character / Paragraph panels, OpenType and variable axes | 70% | `ui_text_edit`, `ui_font_search`; paragraph alignment inverted (#316); toolbar font options (#550); font search by typing (#583) is closed; the font menus' variable-font filter (26.3) landed (#741) |