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_shortcut_color: Option<Color32>,
156        submenu_keyboard_selection_color: Option<Color32>,
157    ) -> Self {
158        let default = Self::light();
159        Self {
160            background_color: background_color.unwrap_or(default.background_color),
161            hover_color: hover_color.unwrap_or(default.hover_color),
162            close_hover_color: close_hover_color.unwrap_or(default.close_hover_color),
163            close_icon_color: close_icon_color.unwrap_or(default.close_icon_color),
164            maximize_icon_color: maximize_icon_color.unwrap_or(default.maximize_icon_color),
165            restore_icon_color: restore_icon_color.unwrap_or(default.restore_icon_color),
166            minimize_icon_color: minimize_icon_color.unwrap_or(default.minimize_icon_color),
167            title_color: title_color.unwrap_or(default.title_color),
168            menu_text_color: menu_text_color.unwrap_or(default.menu_text_color),
169            menu_text_size: menu_text_size.unwrap_or(default.menu_text_size),
170            menu_hover_color: menu_hover_color.unwrap_or(default.menu_hover_color),
171            keyboard_selection_color: keyboard_selection_color
172                .unwrap_or(default.keyboard_selection_color),
173            submenu_background_color: submenu_background_color
174                .unwrap_or(default.submenu_background_color),
175            submenu_text_color: submenu_text_color.unwrap_or(default.submenu_text_color),
176            submenu_text_size: default.submenu_text_size,
177            submenu_hover_color: submenu_hover_color.unwrap_or(default.submenu_hover_color),
178            submenu_disabled_color: default.submenu_disabled_color,
179            submenu_shortcut_color: submenu_shortcut_color
180                .unwrap_or(default.submenu_shortcut_color),
181            submenu_border_color: default.submenu_border_color,
182            submenu_keyboard_selection_color: submenu_keyboard_selection_color
183                .unwrap_or(default.submenu_keyboard_selection_color),
184        }
185    }
186
187    /// Dark theme with selected fields overridden.
188    pub fn dark_with_overrides(
189        background_color: Option<Color32>,
190        hover_color: Option<Color32>,
191        close_hover_color: Option<Color32>,
192        close_icon_color: Option<Color32>,
193        maximize_icon_color: Option<Color32>,
194        restore_icon_color: Option<Color32>,
195        minimize_icon_color: Option<Color32>,
196        title_color: Option<Color32>,
197        menu_text_color: Option<Color32>,
198        menu_text_size: Option<f32>,
199        menu_hover_color: Option<Color32>,
200        keyboard_selection_color: Option<Color32>,
201        submenu_background_color: Option<Color32>,
202        submenu_text_color: Option<Color32>,
203        submenu_hover_color: Option<Color32>,
204        submenu_shortcut_color: Option<Color32>,
205        submenu_keyboard_selection_color: Option<Color32>,
206    ) -> Self {
207        let default = Self::dark();
208        Self {
209            background_color: background_color.unwrap_or(default.background_color),
210            hover_color: hover_color.unwrap_or(default.hover_color),
211            close_hover_color: close_hover_color.unwrap_or(default.close_hover_color),
212            close_icon_color: close_icon_color.unwrap_or(default.close_icon_color),
213            maximize_icon_color: maximize_icon_color.unwrap_or(default.maximize_icon_color),
214            restore_icon_color: restore_icon_color.unwrap_or(default.restore_icon_color),
215            minimize_icon_color: minimize_icon_color.unwrap_or(default.minimize_icon_color),
216            title_color: title_color.unwrap_or(default.title_color),
217            menu_text_color: menu_text_color.unwrap_or(default.menu_text_color),
218            menu_text_size: menu_text_size.unwrap_or(default.menu_text_size),
219            menu_hover_color: menu_hover_color.unwrap_or(default.menu_hover_color),
220            keyboard_selection_color: keyboard_selection_color
221                .unwrap_or(default.keyboard_selection_color),
222            submenu_background_color: submenu_background_color
223                .unwrap_or(default.submenu_background_color),
224            submenu_text_color: submenu_text_color.unwrap_or(default.submenu_text_color),
225            submenu_text_size: default.submenu_text_size,
226            submenu_hover_color: submenu_hover_color.unwrap_or(default.submenu_hover_color),
227            submenu_disabled_color: default.submenu_disabled_color,
228            submenu_shortcut_color: submenu_shortcut_color
229                .unwrap_or(default.submenu_shortcut_color),
230            submenu_border_color: default.submenu_border_color,
231            submenu_keyboard_selection_color: submenu_keyboard_selection_color
232                .unwrap_or(default.submenu_keyboard_selection_color),
233        }
234    }
235}
236
237pub use ThemeMode::*;
238
239/// Detect if the system is using dark mode.
240pub fn detect_system_dark_mode() -> bool {
241    #[cfg(target_os = "windows")]
242    {
243        use std::process::Command;
244
245        // On Windows, check the registry for the system theme
246        match Command::new("reg")
247            .args(&["query", "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "/v", "AppsUseLightTheme"])
248            .output()
249        {
250            Ok(output) => {
251                let output_str = String::from_utf8_lossy(&output.stdout);
252                // If AppsUseLightTheme is 0, then dark mode is enabled
253                !output_str.contains("0x1")
254            }
255            Err(_) => false, // Default to light mode if we can't detect
256        }
257    }
258
259    #[cfg(target_os = "macos")]
260    {
261        use std::process::Command;
262
263        // On macOS, check the system appearance
264        match Command::new("defaults")
265            .args(&["read", "-g", "AppleInterfaceStyle"])
266            .output()
267        {
268            Ok(output) => {
269                let output_str = String::from_utf8_lossy(&output.stdout);
270                output_str.contains("Dark")
271            }
272            Err(_) => false, // Default to light mode if we can't detect
273        }
274    }
275
276    #[cfg(target_os = "linux")]
277    {
278        use std::process::Command;
279
280        // On Linux, try to detect via gsettings (GNOME)
281        if let Ok(output) = Command::new("gsettings")
282            .args(&["get", "org.gnome.desktop.interface", "gtk-theme"])
283            .output()
284        {
285            let output_str = String::from_utf8_lossy(&output.stdout);
286            return output_str.contains("dark") || output_str.contains("Dark");
287        }
288
289        // Fallback: check environment variable
290        std::env::var("GTK_THEME")
291            .map(|theme| theme.contains("dark") || theme.contains("Dark"))
292            .unwrap_or(false)
293    }
294
295    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
296    {
297        false // Default to light mode for unknown platforms
298    }
299}