diff --git a/crates/ui-egui/src/menus/frontend.rs b/crates/ui-egui/src/menus/frontend.rs index a3f95ee1c..5d65ffb8f 100644 --- a/crates/ui-egui/src/menus/frontend.rs +++ b/crates/ui-egui/src/menus/frontend.rs @@ -488,7 +488,15 @@ pub fn frontend(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, p: Valu app.ui.home_templates = true; Value::Null } - "effect.manage" | "anim.browsePresets" => { + "effect.manage" => { + app.show_panel(PanelKind::EffectsPresets); + Value::Null + } + "anim.browsePresets" => { + app.ui.effects_search.clear(); + app.ui.effects_view.presets = true; + app.ui.effects_open.insert("presets".into()); + ctx.data_mut(|data| data.insert_temp(egui::Id::unique(crate::panels::effects_presets::REVEAL_PRESETS_ID), true)); app.show_panel(PanelKind::EffectsPresets); Value::Null } diff --git a/crates/ui-egui/src/panels/effects_presets.rs b/crates/ui-egui/src/panels/effects_presets.rs index 380d1d869..44c77ec22 100644 --- a/crates/ui-egui/src/panels/effects_presets.rs +++ b/crates/ui-egui/src/panels/effects_presets.rs @@ -17,6 +17,7 @@ use crate::{EffectcraftApp, widgets}; const ROW_H: f32 = 21.0; const RECENT_MAX: usize = 8; +pub(crate) const REVEAL_PRESETS_ID: &str = "fx-reveal-presets"; /// A user animation preset file. #[derive(Clone, Debug, PartialEq)] @@ -157,6 +158,24 @@ fn folder_row( open } +/// Number of visible rows before Animation Presets in the current panel state. +fn rows_before_presets(favorites: usize, recent: usize, searching: bool, favorites_open: bool, recent_open: bool) -> usize { + let mut rows = 0usize; + if favorites > 0 { + rows = rows.saturating_add(1); + if searching || favorites_open { + rows = rows.saturating_add(favorites); + } + } + if !searching && recent > 0 { + rows = rows.saturating_add(1); + if recent_open { + rows = rows.saturating_add(recent); + } + } + rows +} + /// An effect row (fx icon, highlighted name, favourite star, 32 bpc badge). #[allow(clippy::too_many_arguments)] fn effect_row( @@ -364,7 +383,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let view = app.ui.effects_view.clone(); let list = Rect::from_min_max(pos2(rect.min.x, sr.max.y + 6.0), rect.max); let lp = p.with_clip_rect(list); - let scroll = widgets::PanelScroll::begin(ui, egui::Id::unique("fx-scroll"), list); + let mut scroll = widgets::PanelScroll::begin(ui, egui::Id::unique("fx-scroll"), list); let query = q.trim().to_lowercase(); let searching = !query.is_empty(); let mut y = list.min.y - scroll.offset; @@ -378,6 +397,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // Favorites and Recently Used. let favs: Vec<_> = app.session.prefs.effects_favorites.iter().filter_map(|id| lookup(id)).filter(|e| keep(e)).collect(); + let recent: Vec<_> = app.ui.effects_recent.iter().filter_map(|id| lookup(id)).filter(|e| keep(e)).collect(); + let reveal_presets = ctx.data_mut(|data| data.remove_temp::(egui::Id::unique(REVEAL_PRESETS_ID))).unwrap_or(false); + if reveal_presets { + // These rows come before Animation Presets. Scroll past expanded lists so the section is + // visible even when Favorites or Recently Used is long. + let preceding_rows = + rows_before_presets(favs.len(), recent.len(), searching, app.ui.effects_open.contains("favorites"), app.ui.effects_open.contains("recent")); + scroll.offset = ROW_H * preceding_rows as f32; + y = list.min.y - scroll.offset; + } if !favs.is_empty() && folder_row(app, ui, &lp, list, &mut y, "favorites", "Favorites", 0.0, searching, Some(Icon::Star)) { let mut favs = favs; favs.sort_by_key(|e| e.name); @@ -385,7 +414,6 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { effect_row(app, ui, &lp, list, &mut y, e, 0.0, &query, "effects.favorites", &mut apply); } } - let recent: Vec<_> = app.ui.effects_recent.iter().filter_map(|id| lookup(id)).filter(|e| keep(e)).collect(); if !recent.is_empty() && !searching && folder_row(app, ui, &lp, list, &mut y, "recent", "Recently Used", 0.0, false, None) { for e in recent { effect_row(app, ui, &lp, list, &mut y, e, 0.0, &query, "effects.recent", &mut apply); diff --git a/crates/ui-egui/tests/it/ui_menus.rs b/crates/ui-egui/tests/it/ui_menus.rs index 38dcd15bc..424b75c48 100644 --- a/crates/ui-egui/tests/it/ui_menus.rs +++ b/crates/ui-egui/tests/it/ui_menus.rs @@ -65,6 +65,47 @@ fn shown(h: &Harness<'_, EffectcraftApp>, label: &str) -> bool { h.query_by_label_contains(label).is_some() } +#[test] +fn browse_presets_clears_search_and_scrolls_to_the_visible_presets_list() { + use effectcraft_ui_egui::dock::PanelKind; + + let mut h = harness(); + let ctx = h.ctx.clone(); + let favorites: Vec = effectcraft_engine::effects::all().iter().map(|effect| effect.id.to_string()).collect(); + assert!(!favorites.is_empty(), "the test needs registered effects"); + let recent: Vec = favorites.iter().take(8).cloned().collect(); + + h.state_mut().session.prefs.effects_favorites = favorites; + h.state_mut().ui.effects_recent = recent; + h.state_mut().ui.effects_open.extend(["favorites".into(), "recent".into()]); + h.state_mut().ui.effects_search = "no matching animation preset".into(); + h.state_mut().ui.effects_view.presets = true; + + let managed = effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "effect.manage", json!({})); + assert!(managed.is_ok(), "{managed:?}"); + assert_eq!(h.state().ui.focused, PanelKind::EffectsPresets); + assert_eq!(h.state().ui.effects_search, "no matching animation preset", "Manage Effects keeps its existing search behavior"); + assert!(h.state().ui.effects_view.presets, "Manage Effects keeps the existing presets setting"); + + h.run_steps(3); + assert!(h.state().auto.find("effects.category.presets").is_none(), "the unmatched search hides Animation Presets"); + h.state_mut().ui.effects_view.presets = false; + + let result = effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "anim.browsePresets", json!({})); + + assert!(result.is_ok(), "{result:?}"); + assert_eq!(h.state().ui.focused, PanelKind::EffectsPresets); + assert!(h.state().ui.effects_search.is_empty()); + assert!(h.state().ui.effects_view.presets); + assert!(h.state().ui.effects_open.contains("presets")); + + h.run_steps(3); + assert!( + h.state().auto.find("effects.category.presets").is_some(), + "Animation Presets must be visible after Browse Presets, even below expanded Favorites and Recently Used" + ); +} + /// Space (or any shortcut) while a menu is open goes to the menu, not the app: it started the /// preview behind the open menu. #[test]