From 62ea6043945a991823a14437f4a5b342c7d553b2 Mon Sep 17 00:00:00 2001 From: SusieeTheFemboy <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:48:25 +0100 Subject: [PATCH] Animations: the Add Animation menu scrolls when it is taller than the window (fixes #1370) Every effect in one column is taller than a 1440p screen, and the popup did not scroll, so the last entries could not be reached. Cap the menu at the window height minus the title bar and ribbon, and scroll inside it. Co-Authored-By: Grok 4.7 Co-Authored-By: Claude Opus 5.5 --- .../ui-egui/src/add_animation_menu_tests.rs | 61 +++++++++++++++++++ crates/ui-egui/src/ribbon.rs | 41 ++++++++----- 2 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 crates/ui-egui/src/add_animation_menu_tests.rs diff --git a/crates/ui-egui/src/add_animation_menu_tests.rs b/crates/ui-egui/src/add_animation_menu_tests.rs new file mode 100644 index 00000000..f760b598 --- /dev/null +++ b/crates/ui-egui/src/add_animation_menu_tests.rs @@ -0,0 +1,61 @@ +//! Add Animation (#1370): the menu scrolls, so the last effect can be reached and added. + +use deckcraft_engine::Session; +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; +use serde_json::json; + +use crate::{Services, SlideApp}; + +fn harness(size: egui::Vec2) -> Harness<'static, SlideApp> { + let mut session = Session::with_new(); + let id = session.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, 80.0, 200.0, 120.0]})).unwrap()["id"].as_u64().unwrap(); + session.execute("edit.select", &json!({"ids": [id]})).unwrap(); + let mut app = SlideApp::new(session, Services::default()); + app.ui.tab = "Animations".into(); + let mut h = Harness::builder().with_size(size).with_step_dt(1.0 / 60.0).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + h.run_steps(3); + h +} + +fn open_add_animation(h: &mut Harness<'static, SlideApp>, window: egui::Vec2) { + // The Animations ribbon is wider than a short window. The edge chevron is what a person uses + // to reach Add Animation; scroll-into-view on the button does not move that bar. + for _ in 0..12 { + let rect = h.get_by_label("Add Animation").rect(); + if rect.min.x >= 8.0 && rect.max.x <= window.x - 8.0 { + break; + } + let Some(cue) = h.query_by_label("Scroll ribbon right") else { break }; + cue.click(); + h.run_steps(2); + } + h.get_by_label("Add Animation").click(); + h.run_steps(3); +} + +fn added_effect(h: &Harness<'_, SlideApp>) -> Option<(String, deckcraft_model::anim::AnimClass)> { + h.state().session.active().and_then(|d| d.current_slide()).and_then(|s| s.animations.first()).map(|a| (a.effect.clone(), a.class)) +} + +#[test] +fn the_last_effect_is_reachable_by_scrolling() { + let size = egui::vec2(800.0, 700.0); + let mut h = harness(size); + open_add_animation(&mut h, size); + let before = h.get_by_label("Neutron — Motion").rect(); + assert!(before.min.y > 700.0, "the last effect starts below the window: {before:?}"); + h.get_by_label("Neutron — Motion").scroll_to_me(); + h.run_steps(4); + let row = h.get_by_label("Neutron — Motion").rect(); + assert!(row.min.y >= 0.0 && row.max.y <= 700.0, "a scrolled effect stays inside the window: {row:?}"); + h.get_by_label("Neutron — Motion").click(); + h.run_steps(3); + assert_eq!(added_effect(&h), Some(("neutron".into(), deckcraft_model::anim::AnimClass::Path))); +} diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index cedd34c4..afafedd2 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -2091,24 +2091,29 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { }); group(ui, |ui| { let r = big_button(ui, Icon::AddAnimation, "Add\nAnimation", has_sel); + // Every effect in one column is taller than a 1440p screen, so cap the height and scroll (#1370). + // The menu opens under the title bar and the ribbon; keep that chrome out of the cap. + let max_h = (r.ctx.viewport_rect().height() - 160.0).max(120.0); egui::Popup::menu(&r).width(260.0).show(|ui| { - for a in deckcraft_model::anim::ANIMATIONS.iter().filter(|a| a.2 != AnimClass::Media) { - if ui - .button(format!( - "{} — {}", - a.1, - match a.2 { - AnimClass::Entrance => "Entrance", - AnimClass::Emphasis => "Emphasis", - AnimClass::Exit => "Exit", - _ => "Motion", - } - )) - .clicked() - { - run(app, "animation.add", json!({"effect": a.0, "class": a.2.xml()})); + egui::ScrollArea::vertical().id_salt("add_animation").max_height(max_h).show(ui, |ui| { + for a in deckcraft_model::anim::ANIMATIONS.iter().filter(|a| a.2 != AnimClass::Media) { + if ui + .button(format!( + "{} — {}", + a.1, + match a.2 { + AnimClass::Entrance => "Entrance", + AnimClass::Emphasis => "Emphasis", + AnimClass::Exit => "Exit", + _ => "Motion", + } + )) + .clicked() + { + run(app, "animation.add", json!({"effect": a.0, "class": a.2.xml()})); + } } - } + }); }); big(app, ui, Icon::AnimationPane, "Animation\nPane", "view.pane", json!({"pane": "animation"})); let r = big_button(ui, Icon::Trigger, "Trigger", cur.is_some()); @@ -3633,6 +3638,10 @@ mod master_layout_tests; #[path = "icons_popup_tests.rs"] mod icons_popup_tests; +#[cfg(test)] +#[path = "add_animation_menu_tests.rs"] +mod add_animation_menu_tests; + #[cfg(test)] #[path = "align_tests.rs"] mod align_tests;