1use crate::settings::Settings;
11use crate::ui::library::{confirm, inline_edit};
12use eframe::egui;
13
14#[cfg(test)]
15use crate::ui::effects_ui::test_rects;
16
17#[derive(Default)]
18pub struct PresetsState {
19 rename: Option<(bool, usize, String)>,
21 name: String,
23}
24
25#[derive(Default)]
26pub struct PresetsResponse {
27 pub apply: Option<usize>,
29 pub place: Vec<String>,
31 pub save: Option<String>,
33 pub settings_changed: bool,
34}
35
36enum Act {
38 Rename(bool, usize, String),
39 Delete(bool, usize),
40}
41
42fn effect_rows(settings: &Settings, graph: bool) -> Vec<(usize, String)> {
44 settings
45 .effect_presets
46 .iter()
47 .enumerate()
48 .filter(|(_, p)| p.is_graph() == graph)
49 .map(|(i, p)| (i, p.name.clone()))
50 .collect()
51}
52
53pub fn show(
54 ui: &mut egui::Ui,
55 state: &mut PresetsState,
56 settings: &mut Settings,
57 has_selection: bool,
58) -> PresetsResponse {
59 #[cfg(test)]
60 test_rects::clear();
61 let mut resp = PresetsResponse::default();
62 ui.horizontal(|ui| {
63 ui.add(egui::TextEdit::singleline(&mut state.name).desired_width(140.0).hint_text("Preset name"));
64 let name = state.name.trim().to_string();
65 let b = ui.add_enabled(has_selection && !name.is_empty(), egui::Button::new("Save from selection"));
66 #[cfg(test)]
67 test_rects::push("save".into(), b.rect);
68 let tip = "The selected clip's node graph or effect stack — or the whole selection as a template";
69 if b.on_hover_text(tip).clicked() {
70 resp.save = Some(name);
71 state.name.clear();
72 }
73 });
74 ui.separator();
75
76 let (fx, graphs) = (effect_rows(settings, false), effect_rows(settings, true));
78 let (mut adj, mut cont, mut tpl) = (Vec::new(), Vec::new(), Vec::new());
79 for (i, t) in settings.templates.iter().enumerate() {
80 let dst = if crate::engine::presets::is_adjustment_template(t) {
81 &mut adj
82 } else if crate::engine::presets::is_container_template(t) {
83 &mut cont
84 } else {
85 &mut tpl
86 };
87 dst.push((i, t.name.clone()));
88 }
89
90 let mut act = None;
91 egui::ScrollArea::vertical().show(ui, |ui| {
92 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
94 section(ui, state, false, "Effects", &fx, &mut act, &mut resp);
95 section(ui, state, false, "Node graphs", &graphs, &mut act, &mut resp);
96 section(ui, state, true, "Adjustment layers", &adj, &mut act, &mut resp);
97 section(ui, state, true, "Container templates", &cont, &mut act, &mut resp);
98 section(ui, state, true, "Templates", &tpl, &mut act, &mut resp);
99 });
100
101 if let Some(act) = act {
102 resp.settings_changed = match act {
103 Act::Rename(true, i, name) => {
104 settings.templates[i].name = name;
105 true
106 }
107 Act::Rename(false, i, name) => {
108 settings.effect_presets[i].name = name;
109 true
110 }
111 Act::Delete(t, i) => {
113 let name = if t { &settings.templates[i].name } else { &settings.effect_presets[i].name };
114 let go = confirm("Delete preset", &format!("Delete the saved preset \"{name}\"?"));
115 if go {
116 if t {
117 settings.templates.remove(i);
118 } else {
119 settings.effect_presets.remove(i);
120 }
121 }
122 go
123 }
124 };
125 }
126 resp
127}
128
129fn section(
131 ui: &mut egui::Ui,
132 state: &mut PresetsState,
133 templates: bool,
134 title: &str,
135 rows: &[(usize, String)],
136 act: &mut Option<Act>,
137 resp: &mut PresetsResponse,
138) {
139 ui.add_space(4.0);
140 ui.strong(title);
141 if rows.is_empty() {
142 ui.weak("(none)");
143 return;
144 }
145 let tip = if templates { "Place at the playhead" } else { "Apply to the selection" };
146 for (i, name) in rows {
147 if state.rename.as_ref().is_some_and(|(t, j, _)| *t == templates && j == i) {
148 let buf = &mut state.rename.as_mut().unwrap().2;
149 if let Some(new) = inline_edit(ui, buf) {
150 if !new.is_empty() {
151 *act = Some(Act::Rename(templates, *i, new));
152 }
153 state.rename = None;
154 }
155 continue;
156 }
157 let w = ui.available_width();
158 let r = ui.add(egui::Button::new(name.as_str()).min_size(egui::vec2(w, 0.0))).on_hover_text(tip);
159 #[cfg(test)]
160 test_rects::push(format!("{title}{i}"), r.rect);
161 if r.clicked() {
162 if templates {
163 resp.place.push(name.clone());
164 } else {
165 resp.apply = Some(*i);
166 }
167 }
168 r.context_menu(|ui| {
169 if ui.button("Rename").clicked() {
170 state.rename = Some((templates, *i, name.clone()));
171 ui.close();
172 }
173 if ui.button("Delete").clicked() {
174 *act = Some(Act::Delete(templates, *i));
175 ui.close();
176 }
177 });
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::settings::EffectPreset;
185 use eframe::egui::{vec2, Event, Modifiers, PointerButton, Pos2, RawInput, Rect};
186
187 struct Harness {
188 ctx: egui::Context,
189 state: PresetsState,
190 settings: Settings,
191 time: f64,
192 }
193
194 impl Harness {
195 fn new() -> Self {
197 let mut settings = Settings::default();
198 settings.effect_presets.push(EffectPreset { name: "Look".into(), json: "[]".into() });
199 settings.effect_presets.push(EffectPreset { name: "Graph".into(), json: "{\"nodes\":[]}".into() });
200 let mut p = crate::model::Project::new();
201 let a = p.add_adjustment_clip(0.0, 2.0);
202 let t = p.add_text_clip(0.0, 2.0);
203 settings.templates.push(crate::engine::presets::capture_template("Grade", &p, &[a]));
204 settings.templates.push(crate::engine::presets::capture_template("Title", &p, &[t]));
205 Self { ctx: egui::Context::default(), state: PresetsState::default(), settings, time: 0.0 }
206 }
207 fn frame(&mut self, events: Vec<Event>) -> PresetsResponse {
208 self.time += 0.05;
209 let input = RawInput {
210 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 600.0))),
211 time: Some(self.time),
212 events,
213 ..Default::default()
214 };
215 let mut resp = PresetsResponse::default();
216 let Harness { ctx, state, settings, .. } = self;
217 let _ = ctx.run(input, |ctx| {
218 egui::CentralPanel::default().show(ctx, |ui| resp = show(ui, state, settings, true));
219 });
220 resp
221 }
222 fn click(&mut self, name: &str) -> PresetsResponse {
224 let pos = test_rects::get(name).unwrap_or_else(|| panic!("no widget rect for {name}")).center();
225 let btn = |pressed| Event::PointerButton {
226 pos,
227 button: PointerButton::Primary,
228 pressed,
229 modifiers: Modifiers::NONE,
230 };
231 self.frame(vec![Event::PointerMoved(pos)]);
232 self.frame(vec![btn(true)]);
233 self.frame(vec![btn(false)])
234 }
235 }
236
237 #[test]
239 fn rows_apply_and_place() {
240 let mut h = Harness::new();
241 h.frame(vec![]);
242 for name in ["Effects0", "Node graphs1", "Adjustment layers0", "Templates1"] {
244 assert!(test_rects::get(name).is_some(), "no row for {name}");
245 }
246 let r = h.click("Node graphs1");
248 assert_eq!(r.apply, Some(1));
249 assert!(r.place.is_empty());
250 let r = h.click("Templates1");
251 assert_eq!(r.place, vec!["Title".to_string()]);
252 assert!(r.apply.is_none());
253 assert!(h.click("save").save.is_none());
255 h.state.name = " Intro ".into();
256 let r = h.click("save");
257 assert_eq!(r.save.as_deref(), Some("Intro"));
258 assert!(h.state.name.is_empty());
259 }
260
261 #[test]
263 fn empty_settings_draw_nothing_to_apply() {
264 let mut h = Harness::new();
265 h.settings = Settings::default();
266 let r = h.frame(vec![]);
267 assert!(r.apply.is_none() && r.place.is_empty() && !r.settings_changed);
268 assert!(test_rects::get("Effects0").is_none());
269 }
270}