diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 92eba0fb0..00af44aa7 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -427,6 +427,7 @@ impl EffectcraftApp { self.ui.theme = theme::ThemeKind::from_name(p.appearance.resolved_theme(system)).unwrap_or_default(); self.tokens = Tokens::from_prefs(p, self.ui.theme); theme::apply_visuals(ctx, &self.tokens); + ctx.set_theme(theme::theme_preference(&p.appearance.appearance_mode, self.ui.theme.is_light())); let tips = p.general.show_tool_tips; ctx.all_styles_mut(|s| s.interaction.tooltip_delay = if tips { 0.5 } else { f32::INFINITY }); // Settings ▸ Appearance ▸ UI Scale (#284). egui's own Ctrl+= / Ctrl+- / Ctrl+0 zoom stays @@ -1891,6 +1892,35 @@ mod appearance_tests { assert_eq!(app.ui.theme, ThemeKind::Dark); } + /// Sync with System leaves egui's theme preference at System, so egui never gives the native + /// window an appearance of its own (on macOS that stops system appearance changes reaching + /// the app, #642); egui's light and dark styles both carry the resolved theme. Fixed modes + /// pin the window to their family. + #[test] + fn auto_mode_leaves_the_native_window_following_the_system() { + let ctx = egui::Context::default(); + let mut app = app(); + app.hooks.system_theme = Some(Box::new(|_| Some(egui::Theme::Light))); + app.set_pref("appearance.appearanceMode", json!("auto")).unwrap(); + app.apply_prefs(&ctx); + assert_eq!(app.ui.theme, ThemeKind::Light); + assert_eq!(ctx.options(|o| o.theme_preference), egui::ThemePreference::System); + for theme in [egui::Theme::Light, egui::Theme::Dark] { + assert_eq!(ctx.style_of(theme).visuals.panel_fill, app.tokens.panel_bg, "{theme:?}"); + } + // egui asks the window to follow the system, never to take Light or Dark. + let mut out = ctx.run_ui(egui::RawInput::default(), |_| {}); + out.textures_delta.clear(); + let cmds = out.viewport_output.values().flat_map(|v| v.commands.iter()).collect::>(); + assert!(!cmds.iter().any(|c| matches!(c, egui::ViewportCommand::SetTheme(t) if t != &egui::SystemTheme::SystemDefault)), "{cmds:?}"); + for (mode, pref) in [("light", egui::ThemePreference::Light), ("dark", egui::ThemePreference::Dark)] { + app.set_pref("appearance.appearanceMode", json!(mode)).unwrap(); + app.apply_prefs(&ctx); + assert_eq!(ctx.options(|o| o.theme_preference), pref, "{mode}"); + assert_eq!(ctx.global_style().visuals.panel_fill, app.tokens.panel_bg, "{mode}"); + } + } + /// Picking a theme (`view.theme.*`, `ui.set {theme}`) fixes the mode to its family. #[test] fn picking_a_theme_fixes_its_mode() { diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 928dca9b4..febd19918 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -405,11 +405,20 @@ fn is_chinese(language: &str) -> bool { matches!(language, "zh-hans" | "zh-hant") } +/// egui's theme preference for an appearance mode showing a `light` (or dark) theme. Sync with +/// System leaves it at System: a concrete preference makes egui set the native window's +/// appearance, and on macOS a window with its own appearance no longer hears when the system's +/// changes (#642). A fixed Light or Dark mode pins the window to match. +pub fn theme_preference(appearance_mode: &str, light: bool) -> egui::ThemePreference { + match appearance_mode { + "auto" => egui::ThemePreference::System, + _ if light => egui::ThemePreference::Light, + _ => egui::ThemePreference::Dark, + } +} + pub fn apply_visuals(ctx: &egui::Context, t: &Tokens) { let light = t.kind.is_light(); - // egui keeps one style per light/dark theme and by default picks it by the system appearance; - // pin it to ours so a fixed Dark or Light mode isn't swapped for egui's defaults. - ctx.set_theme(if light { egui::ThemePreference::Light } else { egui::ThemePreference::Dark }); let mut v = if light { Visuals::light() } else { Visuals::dark() }; v.panel_fill = t.panel_bg; v.window_fill = t.panel_bg; @@ -444,8 +453,10 @@ pub fn apply_visuals(ctx: &egui::Context, t: &Tokens) { v.widgets.active.fg_stroke = Stroke::new(1.0, t.tab_text_active); v.widgets.open.bg_fill = t.hover; v.widgets.open.weak_bg_fill = t.hover; - ctx.set_visuals(v); - ctx.global_style_mut(|s| { + // egui keeps one style per light/dark theme and picks it by its theme preference (see + // [`theme_preference`]); both get ours, so whichever it picks, egui's defaults never show. + ctx.all_styles_mut(|s| { + s.visuals = v.clone(); s.spacing.item_spacing = egui::vec2(6.0, 4.0); s.spacing.button_padding = egui::vec2(8.0, 3.0); s.spacing.interact_size.y = 22.0;