Skip to main content

egui_desktop/theme/
mod.rs

1use egui::{Color32, Visuals};
2
3/// Public API helpers for working with themes.
4pub mod api;
5
6/// Theme mode selection for the title bar and related UI.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum ThemeMode {
9    /// Light appearance.
10    Light,
11    /// Dark appearance.
12    Dark,
13    /// Follow the operating system preference.
14    System,
15}
16
17/// Colors and dimensions used to render the title bar and menus.
18pub struct TitleBarTheme {
19    /// Window/title bar background color.
20    pub background_color: Color32,
21    /// Hover background color for interactive elements.
22    pub hover_color: Color32,
23    /// Hover color for the close button (usually red-ish).
24    pub close_hover_color: Color32,
25    /// Icon color for the close button (normal state).
26    pub close_icon_color: Color32,
27    /// Icon color for the maximize button.
28    pub maximize_icon_color: Color32,
29    /// Icon color for the restore button.
30    pub restore_icon_color: Color32,
31    /// Icon color for the minimize button.
32    pub minimize_icon_color: Color32,
33    /// Title text color.
34    pub title_color: Color32,
35    /// Menu text color.
36    pub menu_text_color: Color32,
37    /// Menu text size in points.
38    pub menu_text_size: f32,
39    /// Menu hover background color.
40    pub menu_hover_color: Color32,
41    /// Highlight color used for keyboard selection in menus.
42    pub keyboard_selection_color: Color32,
43    // Submenu customization
44    /// Submenu background color.
45    pub submenu_background_color: Color32,
46    /// Submenu text color.
47    pub submenu_text_color: Color32,
48    /// Submenu text size in points.
49    pub submenu_text_size: f32,
50    /// Submenu hover background color.
51    pub submenu_hover_color: Color32,
52    /// Color for disabled submenu items.
53    pub submenu_disabled_color: Color32,
54    /// Color for displaying keyboard shortcuts in submenus.
55    pub submenu_shortcut_color: Color32,
56    /// Submenu border color.
57    pub submenu_border_color: Color32,
58    /// Highlight color for keyboard selection in submenus.
59    pub submenu_keyboard_selection_color: Color32,
60}
61
62/// A provider interface for supplying themes by identifier at runtime.
63pub trait ThemeProvider: Send + Sync {
64    /// Return a `TitleBarTheme` for the given theme id and mode, if available
65    fn get_title_bar_theme(&self, theme_id: &str, mode: ThemeMode) -> Option<TitleBarTheme>;
66    /// Return egui Visuals for the given theme id and mode, if available
67    fn get_egui_visuals(&self, theme_id: &str, mode: ThemeMode) -> Option<Visuals>;
68    /// List all available theme ids
69    fn list_available_themes(&self) -> Vec<String>;
70}
71
72/// Theme-related errors.
73#[derive(Debug)]
74pub enum ThemeError {
75    /// Requested theme or id could not be found.
76    ThemeNotFound,
77}
78
79impl Default for TitleBarTheme {
80    fn default() -> Self {
81        Self::light()
82    }
83}
84
85impl TitleBarTheme {
86    /// Built-in light theme.
87    pub fn light() -> Self {
88        Self {
89            background_color: Color32::WHITE,
90            hover_color: Color32::from_rgb(230, 230, 230),
91            close_hover_color: Color32::from_rgb(232, 17, 35),
92            close_icon_color: Color32::from_rgb(100, 100, 100),
93            maximize_icon_color: Color32::from_rgb(100, 100, 100),
94            restore_icon_color: Color32::from_rgb(100, 100, 100),
95            minimize_icon_color: Color32::from_rgb(100, 100, 100),
96            title_color: Color32::from_rgb(50, 50, 50),
97            menu_text_color: Color32::from_rgb(50, 50, 50),
98            menu_text_size: 12.0,
99            menu_hover_color: Color32::from_rgb(230, 230, 230),
100            keyboard_selection_color: Color32::from_rgb(0, 120, 215),
101            submenu_background_color: Color32::WHITE,
102            submenu_text_color: Color32::from_rgb(50, 50, 50),
103            submenu_text_size: 11.0,
104            submenu_hover_color: Color32::from_rgb(240, 240, 240),
105            submenu_disabled_color: Color32::from_rgb(150, 150, 150),
106            submenu_shortcut_color: Color32::from_rgb(100, 100, 100),
107            submenu_border_color: Color32::from_rgb(200, 200, 200),
108            submenu_keyboard_selection_color: Color32::from_rgb(0, 120, 215),
109        }
110    }
111
112    /// Built-in dark theme.
113    pub fn dark() -> Self {
114        Self {
115            background_color: Color32::from_rgb(30, 30, 30),
116            hover_color: Color32::from_rgb(60, 60, 60),
117            close_hover_color: Color32::from_rgb(232, 17, 35),
118            close_icon_color: Color32::from_rgb(200, 200, 200),
119            maximize_icon_color: Color32::from_rgb(200, 200, 200),
120            restore_icon_color: Color32::from_rgb(200, 200, 200),
121            minimize_icon_color: Color32::from_rgb(200, 200, 200),
122            title_color: Color32::from_rgb(200, 200, 200),
123            menu_text_color: Color32::from_rgb(200, 200, 200),
124            menu_text_size: 12.0,
125            menu_hover_color: Color32::from_rgb(60, 60, 60),
126            keyboard_selection_color: Color32::from_rgb(30, 144, 255),
127            submenu_background_color: Color32::from_rgb(40, 40, 40),
128            submenu_text_color: Color32::from_rgb(200, 200, 200),
129            submenu_text_size: 11.0,
130            submenu_hover_color: Color32::from_rgb(70, 70, 70),
131            submenu_disabled_color: Color32::from_rgb(120, 120, 120),
132            submenu_shortcut_color: Color32::from_rgb(160, 160, 160),
133            submenu_border_color: Color32::from_rgb(80, 80, 80),
134            submenu_keyboard_selection_color: Color32::from_rgb(30, 144, 255),
135        }
136    }
137
138    /// Light theme with selected fields overridden.
139    pub fn light_with_overrides(
140        background_color: Option<Color32>,
141        hover_color: Option<Color32>,
142        close_hover_color: Option<Color32>,
143        close_icon_color: Option<Color32>,
144        maximize_icon_color: Option<Color32>,
145        restore_icon_color: Option<Color32>,
146        minimize_icon_color: Option<Color32>,
147        title_color: Option<Color32>,
148        menu_text_color: Option<Color32>,
149        menu_text_size: Option<f32>,
150        menu_hover_color: Option<Color32>,
151        keyboard_selection_color: Option<Color32>,
152        submenu_background_color: Option<Color32>,
153        submenu_text_color: Option<Color32>,
154        submenu_hover_color: Option<Color32>,
155        submenu_disabled_color: Option<Color32>,
156        submenu_shortcut_color: Option<Color32>,
157        submenu_keyboard_selection_color: Option<Color32>,
158    ) -> Self {
159        let default = Self::light();
160        Self {
161            background_color: background_color.unwrap_or(default.background_color),
162            hover_color: hover_color.unwrap_or(default.hover_color),
163            close_hover_color: close_hover_color.unwrap_or(default.close_hover_color),
164            close_icon_color: close_icon_color.unwrap_or(default.close_icon_color),
165            maximize_icon_color: maximize_icon_color.unwrap_or(default.maximize_icon_color),
166            restore_icon_color: restore_icon_color.unwrap_or(default.restore_icon_color),
167            minimize_icon_color: minimize_icon_color.unwrap_or(default.minimize_icon_color),
168            title_color: title_color.unwrap_or(default.title_color),
169            menu_text_color: menu_text_color.unwrap_or(default.menu_text_color),
170            menu_text_size: menu_text_size.unwrap_or(default.menu_text_size),
171            menu_hover_color: menu_hover_color.unwrap_or(default.menu_hover_color),
172            keyboard_selection_color: keyboard_selection_color
173                .unwrap_or(default.keyboard_selection_color),
174            submenu_background_color: submenu_background_color
175                .unwrap_or(default.submenu_background_color),
176            submenu_text_color: submenu_text_color.unwrap_or(default.submenu_text_color),
177            submenu_text_size: default.submenu_text_size,
178            submenu_hover_color: submenu_hover_color.unwrap_or(default.submenu_hover_color),
179            submenu_disabled_color: submenu_disabled_color
180                .unwrap_or(default.submenu_disabled_color),
181            submenu_shortcut_color: submenu_shortcut_color
182                .unwrap_or(default.submenu_shortcut_color),
183            submenu_border_color: default.submenu_border_color,
184            submenu_keyboard_selection_color: submenu_keyboard_selection_color
185                .unwrap_or(default.submenu_keyboard_selection_color),
186        }
187    }
188
189    /// Dark theme with selected fields overridden.
190    pub fn dark_with_overrides(
191        background_color: Option<Color32>,
192        hover_color: Option<Color32>,
193        close_hover_color: Option<Color32>,
194        close_icon_color: Option<Color32>,
195        maximize_icon_color: Option<Color32>,
196        restore_icon_color: Option<Color32>,
197        minimize_icon_color: Option<Color32>,
198        title_color: Option<Color32>,
199        menu_text_color: Option<Color32>,
200        menu_text_size: Option<f32>,
201        menu_hover_color: Option<Color32>,
202        keyboard_selection_color: Option<Color32>,
203        submenu_background_color: Option<Color32>,
204        submenu_text_color: Option<Color32>,
205        submenu_hover_color: Option<Color32>,
206        submenu_disabled_color: Option<Color32>,
207        submenu_shortcut_color: Option<Color32>,
208        submenu_keyboard_selection_color: Option<Color32>,
209    ) -> Self {
210        let default = Self::dark();
211        Self {
212            background_color: background_color.unwrap_or(default.background_color),
213            hover_color: hover_color.unwrap_or(default.hover_color),
214            close_hover_color: close_hover_color.unwrap_or(default.close_hover_color),
215            close_icon_color: close_icon_color.unwrap_or(default.close_icon_color),
216            maximize_icon_color: maximize_icon_color.unwrap_or(default.maximize_icon_color),
217            restore_icon_color: restore_icon_color.unwrap_or(default.restore_icon_color),
218            minimize_icon_color: minimize_icon_color.unwrap_or(default.minimize_icon_color),
219            title_color: title_color.unwrap_or(default.title_color),
220            menu_text_color: menu_text_color.unwrap_or(default.menu_text_color),
221            menu_text_size: menu_text_size.unwrap_or(default.menu_text_size),
222            menu_hover_color: menu_hover_color.unwrap_or(default.menu_hover_color),
223            keyboard_selection_color: keyboard_selection_color
224                .unwrap_or(default.keyboard_selection_color),
225            submenu_background_color: submenu_background_color
226                .unwrap_or(default.submenu_background_color),
227            submenu_text_color: submenu_text_color.unwrap_or(default.submenu_text_color),
228            submenu_text_size: default.submenu_text_size,
229            submenu_hover_color: submenu_hover_color.unwrap_or(default.submenu_hover_color),
230            submenu_disabled_color: submenu_disabled_color
231                .unwrap_or(default.submenu_disabled_color),
232            submenu_shortcut_color: submenu_shortcut_color
233                .unwrap_or(default.submenu_shortcut_color),
234            submenu_border_color: default.submenu_border_color,
235            submenu_keyboard_selection_color: submenu_keyboard_selection_color
236                .unwrap_or(default.submenu_keyboard_selection_color),
237        }
238    }
239}
240
241pub use ThemeMode::*;
242
243/// Detect if the system is using dark mode.
244pub fn detect_system_dark_mode() -> bool {
245    #[cfg(target_os = "windows")]
246    {
247        use std::process::Command;
248
249        // On Windows, check the registry for the system theme
250        match Command::new("reg")
251            .args(&["query", "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "/v", "AppsUseLightTheme"])
252            .output()
253        {
254            Ok(output) => {
255                let output_str = String::from_utf8_lossy(&output.stdout);
256                // If AppsUseLightTheme is 0, then dark mode is enabled
257                !output_str.contains("0x1")
258            }
259            Err(_) => false, // Default to light mode if we can't detect
260        }
261    }
262
263    #[cfg(target_os = "macos")]
264    {
265        use std::process::Command;
266
267        // On macOS, check the system appearance
268        match Command::new("defaults")
269            .args(&["read", "-g", "AppleInterfaceStyle"])
270            .output()
271        {
272            Ok(output) => {
273                let output_str = String::from_utf8_lossy(&output.stdout);
274                output_str.contains("Dark")
275            }
276            Err(_) => false, // Default to light mode if we can't detect
277        }
278    }
279
280    #[cfg(target_os = "linux")]
281    {
282        use std::process::Command;
283
284        // On Linux, try to detect via gsettings (GNOME)
285        if let Ok(output) = Command::new("gsettings")
286            .args(&["get", "org.gnome.desktop.interface", "gtk-theme"])
287            .output()
288        {
289            let output_str = String::from_utf8_lossy(&output.stdout);
290            return output_str.contains("dark") || output_str.contains("Dark");
291        }
292
293        // Fallback: check environment variable
294        std::env::var("GTK_THEME")
295            .map(|theme| theme.contains("dark") || theme.contains("Dark"))
296            .unwrap_or(false)
297    }
298
299    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
300    {
301        false // Default to light mode for unknown platforms
302    }
303}