diff --git a/crates/ui-egui/src/ribbon/animations.rs b/crates/ui-egui/src/ribbon/animations.rs index 67f0bbd00..d43801603 100644 --- a/crates/ui-egui/src/ribbon/animations.rs +++ b/crates/ui-egui/src/ribbon/animations.rs @@ -1,4 +1,3 @@ -use super::transitions::option_label; use super::{ Align2, AnimClass, CornerRadius, Icon, Rect, Sense, SlideApp, Tokens, Ui, big, big_button, big_button_ex, dropdown, enabled, group, icons, json, pos2, rows, run, theme, vec2, widgets, @@ -81,12 +80,13 @@ pub(super) fn animations(app: &mut SlideApp, ui: &mut Ui) { } }); let effect_rows = cur.as_ref().map(|c| animation_effect_rows(&c.effect, c.class, Some(c.option.as_str()))).unwrap_or_default(); + let effect = cur.as_ref().map(|c| c.effect.as_str()).unwrap_or_default(); let build = cur.as_ref().map(|c| text_build_id(c.text_build)); let build_rows = marked_options(&["asOne", "byParagraph"], build); let r = big_button(ui, Icon::EffectOptions, "Effect\nOptions", !effect_rows.is_empty()); egui::Popup::menu(&r).show(|ui| { for &(o, on) in &effect_rows { - if ui.selectable_label(on, option_label(o)).clicked() { + if ui.selectable_label(on, animation_option_label(effect, o)).clicked() { run(app, "animation.options", json!({"option": o})); ui.close(); } @@ -216,6 +216,70 @@ pub(super) fn animation_effect_rows(effect: &str, class: AnimClass, current: Opt marked_options(opts, current) } +fn animation_option_label(effect: &str, option: &str) -> String { + let direction = match option { + "b" => Some("From Bottom"), + "lb" => Some("From Bottom-Left"), + "l" => Some("From Left"), + "lt" => Some("From Top-Left"), + "t" => Some("From Top"), + "rt" => Some("From Top-Right"), + "r" => Some("From Right"), + "rb" => Some("From Bottom-Right"), + _ => None, + }; + if matches!(effect, "fly" | "flyOut" | "wipe" | "wipeOut" | "peek" | "peekOut" | "crawl" | "crawlOut") + && let Some(label) = direction + { + if matches!(effect, "flyOut" | "peekOut" | "crawlOut") { + return label.replacen("From", "To", 1); + } + return label.into(); + } + + match (effect, option) { + ("float" | "floatOut", "u") => "Up".into(), + ("float" | "floatOut", "d") => "Down".into(), + ("split" | "splitOut", "horzIn") => "Horizontal In".into(), + ("split" | "splitOut", "horzOut") => "Horizontal Out".into(), + ("split" | "splitOut", "vertIn") => "Vertical In".into(), + ("split" | "splitOut", "vertOut") => "Vertical Out".into(), + ("shape" | "shapeOut", o) if o.ends_with("In") => format!("{} In", title_first(shape_option_name(o))), + ("shape" | "shapeOut", o) if o.ends_with("Out") => format!("{} Out", title_first(shape_option_name(o))), + ("wheel" | "wheelOut", "1") => "1 Spoke".into(), + ("wheel" | "wheelOut", n) => format!("{n} Spokes"), + ("box" | "boxOut" | "circle" | "diamond" | "diamondOut", "in") => format!("{} In", title_first(effect.trim_end_matches("Out"))), + ("box" | "boxOut" | "circle" | "diamond" | "diamondOut", "out") => format!("{} Out", title_first(effect.trim_end_matches("Out"))), + ("zoom" | "zoomOut", "in") => "Zoom In".into(), + ("zoom" | "zoomOut", "out") => "Zoom Out".into(), + ("strips" | "stripsOut", "lu") => "Left Up".into(), + ("strips" | "stripsOut", "ru") => "Right Up".into(), + ("strips" | "stripsOut", "ld") => "Left Down".into(), + ("strips" | "stripsOut", "rd") => "Right Down".into(), + (_, "horz") => "Horizontal".into(), + (_, "vert") => "Vertical".into(), + (_, "across") => "Across".into(), + (_, "down") => "Down".into(), + (_, "up") => "Up".into(), + (_, "right") => "Right".into(), + (_, "left") => "Left".into(), + (_, "clockwise") => "Clockwise".into(), + (_, "counterClockwise") => "Counterclockwise".into(), + (_, "in") => "In".into(), + (_, "out") => "Out".into(), + (_, other) => super::transitions::option_label(other), + } +} + +fn shape_option_name(option: &str) -> &str { + option.strip_suffix("In").or_else(|| option.strip_suffix("Out")).unwrap_or(option) +} + +fn title_first(value: &str) -> String { + let mut chars = value.chars(); + chars.next().map(|c| c.to_uppercase().collect::() + chars.as_str()).unwrap_or_default() +} + pub(super) fn text_build_id(build: deckcraft_model::anim::TextBuild) -> &'static str { match build { deckcraft_model::anim::TextBuild::ByParagraph => "byParagraph", @@ -224,3 +288,45 @@ pub(super) fn text_build_id(build: deckcraft_model::anim::TextBuild) -> &'static deckcraft_model::anim::TextBuild::AsOne => "asOne", } } + +#[cfg(test)] +mod animation_option_label_tests { + use super::animation_option_label; + use deckcraft_model::anim::ANIMATIONS; + + #[test] + fn labels_every_animation_gallery_option_without_changing_its_code() { + for &(effect, _, _, _, _, options) in ANIMATIONS { + for &option in options { + let label = animation_option_label(effect, option); + assert!(!label.is_empty(), "{effect}/{option}"); + assert_ne!(label, option, "raw option shown for {effect}/{option}"); + } + } + assert_eq!(animation_option_label("fly", "lb"), "From Bottom-Left"); + assert_eq!(animation_option_label("flyOut", "rt"), "To Top-Right"); + assert_eq!(animation_option_label("flyOut", "b"), "To Bottom"); + assert_eq!(animation_option_label("peekOut", "l"), "To Left"); + assert_eq!(animation_option_label("wipe", "b"), "From Bottom"); + assert_eq!(animation_option_label("wheel", "1"), "1 Spoke"); + assert_eq!(animation_option_label("split", "vertOut"), "Vertical Out"); + assert_eq!(animation_option_label("shape", "circleIn"), "Circle In"); + assert_eq!(animation_option_label("diamond", "out"), "Diamond Out"); + assert_eq!(animation_option_label("diamondOut", "in"), "Diamond In"); + assert_eq!(animation_option_label("diamondOut", "out"), "Diamond Out"); + assert_eq!(animation_option_label("strips", "lu"), "Left Up"); + assert_eq!(animation_option_label("strips", "ru"), "Right Up"); + assert_eq!(animation_option_label("strips", "ld"), "Left Down"); + assert_eq!(animation_option_label("strips", "rd"), "Right Down"); + assert_eq!(animation_option_label("crawl", "b"), "From Bottom"); + assert_eq!(animation_option_label("crawl", "l"), "From Left"); + assert_eq!(animation_option_label("crawl", "t"), "From Top"); + assert_eq!(animation_option_label("crawl", "r"), "From Right"); + assert_eq!(animation_option_label("crawlOut", "l"), "To Left"); + assert_eq!(animation_option_label("crawlOut", "r"), "To Right"); + assert_eq!(animation_option_label("plus", "in"), "In"); + assert_eq!(animation_option_label("plus", "out"), "Out"); + assert_eq!(animation_option_label("changeFontSize", "increase"), "Increase"); + assert_eq!(animation_option_label("changeFontSize", "decrease"), "Decrease"); + } +} diff --git a/docs/animation-parity.md b/docs/animation-parity.md index b7f840a92..ffca88c9d 100644 --- a/docs/animation-parity.md +++ b/docs/animation-parity.md @@ -1,6 +1,6 @@ # Transitions and animations: parity with PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-29 · **Change:** minor (going back in the show reverses the left slide's transition, #1531; previous: minor, Animation Pane ▸ Play From plays from the selected effect; previous: community PR wave 2026-10-14: interactive-deck triggers, canvas preview, the Animation Pane timeline, repeat and bounce end round-trip; duplicated rows and headers folded; previous: major, transitions measured frame by frame against PowerPoint for Windows' video export) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-30 · **Change:** trivial (animation Effect Options translate stored codes into readable labels; previous: minor, going back in the show reverses the left slide's transition, #1531; previous: minor, Animation Pane ▸ Play From plays from the selected effect; previous: community PR wave 2026-10-14: interactive-deck triggers, canvas preview, the Animation Pane timeline, repeat and bounce end round-trip; duplicated rows and headers folded; previous: major, transitions measured frame by frame against PowerPoint for Windows' video export) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Transitions and animations are what make a deck a presentation, and the part of PowerPoint people notice first when it's missing. Scores roll up into [target-app-parity.md](target-app-parity.md) @@ -65,7 +65,7 @@ In, Wipe, Zoom, Float In, Grow/Shrink, Pulse, Spin, motion lines): weighted, ~85 | Animation feature | PowerPoint | DeckCraft | |---|---|---| | Start On Click / With Previous / After Previous, duration, delay | yes | yes | -| Effect Options (direction, sequence by paragraph) | yes | yes | +| Effect Options (direction, sequence by paragraph) | yes | yes; every gallery option has a readable label while retaining its stored code | | Effect dialog: sound, after-animation dim/hide, animate text by word/letter, smooth start/end, bounce end, repeat, rewind | yes | partly; repeat, bounce end and rewind round-trip | | By-paragraph text builds, sub-bullets follow parents | yes | yes; builds edited in PowerPoint (own start per paragraph, other effects in between, by 2nd-level paragraphs) import as one effect per paragraph and keep their clicks (#844) | | Triggers (on click of a shape) | yes | yes; played in the show: a step per click, the sequence starts over after its last step, a click completes a playing step. Separate click steps, mixed starts and paragraph builds survive PPTX save (`trigger_steps.rs`) | @@ -94,6 +94,7 @@ In, Wipe, Zoom, Float In, Grow/Shrink, Pulse, Spin, motion lines): weighted, ~85 | Date | Change | Summary | |---|---|---| +| 2026-10-30 | trivial | Animation Effect Options show readable, effect-aware labels for all gallery option codes; stored values and command parameters stay unchanged | | 2026-10-29 | minor | Going back in the show plays the transition of the slide being left, reversed, instead of the previous slide's entrance transition (#1531); new row in the transition features. Scores unchanged | | 2026-10-22 | minor | Animation Pane ▸ Play From: with an effect selected, the preview starts at that effect on the play-all clock; the row says yes and the estimate drops to ~14–26 h | | 2026-10-15 | trivial | PPTX interactive sequences preserve later On Click effects and one click per paragraph; no change to playback or feature scores |