From a2695b55467d711a0cc1e72216b85d2d219aa5bc Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 7 Jul 2026 10:16:32 +0200 Subject: [PATCH] Sync window theme with egui theme --- crates/egui/src/context.rs | 40 +++++++++++++++++++++++++++++++++++ crates/egui/src/memory/mod.rs | 16 ++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1ad4a8fb1..180be7b6d 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -243,6 +243,12 @@ pub struct ViewportState { // ---------------------- // Cross-frame statistics: pub num_multipass_in_row: usize, + + /// The last theme we sent to the native window via [`ViewportCommand::SetTheme`], + /// used to avoid sending redundant commands. + /// + /// See [`crate::Options::sync_window_theme`]. + pub(crate) last_sent_window_theme: Option, } /// What called [`Context::request_repaint`] or [`Context::request_discard`]? @@ -2385,6 +2391,8 @@ impl Context { } } + self.sync_window_theme(); + #[cfg(debug_assertions)] self.debug_painting(); @@ -2396,6 +2404,38 @@ impl Context { output } + /// Keep the native window theme in sync with the egui [`ThemePreference`], + /// if [`crate::Options::sync_window_theme`] is enabled. + /// + /// Sends a [`ViewportCommand::SetTheme`] to the current viewport whenever the + /// derived theme changes, so the native window decorations match the egui theme. + fn sync_window_theme(&self) { + if !self.options(|o| o.sync_window_theme) { + return; + } + + use crate::{SystemTheme, ThemePreference}; + let window_theme = match self.options(|o| o.theme_preference) { + ThemePreference::System => SystemTheme::SystemDefault, + ThemePreference::Dark => SystemTheme::Dark, + ThemePreference::Light => SystemTheme::Light, + }; + + let changed = self.write(|ctx| { + let viewport = ctx.viewport(); + if viewport.last_sent_window_theme == Some(window_theme) { + false + } else { + viewport.last_sent_window_theme = Some(window_theme); + true + } + }); + + if changed { + self.send_viewport_cmd(ViewportCommand::SetTheme(window_theme)); + } + } + /// Called at the end of the pass. #[cfg(debug_assertions)] fn debug_painting(&self) { diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 43694453c..b189e01d0 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -216,6 +216,18 @@ pub struct Options { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) system_theme: Option, + /// If `true`, egui will keep the native window theme in sync with + /// [`Self::theme_preference`] by sending a [`crate::ViewportCommand::SetTheme`] + /// to the root viewport whenever the preference changes. + /// + /// This makes the native window decorations (title bar, borders, …) match the + /// theme selected inside egui. + /// + /// Set this to `false` if you want to manage the native window theme yourself. + /// + /// This is `true` by default. + pub sync_window_theme: bool, + /// Global zoom factor of the UI. /// /// This is used to calculate the `pixels_per_point` @@ -318,6 +330,7 @@ impl Default for Options { theme_preference: Default::default(), fallback_theme: Theme::Dark, system_theme: None, + sync_window_theme: true, zoom_factor: 1.0, zoom_with_keyboard: true, quit_shortcuts: vec![crate::KeyboardShortcut::new( @@ -381,6 +394,7 @@ impl Options { theme_preference, fallback_theme: _, system_theme: _, + sync_window_theme, zoom_factor, zoom_with_keyboard, quit_shortcuts: _, // not shown in ui @@ -429,6 +443,8 @@ impl Options { .show(ui, |ui| { theme_preference.radio_buttons(ui); + ui.checkbox(sync_window_theme, "Sync window theme with egui theme"); + let style = std::sync::Arc::make_mut(match theme { Theme::Dark => dark_style, Theme::Light => light_style,