diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 36f47b27..7d2ab7a2 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -429,6 +429,9 @@ mod master_layout_tests; #[cfg(test)] mod icons_popup_tests; +#[cfg(test)] +mod add_animation_menu_tests; + #[cfg(test)] mod align_tests; diff --git a/crates/ui-egui/src/ribbon/add_animation_menu_tests.rs b/crates/ui-egui/src/ribbon/add_animation_menu_tests.rs new file mode 100644 index 00000000..f760b598 --- /dev/null +++ b/crates/ui-egui/src/ribbon/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/animations.rs b/crates/ui-egui/src/ribbon/animations.rs index 30dbb9e3..67f0bbd0 100644 --- a/crates/ui-egui/src/ribbon/animations.rs +++ b/crates/ui-egui/src/ribbon/animations.rs @@ -103,24 +103,29 @@ pub(super) 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());