Skip to main content

egui_elements/
editor.rs

1//! Live theme editor window.
2//!
3//! [`ThemeEditor::show`] mutates `theme` in place and only returns `Some`
4//! when the inner switcher picks a new [`crate::theme::ThemeKind`]. Always
5//! call [`crate::theme::Theme::install`] after `show`, not only on `Some`.
6
7use egui::{
8   Align, Button, CollapsingHeader, Color32, ComboBox, CornerRadius, DragValue, Frame, Layout,
9   Margin, Order, Popup, PopupCloseBehavior, Rect, Response, RichText, ScrollArea, Sense,
10   SetOpenCommand, Shadow, Slider, Stroke, StrokeKind, TextEdit, Ui, Vec2,
11   color_picker::{Alpha, color_edit_button_srgba},
12   ecolor::HexColor,
13   vec2,
14};
15
16use crate::theme::{Theme, ThemeColors};
17use crate::utils::{self, Hsla};
18use crate::visuals::*;
19use crate::widgets::Window;
20
21const DARK: Color32 = Color32::from_rgba_premultiplied(22, 22, 30, 255);
22
23/// Identify which state of the widget we should edit
24#[derive(Clone, PartialEq)]
25pub enum WidgetState {
26   /// Not interactive.
27   NonInteractive,
28   /// Idle, enabled.
29   Inactive,
30   /// Pointer over the widget.
31   Hovered,
32   /// Pointer down / focused.
33   Active,
34   /// Popup or combo is open.
35   Open,
36}
37
38/// Named palette slot, holding the current [`Color32`] for that slot.
39#[derive(Clone, PartialEq)]
40pub enum Color {
41   Bg(Color32),
42   WidgetBG(Color32),
43   Hover(Color32),
44   Text(Color32),
45   TextMuted(Color32),
46   Highlight(Color32),
47   Border(Color32),
48   Accent(Color32),
49   Error(Color32),
50   Warning(Color32),
51   Success(Color32),
52   Info(Color32),
53}
54
55impl Color {
56   /// Every named slot, filled from `theme`.
57   pub fn all_colors_from(theme: &ThemeColors) -> Vec<Color> {
58      vec![
59         Color::Bg(theme.bg),
60         Color::WidgetBG(theme.widget_bg),
61         Color::Hover(theme.hover),
62         Color::Text(theme.text),
63         Color::TextMuted(theme.text_muted),
64         Color::Highlight(theme.highlight),
65         Color::Border(theme.border),
66         Color::Accent(theme.accent),
67         Color::Error(theme.error),
68         Color::Warning(theme.warning),
69         Color::Success(theme.success),
70         Color::Info(theme.info),
71      ]
72   }
73
74   /// Short label for this slot.
75   pub fn to_str(&self) -> &'static str {
76      match self {
77         Color::Bg(_) => "Bg",
78         Color::WidgetBG(_) => "WidgetBG",
79         Color::Hover(_) => "Hover",
80         Color::Text(_) => "Text",
81         Color::TextMuted(_) => "Text Muted",
82         Color::Highlight(_) => "Highlight",
83         Color::Border(_) => "Border",
84         Color::Accent(_) => "Accent",
85         Color::Error(_) => "Error",
86         Color::Warning(_) => "Warning",
87         Color::Success(_) => "Success",
88         Color::Info(_) => "Info",
89      }
90   }
91
92   /// The wrapped [`Color32`].
93   pub fn color32(&self) -> Color32 {
94      match self {
95         Color::Bg(color) => *color,
96         Color::WidgetBG(color) => *color,
97         Color::Hover(color) => *color,
98         Color::Text(color) => *color,
99         Color::TextMuted(color) => *color,
100         Color::Highlight(color) => *color,
101         Color::Border(color) => *color,
102         Color::Accent(color) => *color,
103         Color::Error(color) => *color,
104         Color::Warning(color) => *color,
105         Color::Success(color) => *color,
106         Color::Info(color) => *color,
107      }
108   }
109
110   /// Slot name if `color` matches a palette entry, otherwise `"Unknown"`.
111   pub fn name_from(color: Color32, theme_colors: &ThemeColors) -> &'static str {
112      if color == theme_colors.bg {
113         "Bg"
114      } else if color == theme_colors.widget_bg {
115         "WidgetBG"
116      } else if color == theme_colors.hover {
117         "Hover"
118      } else if color == theme_colors.text {
119         "Text"
120      } else if color == theme_colors.text_muted {
121         "Text Muted"
122      } else if color == theme_colors.highlight {
123         "Highlight"
124      } else if color == theme_colors.border {
125         "Border"
126      } else if color == theme_colors.accent {
127         "Accent"
128      } else if color == theme_colors.error {
129         "Error"
130      } else if color == theme_colors.warning {
131         "Warning"
132      } else if color == theme_colors.success {
133         "Success"
134      } else if color == theme_colors.info {
135         "Info"
136      } else {
137         "Unknown"
138      }
139   }
140}
141
142impl WidgetState {
143   /// Convert the state to a string
144   pub fn to_str(&self) -> &'static str {
145      match self {
146         WidgetState::NonInteractive => "Non-interactive",
147         WidgetState::Inactive => "Inactive",
148         WidgetState::Hovered => "Hovered",
149         WidgetState::Active => "Active",
150         WidgetState::Open => "Open",
151      }
152   }
153
154   /// Convert the state to a vector
155   pub fn to_vec(&self) -> Vec<WidgetState> {
156      let non_interactive = Self::NonInteractive;
157      let inactive = Self::Inactive;
158      let hovered = Self::Hovered;
159      let active = Self::Active;
160      let open = Self::Open;
161
162      vec![non_interactive, inactive, hovered, active, open]
163   }
164}
165
166/// In-app editor for a [`Theme`].
167///
168/// Toggle [`ThemeEditor::open`], then call [`ThemeEditor::show`] every
169/// frame. Always install the (possibly mutated) theme afterwards.
170#[derive(Clone)]
171pub struct ThemeEditor {
172   /// When `false`, [`ThemeEditor::show`] is a no-op.
173   pub open: bool,
174   /// The current widget state being edited
175   pub widget_state: WidgetState,
176   /// Shared HSLA popup used by the color rows.
177   pub hsla_edit_button: HslaEditButton,
178   /// Currently selected named slot in the palette list.
179   pub color: Color,
180   /// Editor window background.
181   pub bg_color: Color32,
182   /// Minimum `(width, height)` of the editor contents.
183   pub size: (f32, f32),
184}
185
186impl ThemeEditor {
187   /// Closed editor with default size and Tokyo-Night-ish preview bg.
188   pub fn new() -> Self {
189      Self {
190         open: false,
191         widget_state: WidgetState::NonInteractive,
192         hsla_edit_button: HslaEditButton::new(),
193         color: Color::Bg(Color32::TRANSPARENT),
194         bg_color: DARK,
195         size: (300.0, 300.0),
196      }
197   }
198
199   /// Show the theme editor in a window
200   ///
201   /// Returns the new theme if we change it
202   pub fn show(&mut self, theme: &mut Theme, ui: &mut Ui) -> Option<Theme> {
203      if !self.open {
204         return None;
205      }
206
207      let mut open = self.open;
208      let mut new_theme = None;
209      let frame = Frame::window(ui.style()).fill(self.bg_color);
210      let title_frame = frame.stroke(Stroke::NONE);
211
212      Window::new("Theme Editor")
213         .open(&mut open)
214         .resizable([true, true])
215         .title_frame(title_frame)
216         .frame(frame)
217         .show(ui.ctx(), |ui| {
218            ui.set_min_width(self.size.0);
219            ui.set_min_height(self.size.1);
220            ui.spacing_mut().button_padding = vec2(10.0, 8.0);
221
222            new_theme = utils::theme_switcher(theme, ui);
223
224            ui.add_space(20.0);
225
226            ScrollArea::vertical().show(ui, |ui| {
227               ui.set_width(self.size.0);
228               ui.set_height(self.size.1);
229               self.ui(theme, ui);
230            });
231         });
232      self.open = open;
233      new_theme
234   }
235
236   /// Show the ui for the theme editor
237   pub fn ui(&mut self, theme: &mut Theme, ui: &mut Ui) {
238      ui.vertical_centered(|ui| {
239         ui.spacing_mut().item_spacing.y = 10.0;
240         let colors = theme.colors.clone();
241
242         CollapsingHeader::new("Theme Frames").show(ui, |ui| {
243            CollapsingHeader::new("Window Frame").show(ui, |ui| {
244               self.frame_settings(
245                  "window_frame",
246                  &mut theme.window_frame,
247                  &colors,
248                  ui,
249               );
250            });
251
252            CollapsingHeader::new("Frame 1").show(ui, |ui| {
253               self.frame_settings("frame1", &mut theme.frame1, &colors, ui);
254            });
255
256            CollapsingHeader::new("Frame 2").show(ui, |ui| {
257               self.frame_settings("frame2", &mut theme.frame2, &colors, ui);
258            });
259         });
260
261         CollapsingHeader::new("Custom Widgets Visuals").show(ui, |ui| {
262            CollapsingHeader::new("Button").show(ui, |ui| {
263               CollapsingHeader::new("Button Visuals 1").show(ui, |ui| {
264                  self.button_visuals(colors, &mut theme.visuals.button_visuals, ui);
265               });
266            });
267
268            CollapsingHeader::new("Label").show(ui, |ui| {
269               CollapsingHeader::new("Label Visuals 1").show(ui, |ui| {
270                  self.button_visuals(colors, &mut theme.visuals.label_visuals, ui);
271               });
272            });
273
274            CollapsingHeader::new("Combo Box").show(ui, |ui| {
275               CollapsingHeader::new("Combo Box Visuals 1").show(ui, |ui| {
276                  self.combo_box_visuals(colors, &mut theme.visuals.combo_box_visuals, ui);
277               });
278            });
279
280            CollapsingHeader::new("Text Edit").show(ui, |ui| {
281               CollapsingHeader::new("Text Edit Visuals 1").show(ui, |ui| {
282                  self.text_edit_visuals(colors, &mut theme.visuals.text_edit_visuals, ui);
283               });
284            });
285         });
286
287         CollapsingHeader::new("Theme Colors").show(ui, |ui| {
288            let old_colors = theme.colors;
289
290            ui.label("BG");
291            self.hsla_edit_button.show("bg", ui, &mut theme.colors.bg);
292
293            ui.label("WidgetBG");
294            self.hsla_edit_button.show("widgetbg", ui, &mut theme.colors.widget_bg);
295
296            ui.label("Hover");
297            self.hsla_edit_button.show("hover", ui, &mut theme.colors.hover);
298
299            ui.label("Text");
300            self.hsla_edit_button.show("text1", ui, &mut theme.colors.text);
301
302            ui.label("Text Muted");
303            self.hsla_edit_button.show("text_muted1", ui, &mut theme.colors.text_muted);
304
305            ui.label("Highlight");
306            self.hsla_edit_button.show("highlight1", ui, &mut theme.colors.highlight);
307
308            ui.label("Border");
309            self.hsla_edit_button.show("border1", ui, &mut theme.colors.border);
310
311            ui.label("Accent");
312            self.hsla_edit_button.show("accent", ui, &mut theme.colors.accent);
313
314            ui.label("Error");
315            self.hsla_edit_button.show("error1", ui, &mut theme.colors.error);
316
317            ui.label("Warning");
318            self.hsla_edit_button.show("warning1", ui, &mut theme.colors.warning);
319
320            ui.label("Success");
321            self.hsla_edit_button.show("success1", ui, &mut theme.colors.success);
322
323            ui.label("Info");
324            self.hsla_edit_button.show("info1", ui, &mut theme.colors.info);
325
326            theme.remap_derived_frames(&old_colors);
327         });
328
329         CollapsingHeader::new("Typography").show(ui, |ui| {
330            ui.label("Very Small");
331            ui.add(Slider::new(&mut theme.typography.very_small, 0.0..=100.0).text("Size"));
332
333            ui.label("Small");
334            ui.add(Slider::new(&mut theme.typography.small, 0.0..=100.0).text("Size"));
335
336            ui.label("Normal");
337            ui.add(Slider::new(&mut theme.typography.normal, 0.0..=100.0).text("Size"));
338
339            ui.label("Large");
340            ui.add(Slider::new(&mut theme.typography.large, 0.0..=100.0).text("Size"));
341
342            ui.label("Very Large");
343            ui.add(Slider::new(&mut theme.typography.very_large, 0.0..=100.0).text("Size"));
344
345            ui.label("Heading");
346            ui.add(Slider::new(&mut theme.typography.heading, 0.0..=100.0).text("Size"));
347         });
348
349         /*
350         CollapsingHeader::new("Other Colors").show(ui, |ui| {
351            ui.label("Selection Stroke");
352            ui.add(
353               Slider::new(
354                  &mut theme.style.visuals.selection.stroke.width,
355                  0.0..=10.0,
356               )
357               .text("Stroke Width"),
358            );
359            ui.label("Selection Stroke Color");
360            self.hsla_edit_button.show(
361               "selection_stroke_color1",
362               ui,
363               &mut theme.style.visuals.selection.stroke.color,
364            );
365
366            ui.label("Selection Bg Fill");
367            self.hsla_edit_button.show(
368               "selection_bg_fill1",
369               ui,
370               &mut theme.style.visuals.selection.bg_fill,
371            );
372
373            ui.label("Hyperlink Color");
374            self.hsla_edit_button.show(
375               "hyperlink_color1",
376               ui,
377               &mut theme.style.visuals.hyperlink_color,
378            );
379
380            ui.label("Faint Background Color");
381            self.hsla_edit_button.show(
382               "faint_bg_color1",
383               ui,
384               &mut theme.style.visuals.faint_bg_color,
385            );
386
387            ui.label("Extreme Background Color");
388            self.hsla_edit_button.show(
389               "extreme_bg_color1",
390               ui,
391               &mut theme.style.visuals.extreme_bg_color,
392            );
393
394            ui.label("Code Background Color");
395            self.hsla_edit_button.show(
396               "code_bg_color1",
397               ui,
398               &mut theme.style.visuals.code_bg_color,
399            );
400
401            ui.label("Warning Text Color");
402            self.hsla_edit_button.show(
403               "warn_fg_color1",
404               ui,
405               &mut theme.style.visuals.warn_fg_color,
406            );
407
408            ui.label("Error Text Color");
409            self.hsla_edit_button.show(
410               "error_fg_color1",
411               ui,
412               &mut theme.style.visuals.error_fg_color,
413            );
414
415            ui.label("Panel Fill Color");
416            self.hsla_edit_button.show(
417               "panel_fill1",
418               ui,
419               &mut theme.style.visuals.panel_fill,
420            );
421         });
422
423         CollapsingHeader::new("Window Visuals").show(ui, |ui| {
424            ui.label("Window Rounding");
425            edit_corner_radius(&mut theme.style.visuals.window_corner_radius, ui);
426
427            ui.label("Window Shadow");
428            edit_shadow(&mut theme.style.visuals.window_shadow, ui);
429
430            ui.label("Window Fill Color");
431            self.hsla_edit_button.show(
432               "window_fill1",
433               ui,
434               &mut theme.style.visuals.window_fill,
435            );
436
437            ui.label("Window Stroke");
438            self.edit_stroke(
439               "window_stroke",
440               &colors,
441               &mut theme.style.visuals.window_stroke,
442               ui,
443            );
444
445            ui.label("Window Highlight Topmost");
446            ui.checkbox(
447               &mut theme.style.visuals.window_highlight_topmost,
448               "Highlight Topmost",
449            );
450         });
451
452         CollapsingHeader::new("Popup Shadow").show(ui, |ui| {
453            edit_shadow(&mut theme.style.visuals.popup_shadow, ui);
454         });
455
456         CollapsingHeader::new("Menu Rounding").show(ui, |ui| {
457            edit_corner_radius(&mut theme.style.visuals.menu_corner_radius, ui);
458         });
459
460         CollapsingHeader::new("Widget Visuals").show(ui, |ui| {
461            self.widget_settings(theme, ui);
462         });
463
464         CollapsingHeader::new("Other Settings").show(ui, |ui| {
465            ui.label("Resize Corner Size");
466            ui.add(
467               Slider::new(
468                  &mut theme.style.visuals.resize_corner_size,
469                  0.0..=100.0,
470               )
471               .text("Corner Size"),
472            );
473
474            ui.label("Button Frame");
475            ui.checkbox(
476               &mut theme.style.visuals.button_frame,
477               "Button Frame",
478            );
479         });
480          */
481
482         CollapsingHeader::new("Tessellation").show(ui, |ui| {
483            self.tesellation_settings(theme, ui);
484         });
485      });
486   }
487
488   fn tesellation_settings(&mut self, theme: &Theme, ui: &mut Ui) {
489      let text_size = theme.typography.normal;
490
491      let mut options = ui.ctx().tessellation_options(|options| options.clone());
492
493      let text = RichText::new("Feathering").size(text_size);
494
495      ui.checkbox(&mut options.feathering, text);
496
497      ui.add(
498         DragValue::new(&mut options.feathering_size_in_pixels)
499            .speed(0.1)
500            .range(0.0..=100.0),
501      );
502
503      let text = RichText::new("Coarse tessellation culling").size(text_size);
504      ui.checkbox(&mut options.coarse_tessellation_culling, text);
505
506      let text = RichText::new("Precomputed discs").size(text_size);
507      ui.checkbox(&mut options.prerasterized_discs, text);
508
509      let text = RichText::new("Round text to pixels").size(text_size);
510      ui.checkbox(&mut options.round_text_to_pixels, text);
511
512      let text = RichText::new("Round line segments to pixels").size(text_size);
513      ui.checkbox(&mut options.round_line_segments_to_pixels, text);
514
515      let text = RichText::new("Round rects to pixels").size(text_size);
516      ui.checkbox(&mut options.round_rects_to_pixels, text);
517
518      let text = RichText::new("Debug paint text rects").size(text_size);
519      ui.checkbox(&mut options.debug_paint_text_rects, text);
520
521      let text = RichText::new("Debug paint clip rects").size(text_size);
522      ui.checkbox(&mut options.debug_paint_clip_rects, text);
523
524      let text = RichText::new("Debug ignore clip rects").size(text_size);
525      ui.checkbox(&mut options.debug_ignore_clip_rects, text);
526
527      let text = RichText::new("Bezier tolerance").size(text_size);
528      ui.label(text);
529      ui.add(DragValue::new(&mut options.bezier_tolerance).speed(0.1).range(0.0..=1.0));
530
531      let text = RichText::new("Epsilon").size(text_size);
532      ui.label(text);
533      ui.add(DragValue::new(&mut options.epsilon).speed(0.1).range(0.0..=1.0));
534
535      let text = RichText::new("Parallel tessellation").size(text_size);
536      ui.checkbox(&mut options.parallel_tessellation, text);
537
538      let text = RichText::new("Validate meshes").size(text_size);
539      ui.checkbox(&mut options.validate_meshes, text);
540
541      ui.ctx().tessellation_options_mut(|options_mut| {
542         *options_mut = options;
543      });
544   }
545
546   fn button_visuals(&mut self, colors: ThemeColors, visuals: &mut ButtonVisuals, ui: &mut Ui) {
547      let text = RichText::new("Button Visuals");
548      ui.label(text);
549
550      ui.label("Text Color");
551      ui.horizontal(|ui| {
552         let color = self.color_select("1", visuals.text, &colors, ui);
553         if let Some(color) = color {
554            visuals.text = color.color32();
555         }
556
557         self.hsla_edit_button.show("text1", ui, &mut visuals.text);
558      });
559
560      ui.label("Background Color");
561      ui.horizontal(|ui| {
562         let color = self.color_select("2", visuals.bg, &colors, ui);
563         if let Some(color) = color {
564            visuals.bg = color.color32();
565         }
566
567         self.hsla_edit_button.show("bg1", ui, &mut visuals.bg);
568      });
569
570      ui.label("Background Hover Color");
571      ui.horizontal(|ui| {
572         let color = self.color_select("3", visuals.bg_hover, &colors, ui);
573         if let Some(color) = color {
574            visuals.bg_hover = color.color32();
575         }
576
577         self.hsla_edit_button.show("bg_hover1", ui, &mut visuals.bg_hover);
578      });
579
580      ui.label("Background Click Color");
581      ui.horizontal(|ui| {
582         let color = self.color_select("4", visuals.bg_click, &colors, ui);
583         if let Some(color) = color {
584            visuals.bg_click = color.color32();
585         }
586
587         self.hsla_edit_button.show("bg_click1", ui, &mut visuals.bg_click);
588      });
589
590      ui.label("Background Selected");
591      ui.horizontal(|ui| {
592         let color = self.color_select("5", visuals.bg_selected, &colors, ui);
593
594         if let Some(color) = color {
595            visuals.bg_selected = color.color32();
596         }
597
598         self.hsla_edit_button.show("bg_selected1", ui, &mut visuals.bg_selected);
599      });
600
601      ui.label("Border Color");
602      ui.horizontal(|ui| {
603         let color = self.color_select("6", visuals.border.color, &colors, ui);
604
605         if let Some(color) = color {
606            visuals.border.color = color.color32();
607         }
608
609         self.hsla_edit_button.show("border1", ui, &mut visuals.border.color);
610      });
611
612      ui.label("Border Hover Color");
613      ui.horizontal(|ui| {
614         let color = self.color_select("7", visuals.border_hover.color, &colors, ui);
615         if let Some(color) = color {
616            visuals.border_hover.color = color.color32();
617         }
618
619         self.hsla_edit_button.show(
620            "border_hover1",
621            ui,
622            &mut visuals.border_hover.color,
623         );
624      });
625
626      ui.label("Border Click Color");
627      ui.horizontal(|ui| {
628         let color = self.color_select("8", visuals.border_click.color, &colors, ui);
629         if let Some(color) = color {
630            visuals.border_click.color = color.color32();
631         }
632
633         self.hsla_edit_button.show(
634            "border_click1",
635            ui,
636            &mut visuals.border_click.color,
637         );
638      });
639
640      ui.label("Corner Radius");
641      ui.add(Slider::new(&mut visuals.corner_radius.ne, 0..=100).text("NE"));
642      ui.add(Slider::new(&mut visuals.corner_radius.nw, 0..=100).text("NW"));
643      ui.add(Slider::new(&mut visuals.corner_radius.se, 0..=100).text("SE"));
644      ui.add(Slider::new(&mut visuals.corner_radius.sw, 0..=100).text("SW"));
645
646      ui.label("Shadow");
647      ui.horizontal(|ui| {
648         let color = self.color_select("9", visuals.shadow.color, &colors, ui);
649         if let Some(color) = color {
650            visuals.shadow.color = color.color32();
651         }
652
653         /*
654         self.hsla_edit_button.show(
655            "shadow_color1",
656            ui,
657            &mut colors.button_visuals.shadow.color,
658         );
659         */
660
661         color_edit_button_srgba(
662            ui,
663            &mut visuals.shadow.color,
664            Alpha::BlendOrAdditive,
665         );
666      });
667
668      ui.label("Shadow Offset");
669      ui.add(Slider::new(&mut visuals.shadow.offset[0], -100..=100).text("Offset X"));
670      ui.add(Slider::new(&mut visuals.shadow.offset[1], -100..=100).text("Offset Y"));
671
672      ui.label("Shadow Blur");
673      ui.add(Slider::new(&mut visuals.shadow.blur, 0..=100).text("Blur"));
674
675      ui.label("Shadow Spread");
676      ui.add(Slider::new(&mut visuals.shadow.spread, 0..=100).text("Spread"));
677   }
678
679   fn combo_box_visuals(
680      &mut self,
681      colors: ThemeColors,
682      visuals: &mut ComboBoxVisuals,
683      ui: &mut Ui,
684   ) {
685      ui.label("Background Color");
686      ui.horizontal(|ui| {
687         let color = self.color_select("2", visuals.bg, &colors, ui);
688         if let Some(color) = color {
689            visuals.bg = color.color32();
690         }
691
692         self.hsla_edit_button.show("bg1", ui, &mut visuals.bg);
693      });
694
695      ui.label("Background Hover Color");
696      ui.horizontal(|ui| {
697         let color = self.color_select("3", visuals.bg_hover, &colors, ui);
698         if let Some(color) = color {
699            visuals.bg_hover = color.color32();
700         }
701
702         self.hsla_edit_button.show("bg_hover1", ui, &mut visuals.bg_hover);
703      });
704
705      ui.label("Border Color");
706      ui.horizontal(|ui| {
707         let color = self.color_select("4", visuals.border.color, &colors, ui);
708
709         if let Some(color) = color {
710            visuals.border.color = color.color32();
711         }
712
713         self.hsla_edit_button.show("border1", ui, &mut visuals.border.color);
714      });
715
716      ui.label("Border Hover Color");
717      ui.horizontal(|ui| {
718         let color = self.color_select("5", visuals.border_hover.color, &colors, ui);
719         if let Some(color) = color {
720            visuals.border_hover.color = color.color32();
721         }
722
723         self.hsla_edit_button.show(
724            "border_hover1",
725            ui,
726            &mut visuals.border_hover.color,
727         );
728      });
729
730      ui.label("Border Open Color");
731      ui.horizontal(|ui| {
732         let color = self.color_select("6", visuals.border_open.color, &colors, ui);
733         if let Some(color) = color {
734            visuals.border_open.color = color.color32();
735         }
736
737         self.hsla_edit_button.show("border_open1", ui, &mut visuals.border_open.color);
738      });
739
740      ui.label("Corner Radius");
741      ui.add(Slider::new(&mut visuals.corner_radius.ne, 0..=100).text("NE"));
742      ui.add(Slider::new(&mut visuals.corner_radius.nw, 0..=100).text("NW"));
743      ui.add(Slider::new(&mut visuals.corner_radius.se, 0..=100).text("SE"));
744      ui.add(Slider::new(&mut visuals.corner_radius.sw, 0..=100).text("SW"));
745
746      ui.label("Shadow");
747      ui.horizontal(|ui| {
748         let color = self.color_select("9", visuals.shadow.color, &colors, ui);
749         if let Some(color) = color {
750            visuals.shadow.color = color.color32();
751         }
752
753         color_edit_button_srgba(
754            ui,
755            &mut visuals.shadow.color,
756            Alpha::BlendOrAdditive,
757         );
758      });
759
760      ui.label("Shadow Offset");
761      ui.add(Slider::new(&mut visuals.shadow.offset[0], -100..=100).text("Offset X"));
762      ui.add(Slider::new(&mut visuals.shadow.offset[1], -100..=100).text("Offset Y"));
763
764      ui.label("Shadow Blur");
765      ui.add(Slider::new(&mut visuals.shadow.blur, 0..=100).text("Blur"));
766
767      ui.label("Shadow Spread");
768      ui.add(Slider::new(&mut visuals.shadow.spread, 0..=100).text("Spread"));
769   }
770
771   fn text_edit_visuals(
772      &mut self,
773      colors: ThemeColors,
774      visuals: &mut TextEditVisuals,
775      ui: &mut Ui,
776   ) {
777      ui.label("Text Color");
778      ui.horizontal(|ui| {
779         let color = self.color_select("1", visuals.text, &colors, ui);
780         if let Some(color) = color {
781            visuals.text = color.color32();
782         }
783
784         self.hsla_edit_button.show("text1", ui, &mut visuals.text);
785      });
786
787      ui.label("Background Color");
788      ui.horizontal(|ui| {
789         let color = self.color_select("2", visuals.bg, &colors, ui);
790         if let Some(color) = color {
791            visuals.bg = color.color32();
792         }
793
794         self.hsla_edit_button.show("bg1", ui, &mut visuals.bg);
795      });
796
797      ui.label("Border Color");
798      ui.horizontal(|ui| {
799         let color = self.color_select("3", visuals.border.color, &colors, ui);
800
801         if let Some(color) = color {
802            visuals.border.color = color.color32();
803         }
804
805         self.hsla_edit_button.show("border1", ui, &mut visuals.border.color);
806      });
807
808      ui.label("Border Hover Color");
809      ui.horizontal(|ui| {
810         let color = self.color_select("4", visuals.border_hover.color, &colors, ui);
811         if let Some(color) = color {
812            visuals.border_hover.color = color.color32();
813         }
814
815         self.hsla_edit_button.show(
816            "border_hover1",
817            ui,
818            &mut visuals.border_hover.color,
819         );
820      });
821
822      ui.label("Border Open Color");
823      ui.horizontal(|ui| {
824         let color = self.color_select("5", visuals.border_open.color, &colors, ui);
825         if let Some(color) = color {
826            visuals.border_open.color = color.color32();
827         }
828
829         self.hsla_edit_button.show("border_open1", ui, &mut visuals.border_open.color);
830      });
831
832      ui.label("Corner Radius");
833      ui.add(Slider::new(&mut visuals.corner_radius.ne, 0..=100).text("NE"));
834      ui.add(Slider::new(&mut visuals.corner_radius.nw, 0..=100).text("NW"));
835      ui.add(Slider::new(&mut visuals.corner_radius.se, 0..=100).text("SE"));
836      ui.add(Slider::new(&mut visuals.corner_radius.sw, 0..=100).text("SW"));
837
838      ui.label("Shadow");
839      ui.horizontal(|ui| {
840         let color = self.color_select("6", visuals.shadow.color, &colors, ui);
841         if let Some(color) = color {
842            visuals.shadow.color = color.color32();
843         }
844
845         color_edit_button_srgba(
846            ui,
847            &mut visuals.shadow.color,
848            Alpha::BlendOrAdditive,
849         );
850      });
851
852      ui.label("Shadow Offset");
853      ui.add(Slider::new(&mut visuals.shadow.offset[0], -100..=100).text("Offset X"));
854      ui.add(Slider::new(&mut visuals.shadow.offset[1], -100..=100).text("Offset Y"));
855
856      ui.label("Shadow Blur");
857      ui.add(Slider::new(&mut visuals.shadow.blur, 0..=100).text("Blur"));
858
859      ui.label("Shadow Spread");
860      ui.add(Slider::new(&mut visuals.shadow.spread, 0..=100).text("Spread"));
861   }
862
863   /*
864   fn widget_settings(&mut self, theme: &mut Theme, ui: &mut Ui) {
865       self.select_widget_state(ui);
866
867       let widget_visuals = match self.widget_state {
868           WidgetState::NonInteractive => &mut theme.style.visuals.widgets.noninteractive,
869           WidgetState::Inactive => &mut theme.style.visuals.widgets.inactive,
870           WidgetState::Hovered => &mut theme.style.visuals.widgets.hovered,
871           WidgetState::Active => &mut theme.style.visuals.widgets.active,
872           WidgetState::Open => &mut theme.style.visuals.widgets.open,
873       };
874
875       ui.label("Background Fill Color");
876
877       ui.horizontal(|ui| {
878           let color = self.color_select("1", widget_visuals.bg_fill, &theme.colors, ui);
879           if let Some(color) = color {
880               widget_visuals.bg_fill = color.color32();
881           }
882
883           self.hsla_edit_button
884               .show("bg_fill1", ui, &mut widget_visuals.bg_fill);
885       });
886
887       ui.label("Weak Background Fill Color");
888
889       ui.horizontal(|ui| {
890           let color = self.color_select("2", widget_visuals.weak_bg_fill, &theme.colors, ui);
891           if let Some(color) = color {
892               widget_visuals.weak_bg_fill = color.color32();
893           }
894
895           self.hsla_edit_button
896               .show("weak_bg_fill1", ui, &mut widget_visuals.weak_bg_fill);
897       });
898
899       ui.label("Background Stroke Width");
900       ui.add(Slider::new(&mut widget_visuals.bg_stroke.width, 0.0..=10.0));
901
902       ui.label("Background Stroke Color");
903       ui.horizontal(|ui| {
904           let color = self.color_select("3", widget_visuals.bg_stroke.color, &theme.colors, ui);
905           if let Some(color) = color {
906               widget_visuals.bg_stroke.color = color.color32();
907           }
908
909           self.hsla_edit_button
910               .show("bg_stroke_color1", ui, &mut widget_visuals.bg_stroke.color);
911       });
912
913       ui.label("Rounding");
914       edit_corner_radius(&mut widget_visuals.corner_radius, ui);
915
916       ui.label("Foreground Stroke Width");
917       ui.add(Slider::new(&mut widget_visuals.fg_stroke.width, 0.0..=10.0));
918
919       ui.label("Foreground Stroke Color");
920       ui.horizontal(|ui| {
921           let color = self.color_select("4", widget_visuals.fg_stroke.color, &theme.colors, ui);
922
923           if let Some(color) = color {
924               widget_visuals.fg_stroke.color = color.color32();
925           }
926
927           self.hsla_edit_button
928               .show("fg_stroke_color1", ui, &mut widget_visuals.fg_stroke.color);
929       });
930
931       ui.label("Expansion");
932       ui.add(Slider::new(&mut widget_visuals.expansion, 0.0..=100.0).text("Expansion"));
933   }
934   */
935
936   fn frame_settings(&mut self, id: &str, frame: &mut Frame, colors: &ThemeColors, ui: &mut Ui) {
937      CollapsingHeader::new("Inner & Outter Margin").show(ui, |ui| {
938         ui.label("Inner Margin");
939         edit_margin(&mut frame.inner_margin, ui);
940
941         ui.label("Outter Margin");
942         edit_margin(&mut frame.outer_margin, ui);
943      });
944
945      ui.label("Rounding");
946      edit_corner_radius(&mut frame.corner_radius, ui);
947
948      ui.label("Shadow");
949      edit_shadow(&mut frame.shadow, ui);
950
951      ui.label("Fill Color");
952      self.hsla_edit_button.show(&format!("{id}_fill"), ui, &mut frame.fill);
953
954      ui.label("Stroke Width & Color");
955      self.edit_stroke(id, colors, &mut frame.stroke, ui);
956   }
957
958   fn _select_widget_state(&mut self, ui: &mut Ui) {
959      ComboBox::from_label("")
960         .selected_text(self.widget_state.to_str())
961         .show_ui(ui, |ui| {
962            for widget in self.widget_state.to_vec() {
963               let value = ui.selectable_value(
964                  &mut self.widget_state,
965                  widget.clone(),
966                  widget.to_str(),
967               );
968
969               if value.clicked() {
970                  self.widget_state = widget;
971               }
972            }
973         });
974   }
975
976   fn color_select(
977      &mut self,
978      id: &str,
979      current_color: Color32,
980      colors: &ThemeColors,
981      ui: &mut Ui,
982   ) -> Option<Color> {
983      let all_colors = Color::all_colors_from(colors);
984
985      let mut selected_color = None;
986      let current_color_name = Color::name_from(current_color, colors);
987
988      ComboBox::from_id_salt(id).selected_text(current_color_name).show_ui(ui, |ui| {
989         for color in all_colors {
990            let value = ui.selectable_value(&mut self.color, color.clone(), color.to_str());
991
992            if value.clicked() {
993               selected_color = Some(color);
994            }
995         }
996      });
997      selected_color
998   }
999
1000   fn edit_stroke(&mut self, id: &str, colors: &ThemeColors, stroke: &mut Stroke, ui: &mut Ui) {
1001      ui.add(Slider::new(&mut stroke.width, 0.0..=100.0).text("Stroke Width"));
1002
1003      ui.label("Stroke Color");
1004
1005      ui.horizontal(|ui| {
1006         let color = self.color_select(
1007            &format!("{id}_stroke_pal"),
1008            stroke.color,
1009            colors,
1010            ui,
1011         );
1012         if let Some(color) = color {
1013            stroke.color = color.color32();
1014         }
1015
1016         color_edit_button_srgba(ui, &mut stroke.color, Alpha::BlendOrAdditive);
1017
1018         self.hsla_edit_button.show(&format!("{id}_stroke"), ui, &mut stroke.color);
1019      });
1020   }
1021}
1022
1023fn edit_margin(margin: &mut Margin, ui: &mut Ui) {
1024   ui.add(Slider::new(&mut margin.top, 0..=127).text("Top"));
1025   ui.add(Slider::new(&mut margin.bottom, 0..=127).text("Bottom"));
1026   ui.add(Slider::new(&mut margin.left, 0..=127).text("Left"));
1027   ui.add(Slider::new(&mut margin.right, 0..=127).text("Right"));
1028}
1029
1030fn edit_corner_radius(corner_radius: &mut CornerRadius, ui: &mut Ui) {
1031   ui.add(Slider::new(&mut corner_radius.nw, 0..=255).text("Top Left"));
1032   ui.add(Slider::new(&mut corner_radius.ne, 0..=255).text("Top Right"));
1033   ui.add(Slider::new(&mut corner_radius.sw, 0..=255).text("Bottom Left"));
1034   ui.add(Slider::new(&mut corner_radius.se, 0..=255).text("Bottom Right"));
1035}
1036
1037fn edit_shadow(shadow: &mut Shadow, ui: &mut Ui) {
1038   ui.add(Slider::new(&mut shadow.offset[0], -128..=127).text("Offset X"));
1039   ui.add(Slider::new(&mut shadow.offset[1], -128..=127).text("Offset Y"));
1040   ui.add(Slider::new(&mut shadow.blur, 0..=255).text("Blur"));
1041   ui.add(Slider::new(&mut shadow.spread, 0..=255).text("Spread"));
1042
1043   ui.label("Shadow Color");
1044   color_edit_button_srgba(ui, &mut shadow.color, Alpha::BlendOrAdditive);
1045}
1046
1047/// Swatch button that opens an HSLA / hex editor popup.
1048#[derive(Clone)]
1049pub struct HslaEditButton {
1050   from_hex_text: String,
1051}
1052
1053impl HslaEditButton {
1054   /// Empty hex field.
1055   pub fn new() -> Self {
1056      Self {
1057         from_hex_text: String::new(),
1058      }
1059   }
1060
1061   /// Paint the swatch and popup. Mutates `color32` when the user edits it.
1062   pub fn show(&mut self, id: &str, ui: &mut Ui, color32: &mut Color32) -> Response {
1063      let stroke = Stroke::new(1.0, Color32::GRAY);
1064      let button_size = Vec2::new(50.0, 20.0);
1065      let (rect, mut response) = ui.allocate_exact_size(button_size, Sense::click());
1066      ui.painter().rect_filled(rect, 4.0, *color32);
1067      ui.painter().rect_stroke(rect, 4.0, stroke, StrokeKind::Inside);
1068
1069      let popup_id = ui.make_persistent_id(id);
1070
1071      let set_command = if response.clicked() {
1072         Some(SetOpenCommand::Toggle)
1073      } else {
1074         None
1075      };
1076
1077      let close_behavior = PopupCloseBehavior::CloseOnClickOutside;
1078      response.layer_id.order = Order::Debug;
1079
1080      let popup = Popup::from_response(&response)
1081         .close_behavior(close_behavior)
1082         .open_memory(set_command);
1083
1084      let working_id = popup_id.with("working_hsla");
1085      let mut working_hsla = ui
1086         .memory(|mem| mem.data.get_temp(working_id))
1087         .unwrap_or_else(|| Hsla::from_color32(*color32));
1088
1089      let popup_res = popup.show(|ui| self.hsla_picker_ui(ui, &mut working_hsla));
1090
1091      if let Some(inner) = popup_res {
1092         // if color changed
1093         if inner.inner {
1094            ui.memory_mut(|mem| mem.data.insert_temp(working_id, working_hsla));
1095            *color32 = working_hsla.to_color32();
1096            response.mark_changed();
1097         }
1098      } else {
1099         ui.memory_mut(|mem| mem.data.remove::<Hsla>(working_id));
1100      }
1101
1102      response
1103   }
1104
1105   // The core HSLA picker UI (sliders, 2D square, preview). Returns true if changed.
1106   fn hsla_picker_ui(&mut self, ui: &mut Ui, hsla: &mut Hsla) -> bool {
1107      let mut changed = false;
1108      let stroke = Stroke::new(1.0, Color32::GRAY);
1109
1110      ui.horizontal(|ui| {
1111         ui.set_width(200.0);
1112
1113         // Left: 2D S-L square + hue slider below it
1114         ui.vertical(|ui| {
1115            changed |= sl_2d_picker(ui, hsla);
1116            changed |= hue_slider(ui, hsla);
1117            changed |= alpha_slider(ui, hsla);
1118         });
1119
1120         // Right: Preview + numeric controls
1121         ui.vertical(|ui| {
1122            // Preview rect
1123            let preview_size = Vec2::new(80.0, 80.0);
1124            let (rect, _) = ui.allocate_exact_size(preview_size, Sense::hover());
1125            ui.painter().rect_filled(rect, 4.0, hsla.to_color32());
1126            ui.painter().rect_stroke(rect, 4.0, stroke, StrokeKind::Inside);
1127
1128            ui.label(RichText::new("Preview").strong());
1129
1130            // Numeric sliders for precision
1131            ui.add_space(10.0);
1132            changed |= ui.add(Slider::new(&mut hsla.h, 0.0..=360.0).text("Hue")).changed();
1133            changed |= ui.add(Slider::new(&mut hsla.s, 0.0..=100.0).text("Saturation")).changed();
1134            changed |= ui.add(Slider::new(&mut hsla.l, 0.0..=100.0).text("Lightness")).changed();
1135            changed |= ui.add(Slider::new(&mut hsla.a, 0.0..=1.0).text("Alpha")).changed();
1136         });
1137
1138         ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1139            ui.vertical(|ui| {
1140               // RGBA copy button
1141               ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1142                  let (r, g, b, a) = hsla.to_rgba_components();
1143                  let text = RichText::new(format!("RGBA ({r}, {g}, {b}, {a})"));
1144                  let button = Button::new(text).min_size(vec2(160.0, 15.0));
1145                  if ui.add(button).clicked() {
1146                     ui.ctx().copy_text(format!("({r}, {g}, {b}, {a})"));
1147                  }
1148               });
1149
1150               // HEX copy button
1151               ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1152                  let hex_color = HexColor::Hex6(hsla.to_color32());
1153                  let text = RichText::new(format!("HEX {}", hex_color));
1154                  let button = Button::new(text).min_size(vec2(160.0, 15.0));
1155                  if ui.add(button).clicked() {
1156                     ui.ctx().copy_text(format!("{}", hex_color));
1157                  }
1158               });
1159
1160               // From RBG to HSLA
1161               ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1162                  let text = RichText::new("Convert From HEX");
1163                  let button = Button::new(text).small();
1164                  ui.add(TextEdit::singleline(&mut self.from_hex_text));
1165                  if ui.add(button).clicked() {
1166                     let new_color = Hsla::from_hex(&self.from_hex_text);
1167                     if let Some(new_color) = new_color {
1168                        *hsla = new_color;
1169                        changed = true;
1170                     }
1171                  }
1172               });
1173            });
1174         });
1175      });
1176
1177      changed
1178   }
1179}
1180
1181// 2D picker for Saturation (x) and Lightness (y)
1182fn sl_2d_picker(ui: &mut Ui, hsla: &mut Hsla) -> bool {
1183   let size = Vec2::new(150.0, 150.0);
1184   let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
1185
1186   let mut changed = false;
1187
1188   if response.dragged() {
1189      if let Some(pos) = response.hover_pos() {
1190         let relative = pos - rect.min;
1191         hsla.s = (relative.x / size.x).clamp(0.0, 1.0) * 100.0;
1192         hsla.l = (1.0 - (relative.y / size.y)).clamp(0.0, 1.0) * 100.0; // Top: high L, bottom: low L
1193         changed = true;
1194      }
1195   }
1196
1197   // Paint gradient background (grid of small rects for simplicity)
1198   let painter = ui.painter();
1199   const RES: usize = 64; // Higher for smoother, but 64 is fast and looks good
1200   let cell_size = size / RES as f32;
1201   for i in 0..RES {
1202      for j in 0..RES {
1203         let s = (i as f32 / (RES - 1) as f32) * 100.0;
1204         let l = (1.0 - (j as f32 / (RES - 1) as f32)) * 100.0; // Top: l=100, bottom: l=0
1205         let temp_hsla = Hsla {
1206            h: hsla.h,
1207            s,
1208            l,
1209            a: 1.0,
1210         };
1211         let color = temp_hsla.to_color32();
1212
1213         let min = rect.min + Vec2::new(i as f32 * cell_size.x, j as f32 * cell_size.y);
1214         let cell_rect = Rect::from_min_size(min, cell_size);
1215         painter.rect_filled(cell_rect, 0.0, color);
1216      }
1217   }
1218
1219   // Draw cursor at current position
1220   let x = (hsla.s / 100.0) * size.x;
1221   let y = (1.0 - hsla.l / 100.0) * size.y;
1222   let cursor_pos = rect.min + Vec2::new(x, y);
1223   painter.circle_stroke(cursor_pos, 5.0, Stroke::new(1.0, Color32::WHITE));
1224   painter.circle_stroke(cursor_pos, 5.0, Stroke::new(1.0, Color32::BLACK));
1225
1226   // Outline the square
1227   painter.rect_stroke(
1228      rect,
1229      0.0,
1230      Stroke::new(1.0, Color32::GRAY),
1231      StrokeKind::Inside,
1232   );
1233
1234   changed
1235}
1236
1237// Hue gradient slider
1238fn hue_slider(ui: &mut Ui, hsla: &mut Hsla) -> bool {
1239   let size = Vec2::new(150.0, 20.0);
1240   let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
1241
1242   let mut changed = false;
1243
1244   if response.dragged() {
1245      if let Some(pos) = response.hover_pos() {
1246         let relative_x = (pos.x - rect.min.x) / size.x;
1247         hsla.h = relative_x.clamp(0.0, 1.0) * 360.0;
1248         changed = true;
1249      }
1250   }
1251
1252   // Paint rainbow gradient
1253   let painter = ui.painter();
1254   const RES: usize = 128; // Smooth horizontal gradient
1255   let cell_width = size.x / RES as f32;
1256   for i in 0..RES {
1257      let h = (i as f32 / (RES - 1) as f32) * 360.0;
1258      let temp_hsla = Hsla {
1259         h,
1260         s: 100.0,
1261         l: 50.0,
1262         a: 1.0,
1263      }; // Full sat, mid light for vibrant rainbow
1264      let color = temp_hsla.to_color32();
1265
1266      let min = rect.min + Vec2::new(i as f32 * cell_width, 0.0);
1267      let cell_rect = Rect::from_min_size(min, Vec2::new(cell_width, size.y));
1268      painter.rect_filled(cell_rect, 0.0, color);
1269   }
1270
1271   // Cursor indicator (vertical line)
1272   let x = (hsla.h / 360.0) * size.x;
1273   let line_start = rect.min + Vec2::new(x, 0.0);
1274   let line_end = rect.min + Vec2::new(x, size.y);
1275   painter.line_segment(
1276      [line_start, line_end],
1277      Stroke::new(2.0, Color32::WHITE),
1278   );
1279
1280   // Outline
1281   painter.rect_stroke(
1282      rect,
1283      4.0,
1284      Stroke::new(1.0, Color32::GRAY),
1285      StrokeKind::Inside,
1286   );
1287
1288   changed
1289}
1290
1291fn alpha_slider(ui: &mut Ui, hsla: &mut Hsla) -> bool {
1292   let size = Vec2::new(150.0, 20.0);
1293   let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
1294   let mut changed = false;
1295
1296   if response.dragged() {
1297      if let Some(pos) = response.hover_pos() {
1298         let relative_x = (pos.x - rect.min.x) / size.x;
1299         hsla.a = relative_x.clamp(0.0, 1.0);
1300         changed = true;
1301      }
1302   }
1303
1304   let painter = ui.painter();
1305   // Paint checkerboard FIRST for transparency visibility
1306   let checker_size = 5.0;
1307
1308   for x in (0..=((size.x / checker_size) as usize)).step_by(1) {
1309      for y in (0..=((size.y / checker_size) as usize)).step_by(1) {
1310         let color = if (x + y) % 2 == 0 {
1311            Color32::GRAY
1312         } else {
1313            Color32::LIGHT_GRAY
1314         };
1315         let min = rect.min + Vec2::new(x as f32 * checker_size, y as f32 * checker_size);
1316         let cell_rect = Rect::from_min_size(min, Vec2::splat(checker_size)).intersect(rect);
1317         painter.rect_filled(cell_rect, 0.0, color);
1318      }
1319   }
1320
1321   // Then paint gradient on top
1322   const RES: usize = 64;
1323   let cell_width = size.x / RES as f32;
1324
1325   for i in 0..RES {
1326      let a = i as f32 / (RES - 1) as f32;
1327      let temp_hsla = Hsla {
1328         h: hsla.h,
1329         s: hsla.s,
1330         l: hsla.l,
1331         a,
1332      };
1333
1334      let color = temp_hsla.to_color32();
1335      let min = rect.min + Vec2::new(i as f32 * cell_width, 0.0);
1336      let cell_rect = Rect::from_min_size(min, Vec2::new(cell_width, size.y));
1337      painter.rect_filled(cell_rect, 0.0, color);
1338   }
1339
1340   // Cursor line
1341   let x = hsla.a * size.x;
1342   let line_start = rect.min + Vec2::new(x, 0.0);
1343   let line_end = rect.min + Vec2::new(x, size.y);
1344
1345   painter.line_segment(
1346      [line_start, line_end],
1347      Stroke::new(2.0, Color32::WHITE),
1348   );
1349
1350   // Outline
1351   painter.rect_stroke(
1352      rect,
1353      4.0,
1354      Stroke::new(1.0, Color32::GRAY),
1355      StrokeKind::Inside,
1356   );
1357   changed
1358}