From 107cedb84c3851eb9a340b26fe22d293d7172463 Mon Sep 17 00:00:00 2001 From: "Ayberk A." <105879357+AyberkA0@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:40:04 +0300 Subject: [PATCH] fix(effects): starred effects survive a restart (#1692) Effects & Presets kept its favourites in `UiState::effects_favorites`, which nothing writes out (only the workspace layout is saved), so the stars were gone the next time the app opened. They now live in the settings (`Prefs::effects_favorites`, saved when a star is clicked) like the recent fonts and presets, survive Reset All like them, and a hand-edited list loses empty and repeated ids on load. --- crates/engine/src/prefs.rs | 27 +++++++++++++++++++ crates/engine/src/tests_settings.rs | 28 ++++++++++++++++++++ crates/ui-egui/src/panels/effects_presets.rs | 12 ++++----- crates/ui-egui/src/state.rs | 4 --- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 3abf336b..a2ce579e 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -428,6 +428,8 @@ pub struct Prefs { pub recent_presets: Vec, /// Character panel: recently used font families, newest first. pub recent_fonts: Vec, + /// Effects & Presets: the starred effects' ids, in the order starred. + pub effects_favorites: Vec, /// Preview panel: every shortcut's options. pub preview: crate::preview::PreviewSettings, /// View ▸ Simulate Output ▸ My Custom RGB. @@ -468,6 +470,7 @@ impl Default for Prefs { recent_footage: vec![], recent_presets: vec![], recent_fonts: vec![], + effects_favorites: vec![], preview: crate::preview::PreviewSettings::default(), custom_rgb: crate::viewer::CustomRgb::default(), comp_presets: vec![], @@ -597,6 +600,9 @@ impl Prefs { pub fn normalize(&mut self) { self.version = PREFS_VERSION; self.comp_presets.retain(CompPreset::is_valid); + let mut seen = std::collections::HashSet::new(); + self.effects_favorites.retain(|id| !id.is_empty() && seen.insert(id.clone())); + self.effects_favorites.truncate(MAX_EFFECTS_FAVORITES); if !is_choice(WINDOW_GRAPHICS, &self.startup.window_graphics) { self.startup.window_graphics = Startup::default().window_graphics; } @@ -715,6 +721,7 @@ impl Prefs { recent_footage: std::mem::take(&mut self.recent_footage), recent_presets: std::mem::take(&mut self.recent_presets), recent_fonts: std::mem::take(&mut self.recent_fonts), + effects_favorites: std::mem::take(&mut self.effects_favorites), preview: std::mem::take(&mut self.preview), comp_presets: std::mem::take(&mut self.comp_presets), ..d @@ -916,6 +923,23 @@ impl Prefs { push_mru(&mut self.recent_fonts, family, 30); } + /// Is effect `id` starred in Effects & Presets? + pub fn is_effect_favorite(&self, id: &str) -> bool { + self.effects_favorites.iter().any(|f| f == id) + } + + /// Star effect `id` in Effects & Presets, or un-star it; true when it is starred now. + pub fn toggle_effect_favorite(&mut self, id: &str) -> bool { + if self.is_effect_favorite(id) { + self.effects_favorites.retain(|f| f != id); + false + } else { + self.effects_favorites.push(id.to_string()); + self.effects_favorites.truncate(MAX_EFFECTS_FAVORITES); + self.is_effect_favorite(id) + } + } + /// The recent fonts the Character panel shows (at most Number of Recent Fonts to Display). pub fn recent_fonts_shown(&self) -> &[String] { let n = (self.type_.recent_fonts as usize).min(self.recent_fonts.len()); @@ -933,6 +957,9 @@ impl Prefs { } } +/// Most effects Effects & Presets keeps starred (a hand-edited settings file can hold more). +const MAX_EFFECTS_FAVORITES: usize = 1000; + fn push_mru(list: &mut Vec, v: &str, max: usize) { list.retain(|x| x != v); list.insert(0, v.to_string()); diff --git a/crates/engine/src/tests_settings.rs b/crates/engine/src/tests_settings.rs index 65e39b46..bb9fe4cd 100644 --- a/crates/engine/src/tests_settings.rs +++ b/crates/engine/src/tests_settings.rs @@ -212,6 +212,34 @@ fn prefs_commands_get_set_reset_and_persist() { ); } +/// Effects & Presets' starred effects live in the settings (#1692): they come back in the next +/// session, un-starring sticks too, Reset All keeps them, and a hand-edited list loses its +/// repeats and empty ids on load. +#[test] +fn effects_favorites_persist_across_sessions() { + let store = Arc::new(MemoryConfig::default()); + let mut s = Session { config: Some(store.clone()), ..Default::default() }; + assert!(s.prefs.toggle_effect_favorite("ADBE Gaussian Blur 2")); + assert!(s.prefs.toggle_effect_favorite("ADBE Glo2")); + s.save_prefs(); + let reopen = |store: &Arc| { + let mut s = Session { config: Some(store.clone()), ..Default::default() }; + s.load_settings(); + s + }; + let mut s2 = reopen(&store); + assert_eq!(s2.prefs.effects_favorites, ["ADBE Gaussian Blur 2", "ADBE Glo2"]); + assert!(!s2.prefs.toggle_effect_favorite("ADBE Glo2"), "un-starred"); + s2.save_prefs(); + let mut s3 = reopen(&store); + assert_eq!(s3.prefs.effects_favorites, ["ADBE Gaussian Blur 2"]); + s3.execute("prefs.reset", json!({})).unwrap(); + assert!(s3.prefs.is_effect_favorite("ADBE Gaussian Blur 2"), "Reset All keeps the stars"); + let edited = store.read(PREFS_FILE).unwrap().replace("\"effectsFavorites\": [", "\"effectsFavorites\": [\"\", \"ADBE Glo2\", \"ADBE Glo2\", "); + store.write(PREFS_FILE, &edited).unwrap(); + assert_eq!(reopen(&store).prefs.effects_favorites, ["ADBE Glo2", "ADBE Gaussian Blur 2"]); +} + /// Composition Settings presets the user saves live in the settings: they persist, a save with /// an existing name replaces it, they survive Reset All, and invalid ones (from a hand-edited /// settings file) are dropped on load. diff --git a/crates/ui-egui/src/panels/effects_presets.rs b/crates/ui-egui/src/panels/effects_presets.rs index 8eecfd09..1d30cdcb 100644 --- a/crates/ui-egui/src/panels/effects_presets.rs +++ b/crates/ui-egui/src/panels/effects_presets.rs @@ -195,7 +195,7 @@ fn effect_row( } app.auto.add(&format!("{auto_prefix}.{}", e.id), r, e.name); // Favourite star (always shown when starred, else on hover). - let fav = app.ui.effects_favorites.contains(e.id); + let fav = app.session.prefs.is_effect_favorite(e.id); let sresp = ui.interact(star.intersect(list), egui::Id::new((auto_prefix, e.id, "star")), Sense::click()); if fav || resp.hovered() || sresp.hovered() { let gold = Color32::from_rgb(0xe8, 0xc0, 0x40); @@ -203,11 +203,9 @@ fn effect_row( } app.auto.add(&format!("{auto_prefix}.{}.star", e.id), star, if fav { "Remove from Favorites" } else { "Add to Favorites" }); if sresp.clicked() { - if fav { - app.ui.effects_favorites.remove(e.id); - } else { - app.ui.effects_favorites.insert(e.id.to_string()); - } + // Kept in the settings, so the stars are there next time (#1692). + app.session.prefs.toggle_effect_favorite(e.id); + app.session.save_prefs(); return; } if resp.double_clicked() { @@ -364,7 +362,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let lookup = |id: &str| reg.iter().find(|e| e.id == id); // Favorites and Recently Used. - let favs: Vec<_> = app.ui.effects_favorites.iter().filter_map(|id| lookup(id)).filter(|e| keep(e)).collect(); + let favs: Vec<_> = app.session.prefs.effects_favorites.iter().filter_map(|id| lookup(id)).filter(|e| keep(e)).collect(); 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); diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index bbfe0c05..1e2f5720 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -643,9 +643,6 @@ pub struct UiState { /// Levels (Individual Controls) editor: shown channel per effect uid. #[serde(default)] pub fx_levels_channel: BTreeMap, - /// Effects & Presets: favourite effect ids (starred). - #[serde(default)] - pub effects_favorites: BTreeSet, /// Effects & Presets: recently applied effect ids, most recent first. #[serde(default)] pub effects_recent: Vec, @@ -734,7 +731,6 @@ impl Default for UiState { fx_curve_channel: BTreeMap::new(), fx_curve_pencil: BTreeSet::new(), fx_levels_channel: BTreeMap::new(), - effects_favorites: BTreeSet::new(), effects_recent: Vec::new(), effects_view: EffectsView::default(), snapping: true,