egui_colors/
lib.rs

1//! # egui Colors
2//!
3//! Experimental toolkit to explore color styling in [`egui`](https://github.com/emilk/egui)
4//!
5//! Based on the [`Radix`](https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale) system which maps a color scale to 12 functional
6//! UI elements.
7//! Scales (both light and dark mode) are computed with luminosity contrast algorithm defined by [`APCA`](https://github.com/Myndex).
8//! Every scale uses one predefined `[u8; 3]` rgb color that is used as an accent color (if suitable).
9//!
10//!
11
12pub(crate) mod animator;
13pub(crate) mod apca;
14pub(crate) mod color_space;
15pub(crate) mod scales;
16pub mod tokens;
17/// Some predefined themes
18pub mod utils;
19
20use animator::ColorAnimator;
21use egui::{Context, Ui};
22use scales::Scales;
23use std::sync::Arc;
24use tokens::{ColorTokens, ThemeColor};
25use utils::{LABELS, THEME_NAMES, THEMES};
26
27/// A set of colors that are used together to set a visual feel for the ui
28pub type Theme = [ThemeColor; 12];
29
30#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
31pub(crate) enum ApplyTo {
32    Global,
33    Local,
34    #[default]
35    ExtraScale,
36}
37
38/// The Colorix type is the main entry to this crate.
39///
40/// # Examples
41///
42/// ```
43/// use egui::Context;
44/// use egui_colors::{Colorix, tokens::ThemeColor};
45/// //Define a colorix field in your egui App
46/// #[derive(Default)]
47/// struct App {
48///     colorix: Colorix,
49///     //..Default::default()
50/// }
51/// // initialize the Colorix with a theme
52/// // a color theme is defined as [ThemeColor; 12]
53/// // a ThemeColor is an enum with several preset colors and one Custom.
54///  // You can choose between different scopes: global, local or extra_set.
55/// impl App {
56///     fn new(ctx: &Context) -> Self {
57///         let yellow_theme = [ThemeColor::Custom([232, 210, 7]); 12];
58///         let colorix = Colorix::global(ctx, yellow_theme);
59///         Self {
60///             colorix,
61///             ..Default::default()
62///         }
63///     }
64/// }
65/// ```
66#[derive(Debug, Default, Clone)]
67pub struct Colorix {
68    pub tokens: ColorTokens,
69    pub(crate) theme: Theme,
70    theme_index: usize,
71    pub(crate) scales: Scales,
72    animated: bool,
73    pub animator: ColorAnimator,
74    pub(crate) apply_to: ApplyTo,
75}
76
77impl Colorix {
78    #[must_use]
79    pub fn global(ctx: &Context, theme: Theme) -> Self {
80        let mut colorix = Self {
81            theme,
82            ..Default::default()
83        };
84        let mode = ctx.style().visuals.dark_mode;
85        colorix.apply_to = ApplyTo::Global;
86        colorix.tokens.apply_to = ApplyTo::Global;
87        colorix.set_colorix_mode(mode);
88        colorix.get_theme_index();
89        colorix.update_colors(Some(ctx), None);
90        colorix
91    }
92    /// Initialize a Colorix instance that applies to local ui.
93    /// It needs an `update_locally(ui)` in the egui `update` function to work.
94    pub fn local(ui: &mut Ui, theme: Theme) -> Self {
95        let mut colorix = Self {
96            theme,
97            ..Default::default()
98        };
99        let mode = ui.ctx().style().visuals.dark_mode;
100        colorix.set_colorix_mode(mode);
101        colorix.get_theme_index();
102        colorix.apply_to = ApplyTo::Local;
103        colorix.tokens.apply_to = ApplyTo::Local;
104        colorix.update_colors(None, Some(ui));
105        colorix
106    }
107    /// Initialize a colorix to provide extra scale. It doesn't apply to any ui.
108    #[must_use]
109    pub fn extra_scale(ctx: &Context, theme: Theme) -> Self {
110        let mut colorix = Self {
111            theme,
112            ..Default::default()
113        };
114        let mode = ctx.style().visuals.dark_mode;
115        colorix.set_colorix_mode(mode);
116        colorix.get_theme_index();
117        colorix.apply_to = ApplyTo::ExtraScale;
118        colorix.tokens.apply_to = ApplyTo::ExtraScale;
119        colorix.update_colors(Some(ctx), None);
120        colorix
121    }
122    #[must_use]
123    pub fn local_from_style(theme: Theme, dark_mode: bool) -> Self {
124        let mut colorix = Self::default();
125        colorix.set_colorix_mode(dark_mode);
126        colorix.theme = theme;
127        colorix.apply_to = ApplyTo::Local;
128        colorix.tokens.apply_to = ApplyTo::Local;
129        colorix.update_colors(None, None);
130        colorix
131    }
132    /// Set animator
133    /// # Example
134    ///
135    /// ```ignore
136    /// impl App {
137    ///     fn new(ctx: &Context) -> Self {
138    ///         let yellow_theme = [ThemeColor::Custom([232, 210, 7]); 12];
139    ///         let colorix = Colorix::global(ctx, yellow_theme).animated().set_time(2.0);
140    ///         Self {
141    ///             colorix,
142    ///             ..Default::default()
143    ///         }
144    ///     }
145    /// }
146    /// ```
147    #[must_use]
148    pub const fn animated(mut self) -> Self {
149        self.animated = true;
150        self.init_animator();
151        self
152    }
153    /// Change the default time (1.0) of the animation.
154    #[must_use]
155    pub const fn set_time(mut self, new_time: f32) -> Self {
156        if self.animated {
157            self.animator.set_time(new_time);
158        }
159        self
160    }
161
162    /// sets new theme and animates towards it.
163    pub fn update_theme(&mut self, ctx: &egui::Context, theme: Theme) {
164        self.theme = theme;
165        self.get_theme_index();
166        self.update_colors(Some(ctx), None);
167    }
168    // starts animation; has only visible effect on the tokenshifts.
169    pub fn shift_tokens(&mut self, ctx: &egui::Context) {
170        self.animator.start(ctx);
171    }
172    #[must_use]
173    pub const fn dark_mode(&self) -> bool {
174        self.scales.dark_mode
175    }
176
177    const fn init_animator(&mut self) {
178        self.animator = ColorAnimator::new(self.tokens);
179        self.animator.apply_to = self.apply_to;
180    }
181
182    /// Necessary to engage the color animation
183    /// works only for global ui and `extra_scale`
184    pub fn set_animator(&mut self, ctx: &Context) {
185        match self.apply_to {
186            ApplyTo::Global | ApplyTo::ExtraScale => {
187                if self.animated {
188                    self.animator.set_animate(Some(ctx), None, self.tokens);
189                }
190            }
191            ApplyTo::Local => {}
192        }
193    }
194
195    fn get_theme_index(&mut self) {
196        if let Some(i) = THEMES.iter().position(|t| t == &self.theme) {
197            self.theme_index = i;
198        }
199    }
200    /// create theme based on 1 custom color from color picker
201    pub fn twelve_from_custom(&mut self, ui: &mut Ui) {
202        self.theme = [ThemeColor::Custom(self.scales.custom()); 12];
203        self.match_and_update_colors(ui);
204    }
205
206    fn match_and_update_colors(&mut self, ui: &mut Ui) {
207        match self.apply_to {
208            ApplyTo::Global | ApplyTo::ExtraScale => {
209                self.update_colors(Some(ui.ctx()), None);
210            }
211            ApplyTo::Local => {
212                self.update_colors(None, Some(ui));
213            }
214        }
215    }
216    const fn set_colorix_mode(&mut self, mode: bool) {
217        self.scales.dark_mode = mode;
218        self.tokens.dark_mode = mode;
219    }
220    /// If you initialize a Colorix instance with a `local` scope, this needs to be placed
221    /// inside the egui `update` fn.
222    pub fn update_locally(&mut self, ui: &mut Ui) {
223        if self.apply_to == ApplyTo::Local {
224            if self.animated {
225                self.animator.set_animate(None, Some(ui), self.tokens);
226            } else {
227                self.update_colors(None, Some(ui));
228            }
229        }
230    }
231
232    fn set_ui_mode(&self, ui: &mut Ui, mode: bool) {
233        match self.apply_to {
234            ApplyTo::Global => ui.ctx().style_mut(|style| style.visuals.dark_mode = mode),
235            ApplyTo::Local => ui.style_mut().visuals.dark_mode = mode,
236            ApplyTo::ExtraScale => {}
237        }
238    }
239    /// Change the color mode to dark.
240    pub fn set_dark(&mut self, ui: &mut Ui) {
241        self.set_colorix_mode(true);
242        self.set_ui_mode(ui, true);
243        self.match_and_update_colors(ui);
244    }
245    /// Change the color mode to light.
246    pub fn set_light(&mut self, ui: &mut Ui) {
247        self.set_colorix_mode(false);
248        self.set_ui_mode(ui, false);
249        self.match_and_update_colors(ui);
250    }
251
252    fn process_theme(&mut self) {
253        let mut processed: Vec<usize> = vec![];
254        for (i, v) in self.theme.iter().enumerate() {
255            if !processed.contains(&i) {
256                self.scales.process_color(*v);
257                self.tokens.update_schema(i, self.scales.scale[i]);
258                if i < self.theme.len() {
259                    for (j, w) in self.theme[i + 1..].iter().enumerate() {
260                        if w == v {
261                            self.tokens
262                                .update_schema(j + i + 1, self.scales.scale[j + i + 1]);
263                            processed.push(j + i + 1);
264                        }
265                    }
266                }
267            }
268        }
269    }
270
271    fn match_egui_visuals(&self, ui: &mut Ui) {
272        match self.apply_to {
273            ApplyTo::Global => self.tokens.set_ctx_visuals(ui.ctx()),
274            ApplyTo::Local => self.tokens.set_ui_visuals(ui),
275            ApplyTo::ExtraScale => {}
276        }
277    }
278
279    fn update_color(&mut self, ui: &mut Ui, i: usize) {
280        self.scales.process_color(self.theme[i]);
281        self.tokens.update_schema(i, self.scales.scale[i]);
282        self.tokens.color_on_accent();
283        if self.animated {
284            self.animator.start(ui.ctx());
285        } else {
286            self.match_egui_visuals(ui);
287        }
288    }
289
290    fn update_colors(&mut self, ctx: Option<&Context>, ui: Option<&mut Ui>) {
291        if self.animated {
292            self.process_theme();
293            self.tokens.color_on_accent();
294            if let Some(ctx) = ctx {
295                self.animator.start(ctx);
296            } else if let Some(ui) = ui {
297                self.animator.start(ui.ctx());
298            }
299        } else {
300            self.process_theme();
301            self.tokens.color_on_accent();
302            if let Some(ctx) = ctx {
303                if self.apply_to != ApplyTo::ExtraScale {
304                    self.tokens.set_ctx_visuals(ctx);
305                }
306            } else if let Some(ui) = ui {
307                self.tokens.set_ui_visuals(ui);
308            }
309        }
310    }
311
312    /// WARNING: don't use the `light_dark` buttons that egui provides.
313    /// That will override the themes from this crate. It needs the size for the button in f32
314    pub fn light_dark_toggle_button(&mut self, ui: &mut Ui, size: f32) {
315        #![allow(clippy::collapsible_else_if)]
316        if self.dark_mode() {
317            if ui
318                .add(egui::Button::new(egui::RichText::new("☀").size(size)).frame(false))
319                .on_hover_text("Switch to light mode")
320                .clicked()
321            {
322                self.set_colorix_mode(false);
323                self.set_ui_mode(ui, false);
324                self.match_and_update_colors(ui);
325            }
326        } else {
327            if ui
328                .add(egui::Button::new(egui::RichText::new("🌙").size(size)).frame(false))
329                .on_hover_text("Switch to dark mode")
330                .clicked()
331            {
332                self.set_colorix_mode(true);
333                self.set_ui_mode(ui, true);
334                self.match_and_update_colors(ui);
335            }
336        }
337    }
338
339    /// Choose from a list of preset themes. It is possible to add custom themes.
340    /// NOTE: custom values chosen without the custom color picker are not recommended!
341    ///
342    /// # Examples
343    ///
344    /// ```ignore
345    /// use egui_colors::tokens::ThemeColor;
346    /// let names = vec!["YellowGreen"];
347    /// let themes = vec![[ThemeColor::Custom([178, 194, 31]); 12]];
348    /// let custom = Some((names, themes));
349    ///
350    /// // if you want to display custom themes only, set `custom_only` to `true`
351    /// app.colorix.themes_dropdown(ui, custom, false);
352    /// ```
353    pub fn themes_dropdown(
354        &mut self,
355        ui: &mut Ui,
356        custom_themes: Option<(Vec<&str>, Vec<Theme>)>,
357        custom_only: bool,
358    ) {
359        let combi_themes: Vec<Theme>;
360        let combi_names: Vec<&str>;
361
362        if let Some(custom) = custom_themes {
363            let (names, themes) = custom;
364            if custom_only {
365                combi_themes = themes;
366                combi_names = names;
367            } else {
368                combi_themes = THEMES.iter().copied().chain(themes).collect();
369                combi_names = THEME_NAMES.iter().copied().chain(names).collect();
370            }
371        } else {
372            combi_names = THEME_NAMES.to_vec();
373            combi_themes = THEMES.to_vec();
374        }
375        egui::ComboBox::from_id_salt(ui.id())
376            .selected_text(combi_names[self.theme_index])
377            .show_ui(ui, |ui| {
378                for i in 0..combi_themes.len() {
379                    if ui
380                        .selectable_value(&mut self.theme, combi_themes[i], combi_names[i])
381                        .clicked()
382                    {
383                        self.theme_index = i;
384                        self.match_and_update_colors(ui);
385                    }
386                }
387            });
388    }
389    /// A widget with 12 dropdown menus of the UI elements (`ColorTokens`) that can be set.
390    pub fn ui_combo_12(&mut self, ui: &mut Ui, copy: bool) {
391        let dropdown_colors: [ThemeColor; 23] = [
392            ThemeColor::Gray,
393            ThemeColor::EguiBlue,
394            ThemeColor::Tomato,
395            ThemeColor::Red,
396            ThemeColor::Ruby,
397            ThemeColor::Crimson,
398            ThemeColor::Pink,
399            ThemeColor::Plum,
400            ThemeColor::Purple,
401            ThemeColor::Violet,
402            ThemeColor::Iris,
403            ThemeColor::Indigo,
404            ThemeColor::Blue,
405            ThemeColor::Cyan,
406            ThemeColor::Teal,
407            ThemeColor::Jade,
408            ThemeColor::Green,
409            ThemeColor::Grass,
410            ThemeColor::Brown,
411            ThemeColor::Bronze,
412            ThemeColor::Gold,
413            ThemeColor::Orange,
414            ThemeColor::Custom(self.scales.custom()),
415        ];
416        ui.vertical(|ui| {
417            for (i, label) in LABELS.iter().enumerate() {
418                ui.horizontal(|ui| {
419                    let color_edit_size = egui::vec2(40.0, 18.0);
420                    if let Some(ThemeColor::Custom(rgb)) = self.theme.get_mut(i) {
421                        let re = ui.color_edit_button_srgb(rgb);
422                        if re.changed() {
423                            self.update_color(ui, i);
424                        }
425                    } else {
426                        // Allocate a color edit button's worth of space for non-custom presets,
427                        // for alignment purposes.
428                        ui.add_space(color_edit_size.x + ui.style().spacing.item_spacing.x);
429                    }
430                    let color = if self.animated {
431                        self.animator.animated_tokens.get_token(i)
432                    } else {
433                        self.tokens.get_token(i)
434                    };
435                    egui::widgets::color_picker::show_color(ui, color, color_edit_size);
436                    egui::ComboBox::from_label(*label)
437                        .selected_text(self.theme[i].label())
438                        .show_ui(ui, |ui| {
439                            for preset in dropdown_colors {
440                                if ui
441                                    .selectable_value(&mut self.theme[i], preset, preset.label())
442                                    .clicked()
443                                {
444                                    self.update_color(ui, i);
445                                }
446                            }
447                        });
448                });
449            }
450        });
451        if copy {
452            ui.add_space(10.);
453            if ui.button("Copy theme to clipboard").clicked() {
454                ui.ctx().copy_text(format!("{:#?}", self.theme));
455            }
456        }
457    }
458
459    /// NOTE: values are clamped for useability.
460    /// Creating custom themes outside these values is not recommended.
461    pub fn custom_picker(&mut self, ui: &mut Ui) {
462        if egui::color_picker::color_edit_button_hsva(
463            ui,
464            &mut self.scales.custom,
465            egui::color_picker::Alpha::Opaque,
466        )
467        .changed()
468        {
469            self.scales.clamp_custom();
470        }
471    }
472
473    /// Set a background gradient. Choose 'true' for color from `solid_backgrounds` (if animated `active_ui_element_background`)
474    /// and 'false' for `ui_element_background`
475    pub fn draw_background(&mut self, ctx: &Context, accent: bool) {
476        let (ui_element, background) = if self.animated {
477            (
478                self.animator.animated_tokens.ui_element_background,
479                self.animator.animated_tokens.app_background,
480            )
481        } else {
482            (
483                self.tokens.ui_element_background,
484                self.tokens.app_background,
485            )
486        };
487        let bg = if accent {
488            self.animator.animated_tokens.active_ui_element_background
489        } else {
490            ui_element
491        };
492        let rect = egui::Context::available_rect(ctx);
493        let layer_id = egui::LayerId::background();
494        let painter = egui::Painter::new(ctx.clone(), layer_id, rect);
495        let mut mesh = egui::Mesh::default();
496        mesh.colored_vertex(rect.left_top(), background);
497        mesh.colored_vertex(rect.right_top(), background);
498        mesh.colored_vertex(rect.left_bottom(), bg);
499        mesh.colored_vertex(rect.right_bottom(), bg);
500        mesh.add_triangle(0, 1, 2);
501        mesh.add_triangle(1, 2, 3);
502        painter.add(egui::Shape::Mesh(Arc::new(mesh)));
503    }
504    /// Returns the currently set theme
505    #[must_use]
506    pub const fn theme(&self) -> &Theme {
507        &self.theme
508    }
509}