zeus_theme/
editor.rs

1use egui::{
2   Align, Button, CollapsingHeader, Color32, ComboBox, DragValue, Frame, Layout, Popup,
3   PopupCloseBehavior, Rect, Response, RichText, ScrollArea, Sense, SetOpenCommand, Slider, Stroke,
4   StrokeKind, Ui, Vec2, Window, ecolor::HexColor, vec2,
5};
6
7use super::{Theme, hsla::Hsla, utils};
8use crate::ThemeColors;
9
10/// Identify which state of the widget we should edit
11#[derive(Clone, PartialEq)]
12pub enum WidgetState {
13   NonInteractive,
14   Inactive,
15   Hovered,
16   Active,
17   Open,
18}
19
20#[derive(Clone, PartialEq)]
21pub enum Color {
22   Bg(Color32),
23   Bg2(Color32),
24   Bg3(Color32),
25   Bg4(Color32),
26   Text(Color32),
27   TextMuted(Color32),
28   Highlight(Color32),
29   Border(Color32),
30   Primary(Color32),
31   Secondary(Color32),
32   Error(Color32),
33   Warning(Color32),
34   Success(Color32),
35   Info(Color32),
36}
37
38impl Color {
39   pub fn all_colors_from(theme: &ThemeColors) -> Vec<Color> {
40      vec![
41         Color::Bg(theme.bg),
42         Color::Bg2(theme.bg2),
43         Color::Bg3(theme.bg3),
44         Color::Bg4(theme.bg4),
45         Color::Text(theme.text),
46         Color::TextMuted(theme.text_muted),
47         Color::Highlight(theme.highlight),
48         Color::Border(theme.border),
49         Color::Primary(theme.primary),
50         Color::Secondary(theme.secondary),
51         Color::Error(theme.error),
52         Color::Warning(theme.warning),
53         Color::Success(theme.success),
54         Color::Info(theme.info),
55      ]
56   }
57
58   pub fn to_str(&self) -> &'static str {
59      match self {
60         Color::Bg(_) => "Bg",
61         Color::Bg2(_) => "Bg2",
62         Color::Bg3(_) => "Bg3",
63         Color::Bg4(_) => "Bg4",
64         Color::Text(_) => "Text",
65         Color::TextMuted(_) => "Text Muted",
66         Color::Highlight(_) => "Highlight",
67         Color::Border(_) => "Border",
68         Color::Primary(_) => "Primary",
69         Color::Secondary(_) => "Secondary",
70         Color::Error(_) => "Error",
71         Color::Warning(_) => "Warning",
72         Color::Success(_) => "Success",
73         Color::Info(_) => "Info",
74      }
75   }
76
77   pub fn color32(&self) -> Color32 {
78      match self {
79         Color::Bg(color) => *color,
80         Color::Bg2(color) => *color,
81         Color::Bg3(color) => *color,
82         Color::Bg4(color) => *color,
83         Color::Text(color) => *color,
84         Color::TextMuted(color) => *color,
85         Color::Highlight(color) => *color,
86         Color::Border(color) => *color,
87         Color::Primary(color) => *color,
88         Color::Secondary(color) => *color,
89         Color::Error(color) => *color,
90         Color::Warning(color) => *color,
91         Color::Success(color) => *color,
92         Color::Info(color) => *color,
93      }
94   }
95
96   pub fn name_from(color: Color32, theme_colors: &ThemeColors) -> &'static str {
97      if color == theme_colors.bg {
98         "Bg"
99      } else if color == theme_colors.bg2 {
100         "Bg2"
101      } else if color == theme_colors.bg3 {
102         "Bg3"
103      } else if color == theme_colors.bg4 {
104         "Bg4"
105      } else if color == theme_colors.text {
106         "Text"
107      } else if color == theme_colors.text_muted {
108         "Text Muted"
109      } else if color == theme_colors.highlight {
110         "Highlight"
111      } else if color == theme_colors.border {
112         "Border"
113      } else if color == theme_colors.primary {
114         "Primary"
115      } else if color == theme_colors.secondary {
116         "Secondary"
117      } else if color == theme_colors.error {
118         "Error"
119      } else if color == theme_colors.warning {
120         "Warning"
121      } else if color == theme_colors.success {
122         "Success"
123      } else if color == theme_colors.info {
124         "Info"
125      } else {
126         "Unknown"
127      }
128   }
129}
130
131impl WidgetState {
132   /// Convert the state to a string
133   pub fn to_str(&self) -> &'static str {
134      match self {
135         WidgetState::NonInteractive => "Non-interactive",
136         WidgetState::Inactive => "Inactive",
137         WidgetState::Hovered => "Hovered",
138         WidgetState::Active => "Active",
139         WidgetState::Open => "Open",
140      }
141   }
142
143   /// Convert the state to a vector
144   pub fn to_vec(&self) -> Vec<WidgetState> {
145      let non_interactive = Self::NonInteractive;
146      let inactive = Self::Inactive;
147      let hovered = Self::Hovered;
148      let active = Self::Active;
149      let open = Self::Open;
150
151      vec![non_interactive, inactive, hovered, active, open]
152   }
153}
154
155#[derive(Clone)]
156pub struct ThemeEditor {
157   pub open: bool,
158   /// The current widget state being edited
159   pub widget_state: WidgetState,
160   pub color: Color,
161   pub bg_color: Color32,
162   pub size: (f32, f32),
163}
164
165impl ThemeEditor {
166   pub fn new() -> Self {
167      Self {
168         open: false,
169         widget_state: WidgetState::NonInteractive,
170         color: Color::Bg(Color32::TRANSPARENT),
171         bg_color: Color32::from_rgba_premultiplied(32, 45, 70, 255),
172         size: (300.0, 300.0),
173      }
174   }
175
176   /// Show the theme editor in a window
177   ///
178   /// Returns the new theme if we change it
179   pub fn show(&mut self, theme: &mut Theme, ui: &mut Ui) -> Option<Theme> {
180      if !self.open {
181         return None;
182      }
183
184      let mut open = self.open;
185      let mut new_theme = None;
186      let frame = Frame::window(ui.style()).fill(self.bg_color);
187
188      Window::new("Theme Editor")
189         .open(&mut open)
190         .resizable([true, true])
191         .frame(frame)
192         .show(ui.ctx(), |ui| {
193            ui.set_min_width(self.size.0);
194            ui.set_min_height(self.size.1);
195            ui.spacing_mut().button_padding = vec2(10.0, 8.0);
196            ui.style_mut().visuals = super::themes::dark::theme().style.visuals.clone();
197
198            new_theme = utils::change_theme(theme, ui);
199
200            ui.add_space(20.0);
201
202            ScrollArea::vertical().show(ui, |ui| {
203               ui.set_width(self.size.0);
204               ui.set_height(self.size.1);
205               self.ui(theme, ui);
206            });
207         });
208      self.open = open;
209      new_theme
210   }
211
212   /// Show the ui for the theme editor
213   pub fn ui(&mut self, theme: &mut Theme, ui: &mut Ui) {
214      ui.vertical_centered(|ui| {
215         ui.spacing_mut().item_spacing.y = 10.0;
216
217         CollapsingHeader::new("Theme Frames").show(ui, |ui| {
218            CollapsingHeader::new("Native Window Frame").show(ui, |ui| {
219               self.frame_settings(&mut theme.window_frame, ui);
220            });
221
222            CollapsingHeader::new("Frame 1").show(ui, |ui| {
223               self.frame_settings(&mut theme.frame1, ui);
224            });
225
226            CollapsingHeader::new("Frame 2").show(ui, |ui| {
227               self.frame_settings(&mut theme.frame2, ui);
228            });
229         });
230
231         CollapsingHeader::new("Theme Colors").show(ui, |ui| {
232            ui.label("BG");
233            hsla_edit_button("bg1", ui, &mut theme.colors.bg);
234
235            ui.label("BG2");
236            hsla_edit_button("bg2", ui, &mut theme.colors.bg2);
237
238            ui.label("BG3");
239            hsla_edit_button("bg3", ui, &mut theme.colors.bg3);
240
241            ui.label("BG4");
242            hsla_edit_button("bg4", ui, &mut theme.colors.bg4);
243
244            ui.label("Text");
245            hsla_edit_button("text1", ui, &mut theme.colors.text);
246
247            ui.label("Text Muted");
248            hsla_edit_button("text_muted1", ui, &mut theme.colors.text_muted);
249
250            ui.label("Highlight");
251            hsla_edit_button("highlight1", ui, &mut theme.colors.highlight);
252
253            ui.label("Border");
254            hsla_edit_button("border1", ui, &mut theme.colors.border);
255
256            ui.label("Primary");
257            hsla_edit_button("primary1", ui, &mut theme.colors.primary);
258
259            ui.label("Secondary");
260            hsla_edit_button("secondary1", ui, &mut theme.colors.secondary);
261
262            ui.label("Error");
263            hsla_edit_button("error1", ui, &mut theme.colors.error);
264
265            ui.label("Warning");
266            hsla_edit_button("warning1", ui, &mut theme.colors.warning);
267
268            ui.label("Success");
269            hsla_edit_button("success1", ui, &mut theme.colors.success);
270
271            ui.label("Info");
272            hsla_edit_button("info1", ui, &mut theme.colors.info);
273         });
274
275         CollapsingHeader::new("Text Sizes").show(ui, |ui| {
276            ui.label("Very Small");
277            ui.add(Slider::new(&mut theme.text_sizes.very_small, 0.0..=100.0).text("Size"));
278
279            ui.label("Small");
280            ui.add(Slider::new(&mut theme.text_sizes.small, 0.0..=100.0).text("Size"));
281
282            ui.label("Normal");
283            ui.add(Slider::new(&mut theme.text_sizes.normal, 0.0..=100.0).text("Size"));
284
285            ui.label("Large");
286            ui.add(Slider::new(&mut theme.text_sizes.large, 0.0..=100.0).text("Size"));
287
288            ui.label("Very Large");
289            ui.add(Slider::new(&mut theme.text_sizes.very_large, 0.0..=100.0).text("Size"));
290
291            ui.label("Heading");
292            ui.add(Slider::new(&mut theme.text_sizes.heading, 0.0..=100.0).text("Size"));
293         });
294
295         CollapsingHeader::new("Other Colors").show(ui, |ui| {
296            ui.label("Selection Stroke");
297            ui.add(
298               Slider::new(
299                  &mut theme.style.visuals.selection.stroke.width,
300                  0.0..=10.0,
301               )
302               .text("Stroke Width"),
303            );
304            ui.label("Selection Stroke Color");
305            hsla_edit_button(
306               "selection_stroke_color1",
307               ui,
308               &mut theme.style.visuals.selection.stroke.color,
309            );
310
311            ui.label("Selection Bg Fill");
312            hsla_edit_button(
313               "selection_bg_fill1",
314               ui,
315               &mut theme.style.visuals.selection.bg_fill,
316            );
317
318            ui.label("Hyperlink Color");
319            hsla_edit_button(
320               "hyperlink_color1",
321               ui,
322               &mut theme.style.visuals.hyperlink_color,
323            );
324
325            ui.label("Faint Background Color");
326            hsla_edit_button(
327               "faint_bg_color1",
328               ui,
329               &mut theme.style.visuals.faint_bg_color,
330            );
331
332            ui.label("Extreme Background Color");
333            hsla_edit_button(
334               "extreme_bg_color1",
335               ui,
336               &mut theme.style.visuals.extreme_bg_color,
337            );
338
339            ui.label("Code Background Color");
340            hsla_edit_button(
341               "code_bg_color1",
342               ui,
343               &mut theme.style.visuals.code_bg_color,
344            );
345
346            ui.label("Warning Text Color");
347            hsla_edit_button(
348               "warn_fg_color1",
349               ui,
350               &mut theme.style.visuals.warn_fg_color,
351            );
352
353            ui.label("Error Text Color");
354            hsla_edit_button(
355               "error_fg_color1",
356               ui,
357               &mut theme.style.visuals.error_fg_color,
358            );
359
360            ui.label("Panel Fill Color");
361            hsla_edit_button(
362               "panel_fill1",
363               ui,
364               &mut theme.style.visuals.panel_fill,
365            );
366         });
367
368         CollapsingHeader::new("Window Visuals").show(ui, |ui| {
369            ui.label("Window Rounding");
370            ui.add(
371               Slider::new(
372                  &mut theme.style.visuals.window_corner_radius.nw,
373                  0..=35,
374               )
375               .text("Top Left"),
376            );
377
378            ui.add(
379               Slider::new(
380                  &mut theme.style.visuals.window_corner_radius.ne,
381                  0..=35,
382               )
383               .text("Top Right"),
384            );
385
386            ui.add(
387               Slider::new(
388                  &mut theme.style.visuals.window_corner_radius.sw,
389                  0..=35,
390               )
391               .text("Bottom Left"),
392            );
393
394            ui.add(
395               Slider::new(
396                  &mut theme.style.visuals.window_corner_radius.se,
397                  0..=35,
398               )
399               .text("Bottom Right"),
400            );
401
402            ui.label("Window Shadow");
403            ui.add(
404               Slider::new(
405                  &mut theme.style.visuals.window_shadow.offset[0],
406                  -100..=100,
407               )
408               .text("Offset X"),
409            );
410
411            ui.add(
412               Slider::new(
413                  &mut theme.style.visuals.window_shadow.offset[1],
414                  -100..=100,
415               )
416               .text("Offset Y"),
417            );
418
419            ui.add(
420               Slider::new(
421                  &mut theme.style.visuals.window_shadow.blur,
422                  0..=100,
423               )
424               .text("Blur"),
425            );
426
427            ui.add(
428               Slider::new(
429                  &mut theme.style.visuals.window_shadow.spread,
430                  0..=100,
431               )
432               .text("Spread"),
433            );
434
435            ui.label("Shadow Color");
436            hsla_edit_button(
437               "window_shadow_color1",
438               ui,
439               &mut theme.style.visuals.window_shadow.color,
440            );
441
442            ui.label("Window Fill Color");
443            hsla_edit_button(
444               "window_fill1",
445               ui,
446               &mut theme.style.visuals.window_fill,
447            );
448
449            ui.label("Window Stroke Color");
450            ui.add(
451               Slider::new(
452                  &mut theme.style.visuals.window_stroke.width,
453                  0.0..=10.0,
454               )
455               .text("Stroke Width"),
456            );
457
458            hsla_edit_button(
459               "window_stroke_color1",
460               ui,
461               &mut theme.style.visuals.window_stroke.color,
462            );
463
464            ui.label("Window Highlight Topmost");
465            ui.checkbox(
466               &mut theme.style.visuals.window_highlight_topmost,
467               "Highlight Topmost",
468            );
469         });
470
471         CollapsingHeader::new("Popup Shadow").show(ui, |ui| {
472            ui.add(
473               Slider::new(
474                  &mut theme.style.visuals.popup_shadow.offset[0],
475                  -100..=100,
476               )
477               .text("Offset X"),
478            );
479
480            ui.add(
481               Slider::new(
482                  &mut theme.style.visuals.popup_shadow.offset[1],
483                  -100..=100,
484               )
485               .text("Offset Y"),
486            );
487
488            ui.add(
489               Slider::new(
490                  &mut theme.style.visuals.popup_shadow.blur,
491                  0..=100,
492               )
493               .text("Blur"),
494            );
495
496            ui.add(
497               Slider::new(
498                  &mut theme.style.visuals.popup_shadow.spread,
499                  0..=100,
500               )
501               .text("Spread"),
502            );
503
504            hsla_edit_button(
505               "popup_shadow_color1",
506               ui,
507               &mut theme.style.visuals.popup_shadow.color,
508            );
509         });
510
511         CollapsingHeader::new("Menu Rounding").show(ui, |ui| {
512            ui.add(
513               Slider::new(
514                  &mut theme.style.visuals.menu_corner_radius.nw,
515                  0..=35,
516               )
517               .text("Top Left"),
518            );
519
520            ui.add(
521               Slider::new(
522                  &mut theme.style.visuals.menu_corner_radius.ne,
523                  0..=35,
524               )
525               .text("Top Right"),
526            );
527
528            ui.add(
529               Slider::new(
530                  &mut theme.style.visuals.menu_corner_radius.sw,
531                  0..=35,
532               )
533               .text("Bottom Left"),
534            );
535
536            ui.add(
537               Slider::new(
538                  &mut theme.style.visuals.menu_corner_radius.se,
539                  0..=35,
540               )
541               .text("Bottom Right"),
542            );
543         });
544
545         CollapsingHeader::new("Widget Visuals").show(ui, |ui| {
546            self.widget_settings(theme, ui);
547         });
548
549         CollapsingHeader::new("Other Settings").show(ui, |ui| {
550            ui.label("Resize Corner Size");
551            ui.add(
552               Slider::new(
553                  &mut theme.style.visuals.resize_corner_size,
554                  0.0..=100.0,
555               )
556               .text("Corner Size"),
557            );
558
559            ui.label("Button Frame");
560            ui.checkbox(
561               &mut theme.style.visuals.button_frame,
562               "Button Frame",
563            );
564         });
565
566         CollapsingHeader::new("Tessellation").show(ui, |ui| {
567            self.tesellation_settings(theme, ui);
568         });
569      });
570   }
571
572   fn tesellation_settings(&mut self, theme: &Theme, ui: &mut Ui) {
573      let text_size = theme.text_sizes.normal;
574
575      let mut options = ui.ctx().tessellation_options(|options| options.clone());
576
577      let text = RichText::new("Feathering").size(text_size);
578
579      ui.checkbox(&mut options.feathering, text);
580
581      ui.add(
582         DragValue::new(&mut options.feathering_size_in_pixels)
583            .speed(0.1)
584            .range(0.0..=100.0),
585      );
586
587      let text = RichText::new("Coarse tessellation culling").size(text_size);
588      ui.checkbox(&mut options.coarse_tessellation_culling, text);
589
590      let text = RichText::new("Precomputed discs").size(text_size);
591      ui.checkbox(&mut options.prerasterized_discs, text);
592
593      let text = RichText::new("Round text to pixels").size(text_size);
594      ui.checkbox(&mut options.round_text_to_pixels, text);
595
596      let text = RichText::new("Round line segments to pixels").size(text_size);
597      ui.checkbox(&mut options.round_line_segments_to_pixels, text);
598
599      let text = RichText::new("Round rects to pixels").size(text_size);
600      ui.checkbox(&mut options.round_rects_to_pixels, text);
601
602      let text = RichText::new("Debug paint text rects").size(text_size);
603      ui.checkbox(&mut options.debug_paint_text_rects, text);
604
605      let text = RichText::new("Debug paint clip rects").size(text_size);
606      ui.checkbox(&mut options.debug_paint_clip_rects, text);
607
608      let text = RichText::new("Debug ignore clip rects").size(text_size);
609      ui.checkbox(&mut options.debug_ignore_clip_rects, text);
610
611      let text = RichText::new("Bezier tolerance").size(text_size);
612      ui.label(text);
613      ui.add(DragValue::new(&mut options.bezier_tolerance).speed(0.1).range(0.0..=1.0));
614
615      let text = RichText::new("Epsilon").size(text_size);
616      ui.label(text);
617      ui.add(DragValue::new(&mut options.epsilon).speed(0.1).range(0.0..=1.0));
618
619      let text = RichText::new("Parallel tessellation").size(text_size);
620      ui.checkbox(&mut options.parallel_tessellation, text);
621
622      let text = RichText::new("Validate meshes").size(text_size);
623      ui.checkbox(&mut options.validate_meshes, text);
624
625      ui.ctx().tessellation_options_mut(|options_mut| {
626         *options_mut = options;
627      });
628   }
629
630   fn widget_settings(&mut self, theme: &mut Theme, ui: &mut Ui) {
631      self.select_widget_state(ui);
632
633      let widget_visuals = match self.widget_state {
634         WidgetState::NonInteractive => &mut theme.style.visuals.widgets.noninteractive,
635         WidgetState::Inactive => &mut theme.style.visuals.widgets.inactive,
636         WidgetState::Hovered => &mut theme.style.visuals.widgets.hovered,
637         WidgetState::Active => &mut theme.style.visuals.widgets.active,
638         WidgetState::Open => &mut theme.style.visuals.widgets.open,
639      };
640
641      ui.label("Background Fill Color");
642
643      ui.horizontal(|ui| {
644         let color = self.color_select("1", widget_visuals.bg_fill, &theme.colors, ui);
645         if let Some(color) = color {
646            widget_visuals.bg_fill = color.color32();
647         }
648
649         hsla_edit_button("bg_fill1", ui, &mut widget_visuals.bg_fill);
650      });
651
652      ui.label("Weak Background Fill Color");
653
654      ui.horizontal(|ui| {
655         let color = self.color_select(
656            "2",
657            widget_visuals.weak_bg_fill,
658            &theme.colors,
659            ui,
660         );
661         if let Some(color) = color {
662            widget_visuals.weak_bg_fill = color.color32();
663         }
664
665         hsla_edit_button(
666            "weak_bg_fill1",
667            ui,
668            &mut widget_visuals.weak_bg_fill,
669         );
670      });
671
672      ui.label("Background Stroke Width");
673      ui.add(Slider::new(
674         &mut widget_visuals.bg_stroke.width,
675         0.0..=10.0,
676      ));
677
678      ui.label("Background Stroke Color");
679      ui.horizontal(|ui| {
680         let color = self.color_select(
681            "3",
682            widget_visuals.bg_stroke.color,
683            &theme.colors,
684            ui,
685         );
686         if let Some(color) = color {
687            widget_visuals.bg_stroke.color = color.color32();
688         }
689
690         hsla_edit_button(
691            "bg_stroke_color1",
692            ui,
693            &mut widget_visuals.bg_stroke.color,
694         );
695      });
696
697      ui.label("Rounding");
698      ui.add(Slider::new(&mut widget_visuals.corner_radius.nw, 0..=255).text("Top Left"));
699      ui.add(Slider::new(&mut widget_visuals.corner_radius.ne, 0..=255).text("Top Right"));
700      ui.add(Slider::new(&mut widget_visuals.corner_radius.sw, 0..=255).text("Bottom Left"));
701      ui.add(Slider::new(&mut widget_visuals.corner_radius.se, 0..=255).text("Bottom Right"));
702
703      ui.label("Foreground Stroke Width");
704      ui.add(Slider::new(
705         &mut widget_visuals.fg_stroke.width,
706         0.0..=10.0,
707      ));
708
709      ui.label("Foreground Stroke Color");
710      ui.horizontal(|ui| {
711         let color = self.color_select(
712            "4",
713            widget_visuals.fg_stroke.color,
714            &theme.colors,
715            ui,
716         );
717
718         if let Some(color) = color {
719            widget_visuals.fg_stroke.color = color.color32();
720         }
721
722         hsla_edit_button(
723            "fg_stroke_color1",
724            ui,
725            &mut widget_visuals.fg_stroke.color,
726         );
727      });
728
729      ui.label("Expansion");
730      ui.add(Slider::new(&mut widget_visuals.expansion, 0.0..=100.0).text("Expansion"));
731   }
732
733   fn frame_settings(&mut self, frame: &mut Frame, ui: &mut Ui) {
734      CollapsingHeader::new("Inner & Outter Margin").show(ui, |ui| {
735         ui.label("Inner Margin");
736         ui.add(Slider::new(&mut frame.inner_margin.top, 0..=127).text("Top"));
737         ui.add(Slider::new(&mut frame.inner_margin.bottom, 0..=127).text("Bottom"));
738         ui.add(Slider::new(&mut frame.inner_margin.left, 0..=127).text("Left"));
739         ui.add(Slider::new(&mut frame.inner_margin.right, 0..=127).text("Right"));
740
741         ui.label("Outter Margin");
742         ui.add(Slider::new(&mut frame.outer_margin.top, 0..=127).text("Top"));
743         ui.add(Slider::new(&mut frame.outer_margin.bottom, 0..=127).text("Bottom"));
744         ui.add(Slider::new(&mut frame.outer_margin.left, 0..=127).text("Left"));
745         ui.add(Slider::new(&mut frame.outer_margin.right, 0..=127).text("Right"));
746      });
747
748      ui.label("Rounding");
749      ui.add(Slider::new(&mut frame.corner_radius.nw, 0..=255).text("Top Left"));
750      ui.add(Slider::new(&mut frame.corner_radius.ne, 0..=255).text("Top Right"));
751      ui.add(Slider::new(&mut frame.corner_radius.sw, 0..=255).text("Bottom Left"));
752      ui.add(Slider::new(&mut frame.corner_radius.se, 0..=255).text("Bottom Right"));
753
754      ui.label("Shadow");
755      ui.add(Slider::new(&mut frame.shadow.offset[0], -128..=127).text("Offset X"));
756      ui.add(Slider::new(&mut frame.shadow.offset[1], -128..=127).text("Offset Y"));
757      ui.add(Slider::new(&mut frame.shadow.blur, 0..=255).text("Blur"));
758      ui.add(Slider::new(&mut frame.shadow.spread, 0..=255).text("Spread"));
759
760      ui.label("Shadow Color");
761      hsla_edit_button("shadow_color1", ui, &mut frame.shadow.color);
762
763      ui.label("Fill Color");
764      hsla_edit_button("fill_color1", ui, &mut frame.fill);
765
766      ui.label("Stroke Width & Color");
767      ui.add(Slider::new(&mut frame.stroke.width, 0.0..=100.0).text("Stroke Width"));
768      hsla_edit_button("stroke_color1", ui, &mut frame.stroke.color);
769   }
770
771   fn select_widget_state(&mut self, ui: &mut Ui) {
772      ComboBox::from_label("")
773         .selected_text(self.widget_state.to_str())
774         .show_ui(ui, |ui| {
775            for widget in self.widget_state.to_vec() {
776               let value = ui.selectable_value(
777                  &mut self.widget_state,
778                  widget.clone(),
779                  widget.to_str(),
780               );
781
782               if value.clicked() {
783                  self.widget_state = widget;
784               }
785            }
786         });
787   }
788
789   fn color_select(
790      &mut self,
791      id: &str,
792      current_color: Color32,
793      colors: &ThemeColors,
794      ui: &mut Ui,
795   ) -> Option<Color> {
796      let all_colors = Color::all_colors_from(colors);
797
798      let mut selected_color = None;
799      let current_color_name = Color::name_from(current_color, colors);
800
801      ComboBox::from_id_salt(id).selected_text(current_color_name).show_ui(ui, |ui| {
802         for color in all_colors {
803            let value = ui.selectable_value(&mut self.color, color.clone(), color.to_str());
804
805            if value.clicked() {
806               selected_color = Some(color);
807            }
808         }
809      });
810      selected_color
811   }
812}
813
814pub fn hsla_edit_button(id: &str, ui: &mut Ui, color32: &mut Color32) -> Response {
815   let stroke = Stroke::new(1.0, Color32::GRAY);
816   let button_size = Vec2::new(50.0, 20.0);
817   let (rect, mut response) = ui.allocate_exact_size(button_size, Sense::click());
818   ui.painter().rect_filled(rect, 4.0, *color32);
819   ui.painter().rect_stroke(rect, 4.0, stroke, StrokeKind::Inside);
820
821   let popup_id = ui.make_persistent_id(id);
822
823   let set_command = if response.clicked() {
824      Some(SetOpenCommand::Toggle)
825   } else {
826      None
827   };
828
829   let close_behavior = PopupCloseBehavior::CloseOnClickOutside;
830   let popup = Popup::from_response(&response)
831      .close_behavior(close_behavior)
832      .open_memory(set_command);
833
834   let working_id = popup_id.with("working_hsla");
835   let mut working_hsla = ui
836      .memory(|mem| mem.data.get_temp(working_id))
837      .unwrap_or_else(|| Hsla::from_color32(*color32));
838
839   let popup_res = popup.show(|ui| hsla_picker_ui(ui, &mut working_hsla));
840
841   if let Some(inner) = popup_res {
842      // if color changed
843      if inner.inner {
844         ui.memory_mut(|mem| mem.data.insert_temp(working_id, working_hsla));
845         *color32 = working_hsla.to_color32();
846         response.mark_changed();
847      }
848   } else {
849      ui.memory_mut(|mem| mem.data.remove::<Hsla>(working_id));
850   }
851
852   response
853}
854
855// The core HSLA picker UI (sliders, 2D square, preview). Returns true if changed.
856fn hsla_picker_ui(ui: &mut Ui, hsla: &mut Hsla) -> bool {
857   let mut changed = false;
858   let stroke = Stroke::new(1.0, Color32::GRAY);
859
860   ui.horizontal(|ui| {
861      ui.set_width(200.0);
862
863      // Left: 2D S-L square + hue slider below it
864      ui.vertical(|ui| {
865         changed |= sl_2d_picker(ui, hsla);
866         changed |= hue_slider(ui, hsla);
867         changed |= alpha_slider(ui, hsla);
868      });
869
870      // Right: Preview + numeric controls
871      ui.vertical(|ui| {
872         // Preview rect
873         let preview_size = Vec2::new(80.0, 80.0);
874         let (rect, _) = ui.allocate_exact_size(preview_size, Sense::hover());
875         ui.painter().rect_filled(rect, 4.0, hsla.to_color32());
876         ui.painter().rect_stroke(rect, 4.0, stroke, StrokeKind::Inside);
877
878         ui.label(RichText::new("Preview").strong());
879
880         // Numeric sliders for precision
881         ui.add_space(10.0);
882         changed |= ui.add(Slider::new(&mut hsla.h, 0.0..=360.0).text("Hue")).changed();
883         changed |= ui.add(Slider::new(&mut hsla.s, 0.0..=100.0).text("Saturation")).changed();
884         changed |= ui.add(Slider::new(&mut hsla.l, 0.0..=100.0).text("Lightness")).changed();
885         changed |= ui.add(Slider::new(&mut hsla.a, 0.0..=1.0).text("Alpha")).changed();
886      });
887
888      ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
889         ui.vertical(|ui| {
890            // RGBA copy button
891            ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
892               let (r, g, b, a) = hsla.to_rgba_components();
893               let text = RichText::new(format!("RGBA ({r}, {g}, {b}, {a})"));
894               let button = Button::new(text).min_size(vec2(160.0, 15.0));
895               if ui.add(button).clicked() {
896                  ui.ctx().copy_text(format!("({r}, {g}, {b}, {a})"));
897               }
898            });
899
900            // HEX copy button
901            ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
902               let hex_color = HexColor::Hex6(hsla.to_color32());
903               let text = RichText::new(format!("HEX {}", hex_color));
904               let button = Button::new(text).min_size(vec2(160.0, 15.0));
905               if ui.add(button).clicked() {
906                  ui.ctx().copy_text(format!("{}", hex_color));
907               }
908            });
909         });
910      });
911   });
912
913   changed
914}
915
916// 2D picker for Saturation (x) and Lightness (y)
917fn sl_2d_picker(ui: &mut Ui, hsla: &mut Hsla) -> bool {
918   let size = Vec2::new(150.0, 150.0);
919   let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
920
921   let mut changed = false;
922
923   if response.dragged() {
924      if let Some(pos) = response.hover_pos() {
925         let relative = pos - rect.min;
926         hsla.s = (relative.x / size.x).clamp(0.0, 1.0) * 100.0;
927         hsla.l = (1.0 - (relative.y / size.y)).clamp(0.0, 1.0) * 100.0; // Top: high L, bottom: low L
928         changed = true;
929      }
930   }
931
932   // Paint gradient background (grid of small rects for simplicity)
933   let painter = ui.painter();
934   const RES: usize = 64; // Higher for smoother, but 64 is fast and looks good
935   let cell_size = size / RES as f32;
936   for i in 0..RES {
937      for j in 0..RES {
938         let s = (i as f32 / (RES - 1) as f32) * 100.0;
939         let l = (1.0 - (j as f32 / (RES - 1) as f32)) * 100.0; // Top: l=100, bottom: l=0
940         let temp_hsla = Hsla {
941            h: hsla.h,
942            s,
943            l,
944            a: 1.0,
945         };
946         let color = temp_hsla.to_color32();
947
948         let min = rect.min + Vec2::new(i as f32 * cell_size.x, j as f32 * cell_size.y);
949         let cell_rect = Rect::from_min_size(min, cell_size);
950         painter.rect_filled(cell_rect, 0.0, color);
951      }
952   }
953
954   // Draw cursor at current position
955   let x = (hsla.s / 100.0) * size.x;
956   let y = (1.0 - hsla.l / 100.0) * size.y;
957   let cursor_pos = rect.min + Vec2::new(x, y);
958   painter.circle_stroke(cursor_pos, 5.0, Stroke::new(1.0, Color32::WHITE));
959   painter.circle_stroke(cursor_pos, 5.0, Stroke::new(1.0, Color32::BLACK));
960
961   // Outline the square
962   painter.rect_stroke(
963      rect,
964      0.0,
965      Stroke::new(1.0, Color32::GRAY),
966      StrokeKind::Inside,
967   );
968
969   changed
970}
971
972// Hue gradient slider
973fn hue_slider(ui: &mut Ui, hsla: &mut Hsla) -> bool {
974   let size = Vec2::new(150.0, 20.0);
975   let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
976
977   let mut changed = false;
978
979   if response.dragged() {
980      if let Some(pos) = response.hover_pos() {
981         let relative_x = (pos.x - rect.min.x) / size.x;
982         hsla.h = relative_x.clamp(0.0, 1.0) * 360.0;
983         changed = true;
984      }
985   }
986
987   // Paint rainbow gradient
988   let painter = ui.painter();
989   const RES: usize = 128; // Smooth horizontal gradient
990   let cell_width = size.x / RES as f32;
991   for i in 0..RES {
992      let h = (i as f32 / (RES - 1) as f32) * 360.0;
993      let temp_hsla = Hsla {
994         h,
995         s: 100.0,
996         l: 50.0,
997         a: 1.0,
998      }; // Full sat, mid light for vibrant rainbow
999      let color = temp_hsla.to_color32();
1000
1001      let min = rect.min + Vec2::new(i as f32 * cell_width, 0.0);
1002      let cell_rect = Rect::from_min_size(min, Vec2::new(cell_width, size.y));
1003      painter.rect_filled(cell_rect, 0.0, color);
1004   }
1005
1006   // Cursor indicator (vertical line)
1007   let x = (hsla.h / 360.0) * size.x;
1008   let line_start = rect.min + Vec2::new(x, 0.0);
1009   let line_end = rect.min + Vec2::new(x, size.y);
1010   painter.line_segment(
1011      [line_start, line_end],
1012      Stroke::new(2.0, Color32::WHITE),
1013   );
1014
1015   // Outline
1016   painter.rect_stroke(
1017      rect,
1018      4.0,
1019      Stroke::new(1.0, Color32::GRAY),
1020      StrokeKind::Inside,
1021   );
1022
1023   changed
1024}
1025
1026fn alpha_slider(ui: &mut Ui, hsla: &mut Hsla) -> bool {
1027   let size = Vec2::new(150.0, 20.0);
1028   let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
1029   let mut changed = false;
1030
1031   if response.dragged() {
1032      if let Some(pos) = response.hover_pos() {
1033         let relative_x = (pos.x - rect.min.x) / size.x;
1034         hsla.a = relative_x.clamp(0.0, 1.0);
1035         changed = true;
1036      }
1037   }
1038
1039   let painter = ui.painter();
1040   // Paint checkerboard FIRST for transparency visibility
1041   let checker_size = 5.0;
1042
1043   for x in (0..=((size.x / checker_size) as usize)).step_by(1) {
1044      for y in (0..=((size.y / checker_size) as usize)).step_by(1) {
1045         let color = if (x + y) % 2 == 0 {
1046            Color32::GRAY
1047         } else {
1048            Color32::LIGHT_GRAY
1049         };
1050         let min = rect.min + Vec2::new(x as f32 * checker_size, y as f32 * checker_size);
1051         let cell_rect = Rect::from_min_size(min, Vec2::splat(checker_size)).intersect(rect);
1052         painter.rect_filled(cell_rect, 0.0, color);
1053      }
1054   }
1055
1056   // Then paint gradient on top
1057   const RES: usize = 64;
1058   let cell_width = size.x / RES as f32;
1059
1060   for i in 0..RES {
1061      let a = i as f32 / (RES - 1) as f32;
1062      let temp_hsla = Hsla {
1063         h: hsla.h,
1064         s: hsla.s,
1065         l: hsla.l,
1066         a,
1067      };
1068
1069      let color = temp_hsla.to_color32();
1070      let min = rect.min + Vec2::new(i as f32 * cell_width, 0.0);
1071      let cell_rect = Rect::from_min_size(min, Vec2::new(cell_width, size.y));
1072      painter.rect_filled(cell_rect, 0.0, color);
1073   }
1074
1075   // Cursor line
1076   let x = hsla.a * size.x;
1077   let line_start = rect.min + Vec2::new(x, 0.0);
1078   let line_end = rect.min + Vec2::new(x, size.y);
1079
1080   painter.line_segment(
1081      [line_start, line_end],
1082      Stroke::new(2.0, Color32::WHITE),
1083   );
1084
1085   // Outline
1086   painter.rect_stroke(
1087      rect,
1088      4.0,
1089      Stroke::new(1.0, Color32::GRAY),
1090      StrokeKind::Inside,
1091   );
1092   changed
1093}