1use std::ops::RangeInclusive;
2use std::rc::Rc;
3
4use strum::VariantArray;
5
6use crate::palette::{lutgen_dir, DynamicPalette};
7use crate::state::LutAlgorithm;
8use crate::App;
9
10fn labeled_slider<Num: egui::emath::Numeric>(
13 ui: &mut egui::Ui,
14 label: &str,
15 value: &mut Num,
16 range: RangeInclusive<Num>,
17) -> egui::Response {
18 ui.label(label);
19 ui.horizontal(|ui| {
20 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
21 let drag = ui.add(egui::DragValue::new(value).range(range.clone()));
22 ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
23 ui.style_mut().spacing.slider_width =
24 ui.available_width() - ui.spacing().item_spacing.x;
25 let slider = ui.add(egui::Slider::new(value, range).show_value(false));
26 drag | slider
27 })
28 .inner
29 })
30 .inner
31 })
32 .inner
33}
34
35pub struct PaletteFilterBox {
36 items: Vec<Rc<DynamicPalette>>,
37 idx: usize,
38 filter: String,
39 filtered: Vec<Rc<DynamicPalette>>,
40}
41
42impl PaletteFilterBox {
43 pub fn new(current: &DynamicPalette) -> Self {
44 let items: Vec<_> = DynamicPalette::get_all()
45 .unwrap()
46 .into_iter()
47 .map(Rc::new)
48 .collect();
49
50 Self {
51 filter: String::new(),
52 filtered: items.clone(),
53 idx: items
54 .iter()
55 .position(|v| **v == *current)
56 .unwrap_or_default(),
57 items,
58 }
59 }
60
61 pub fn reindex(&mut self, current: &DynamicPalette) {
62 self.items = DynamicPalette::get_all()
63 .unwrap()
64 .into_iter()
65 .map(Rc::new)
66 .collect();
67 self.filter();
68 self.idx = self
69 .filtered
70 .iter()
71 .position(|v| **v == *current)
72 .unwrap_or_default();
73 }
74
75 fn filter(&mut self) {
76 if self.filter.is_empty() {
77 self.filtered = self.items.clone();
78 } else {
79 self.filtered = self
80 .items
81 .iter()
82 .filter(|palette| palette.as_str().contains(&self.filter.to_lowercase()))
83 .cloned()
84 .collect();
85 }
86 }
87
88 pub fn show(&mut self, ui: &mut egui::Ui, current: &mut DynamicPalette) -> egui::Response {
89 let mut apply = false;
90 let mut res = ui
91 .group(|ui| {
92 egui::Resize::default()
93 .resizable([true, false])
94 .min_width(ui.available_width())
95 .max_width(ui.available_width())
96 .with_stroke(false)
97 .show(ui, |ui| {
98 ui.horizontal(|ui| {
99 if egui::TextEdit::singleline(&mut self.filter)
100 .hint_text("Search Palettes ...")
101 .desired_width(ui.available_width() - 55.)
102 .show(ui)
103 .response
104 .changed()
105 {
106 self.filter();
108 if !self.filtered.is_empty() {
109 self.idx = 0;
110 *current = (*self.filtered[self.idx]).clone();
111 }
112 }
113
114 if ui.button("<").clicked() && !self.filtered.is_empty() {
115 if self.idx == 0 {
116 self.idx = self.filtered.len() - 1;
117 } else {
118 self.idx -= 1;
119 }
120 *current = (*self.filtered[self.idx]).clone();
121 apply = true;
122 }
123 if ui.button(">").clicked() && !self.filtered.is_empty() {
124 if self.idx >= self.filtered.len() - 1 {
125 self.idx = 0;
126 } else {
127 self.idx += 1;
128 }
129 *current = (*self.filtered[self.idx]).clone();
130 apply = true;
131 }
132 });
133
134 ui.separator();
135
136 egui::ScrollArea::new([true, true])
137 .auto_shrink([false, false])
138 .show(ui, |ui| {
139 for (i, palette) in self.filtered.iter().enumerate() {
140 let selected = (**palette).clone();
141 let res = ui.add(
142 egui::Button::selectable(
143 *current == selected,
144 palette.as_str(),
145 )
146 .min_size(egui::Vec2::new(ui.available_width() - 1., 16.)),
147 );
148 res.gained_focus()
150 .then(|| ui.scroll_to_cursor(Some(egui::Align::Center)));
151 if apply && *current == **palette {
153 res.request_focus();
154 ui.scroll_to_cursor(Some(egui::Align::Center));
155 }
156 if res.clicked() {
157 *current = selected;
158 self.idx = i;
159 apply = true;
160 }
161 }
162 });
163 })
164 })
165 .response;
166
167 if apply {
169 res.mark_changed();
170 }
171
172 res
173 }
174}
175
176pub struct PaletteEditor {
177 name: String,
178}
179
180impl PaletteEditor {
181 pub fn new(current: &DynamicPalette) -> Self {
182 Self {
183 name: current.to_string(),
184 }
185 }
186
187 pub fn show(
188 &mut self,
189 ui: &mut egui::Ui,
190 palette: &mut Vec<[u8; 3]>,
191 current: &mut DynamicPalette,
192 ) -> [bool; 2] {
193 let mut apply = false;
195 let mut saved = false;
196 ui.group(|ui| {
197 ui.horizontal(|ui| {
198 let enabled = matches!(current, DynamicPalette::Custom(_));
199 ui.add_enabled(
200 enabled,
201 egui::TextEdit::singleline(&mut self.name)
202 .desired_width(ui.available_width() - 49.),
203 );
204
205 if ui.add_enabled(enabled, egui::Button::new("save")).clicked() {
206 *current = DynamicPalette::Custom(self.name.clone());
207 current.save(palette).unwrap();
208 saved = true;
209 }
210 });
211 ui.separator();
212 ui.horizontal_wrapped(|ui| {
213 ui.spacing_mut().interact_size.x =
214 calculate_width(ui.available_width() + 7., 40., ui.spacing().item_spacing.x);
215
216 let mut res = Vec::new();
217 for color in palette.iter_mut() {
218 res.push(egui::widgets::color_picker::color_edit_button_srgb(
219 ui, color,
220 ));
221 }
222 for (i, res) in res.iter().enumerate() {
223 if res.changed() {
224 if matches!(current, DynamicPalette::Builtin(_)) {
225 let name = current.to_string() + "-custom";
226 self.name = name.clone();
227 *current = DynamicPalette::Custom(name);
228 }
229 apply = true;
230 }
231 if res.secondary_clicked() {
232 if matches!(current, DynamicPalette::Builtin(_)) {
233 let name = current.to_string() + "-custom";
234 self.name = name.clone();
235 *current = DynamicPalette::Custom(name);
236 }
237 palette.remove(i);
238 apply = true;
239 }
240 }
241
242 if ui
243 .add(egui::Button::new("+").min_size(ui.spacing().interact_size))
244 .clicked()
245 {
246 if matches!(current, DynamicPalette::Builtin(_)) {
247 let name = current.to_string() + "-custom";
248 self.name = name.clone();
249 *current = DynamicPalette::Custom(name);
250 }
251 palette.push([0u8; 3]);
252 apply = true;
253 };
254 });
255 });
256 [apply, saved]
257 }
258}
259
260pub fn calculate_width(width: f32, target: f32, padding: f32) -> f32 {
262 if width <= 0.0 || target <= 0.0 {
263 return 0.0;
264 }
265
266 let target_with_padding = target + padding;
267 if width < target_with_padding {
268 return (width - padding).max(0.0);
269 }
270
271 let buttons_that_fit = (width / target_with_padding).round();
272 if buttons_that_fit <= 0.0 {
273 return (width - padding).max(0.0);
274 }
275
276 (width / buttons_that_fit) - padding
277}
278
279impl App {
280 fn show_settings(&mut self, ui: &mut egui::Ui) -> bool {
281 let mut apply = false;
282 ui.group(|ui| {
283 ui.horizontal(|ui| {
285 ui.with_layout(
286 egui::Layout::right_to_left(egui::Align::Center),
287 |ui| {
288 if ui.button("Reset").clicked() {
289 self.state.reset_current_args();
290 self.apply();
291 }
292 egui::ComboBox::from_id_salt("algorithm")
293 .selected_text(format!("{:?}", self.state.current_alg))
294 .width(ui.available_width())
295 .show_ui(ui, |ui| {
296 for alg in LutAlgorithm::VARIANTS {
297 let val = ui.selectable_value(
298 &mut self.state.current_alg,
299 *alg,
300 alg.to_string(),
301 );
302 apply |= val.clicked();
303 val.gained_focus().then(|| {
304 ui.scroll_to_cursor(Some(egui::Align::Center))
305 });
306 }
307 });
308 },
309 );
310 });
311 ui.separator();
312
313 ui.heading("Common Arguments");
315 ui.add_space(5.);
316
317 let res = labeled_slider(ui, "Hald-Clut Level", &mut self.state.common.level, 4..=16);
318 apply |= res.drag_stopped() | res.lost_focus();
319 res.on_hover_text("\
320 Hald clut level to generate. Heavy impact on performance for high levels. \n\
321 A level of 16 computes a value for the entire sRGB color space.\n\n\
322 Range: 4-16",
323 );
324
325 let res = labeled_slider(ui, "Luminosity Factor", self.state.common.lum_factor.as_mut(), 0.001..=2.);
326 apply |= res.drag_stopped() | res.lost_focus();
327 res.on_hover_text("\
328 Factor to multiply luminocity values by. \
329 Effectively weights the interpolation to prefer more \
330 colorful or more greyscale/unsaturated matches.\n\n\
331 Tip: Use values below 1.0 for more colorful results, \
332 above 1.0 for less colorful results. \
333 Extreme values usually are paired with 'Preserve Luminosity'.\n\n\
334 Default: 0.7");
335
336 let res = ui
337 .checkbox(&mut self.state.common.preserve, "Preserve Luminosity");
338 apply |= res.changed();
339 res.on_hover_text("\
340 Preserve the original image's luminocity values after interpolation. \
341 This effectively retains the image's contrast and generally improves gradients.\n\n\
342 Default: true");
343
344 match self.state.current_alg {
346 LutAlgorithm::GaussianRbf => {
347 ui.separator();
348 ui.heading("Gaussian Arguments");
349 ui.add_space(5.);
350
351 let res = labeled_slider(ui, "Shape", self.state.guassian_rbf.shape.as_mut(), 0.001..=512.);
352 apply |= res.drag_stopped() | res.lost_focus();
353 res.on_hover_text("\
354 Shape parameter for the default Gaussian RBF interpolation. \
355 Effectively creates more or less blending between colors in the palette.\n\n\
356 Bigger numbers = less blending (closer to original colors)\n\
357 Smaller numbers = more blending (smoother results)\n\n\
358 Default: 128.0");
359 },
360 LutAlgorithm::ShepardsMethod => {
361 ui.separator();
362 ui.heading("Shepard's Method Arguments");
363 ui.add_space(10.);
364
365 let res = labeled_slider(ui, "Power", self.state.shepards_method.power.as_mut(), 0.001..=64.);
366 apply |= res.drag_stopped() | res.lost_focus();
367 res.on_hover_text("\
368 Power parameter for Shepard's method (Inverse Distance RBF).\n\
369 Higher values give more weight to closer palette colors.\n\n\
370 Default: 4.0");
371 },
372 LutAlgorithm::GaussianSampling => {
373 ui.separator();
374 ui.heading("Guassian Sampling Arguments");
375 ui.add_space(10.);
376
377 let res = labeled_slider(ui, "Mean", self.state.guassian_sampling.mean.as_mut(), -127.0..=127.);
378 apply |= res.drag_stopped() | res.lost_focus();
379 res.on_hover_text("\
380 Average amount of noise to apply in each iteration. \
381 Controls the bias of the random sampling process, and can lighten \
382 or darken the image overall.\n\n\
383 Default: 0.0\nRange: -127.0 to 127.0");
384
385 let res = labeled_slider(ui, "Standard Deviation", self.state.guassian_sampling.std_dev.as_mut(), 1.0..=128.);
386 apply |= res.drag_stopped() | res.lost_focus();
387 res.on_hover_text("\
388 Standard deviation parameter for the noise applied in each iteration. \
389 Controls how much variation is applied during sampling.\n\n\
390 Default: 20.0");
391
392 let res = labeled_slider(ui, "Iterations", &mut self.state.guassian_sampling.iterations, 1..=1024);
393 apply |= res.drag_stopped() | res.lost_focus();
394 res.on_hover_text("\
395 Number of iterations of noise to apply to each pixel.\n\
396 More iterations = better blending but slower processing.\n\n\
397 Default: 512");
398
399 ui.label("RNG Seed");
400 let res = ui.add(
401 egui::DragValue::new(&mut self.state.guassian_sampling.seed)
402 .speed(2i32.pow(20)),
403 );
404 apply |= res.drag_stopped() | res.lost_focus();
405 res.on_hover_text("\
406 Seed for the random number generator used in noise generation.\n\n\
407 Default: 42080085");
408 },
409 LutAlgorithm::GaussianBlur => {
410 ui.separator();
411 ui.heading("Gaussian Blur Arguments");
412 ui.add_space(10.);
413
414 let res = labeled_slider(ui, "Radius", self.state.gaussian_blur.radius.as_mut(), 1.0..=64.0);
415 apply |= res.drag_stopped() | res.lost_focus();
416 res.on_hover_text("\
417 Gaussian blur radius (sigma) applied in OKLab color space.\n\n\
418 Higher values = larger blur kernel = more color blending\n\
419 Lower values = smaller kernel = sharper boundaries\n\n\
420 Default: 8.0");
421 },
422 _ => {},
423 }
424
425 match self.state.current_alg {
427 LutAlgorithm::GaussianRbf | LutAlgorithm::ShepardsMethod => {
428 let res = labeled_slider(ui, "Nearest Colors", &mut self.state.common_rbf.nearest, 0..=32);
429 apply |= res.drag_stopped() | res.lost_focus();
430 res.on_hover_text("\
431 Number of nearest colors to consider when interpolating.\n\n\
432 0 = uses all available colors ( O(n) )\n\
433 Lower values = faster processing\n\
434 Higher values = more blending\n\n\
435 Default: 16");
436 },
437 _ => {},
438 }
439 });
440
441 ui.horizontal(|ui| {
442 let res = ui.add(
443 egui::Button::new("Copy CLI Arguments")
444 .min_size(egui::Vec2::new(ui.available_width(), 16.)),
445 );
446 if res.clicked() {
447 let args = self.state.cli_args();
448 ui.ctx()
449 .copy_text("lutgen apply ".to_string() + &args.join(" "));
450 }
451 });
452
453 apply
454 }
455
456 pub fn show_sidebar_inner(&mut self, ui: &mut egui::Ui) {
457 let mut apply = false;
458 ui.add_space(4.);
459
460 if self
462 .palette_box
463 .show(ui, &mut self.state.palette_selection)
464 .changed()
465 {
466 self.state.palette = self.state.palette_selection.get().to_vec();
467 self.palette_edit.name = self.state.palette_selection.to_string();
468 apply = true;
469 }
470
471 let [changed, saved] = self.palette_edit.show(
473 ui,
474 &mut self.state.palette,
475 &mut self.state.palette_selection,
476 );
477 apply |= changed;
478 if saved {
479 self.palette_box.reindex(&self.state.palette_selection);
480 self.state.last_event = format!(
481 "Saved custom palette to {}",
482 lutgen_dir().join(&self.palette_edit.name).display()
483 );
484 }
485
486 apply |= self.show_settings(ui);
488
489 if apply {
490 self.apply();
491 }
492 }
493
494 pub fn show_sidebar(&mut self, ctx: &egui::Context) {
496 if !self.inline_layout {
497 egui::SidePanel::left("args")
498 .resizable(true)
499 .min_width(214.)
500 .show(ctx, |ui| {
501 ui.take_available_width();
502 egui::ScrollArea::vertical().show(ui, |ui| {
503 self.show_sidebar_inner(ui);
504 });
505 });
506 }
507 }
508}