From 977749b0e00dd98d7dfeeb311737a3fd5fa9824e Mon Sep 17 00:00:00 2001 From: Ruben <4602612+bash@users.noreply.github.com> Date: Wed, 29 Mar 2023 16:39:30 +0200 Subject: [PATCH] eframe: Automatically change theme when system dark/light mode changes (#2750) * React to ThemeChanged event from winit * React to theme change using media query change event in WASM * Share conversion from bool -> Theme * Suppress too_many_arguments warning * Document limitations of automatically following the dark vs light mode preference * Simplify expression * Conditionally compile code to prevent unused item warnings * Remove needless borrow * Remove another needless borrow * Make associated functions to standalone * Request repaint after theme has changed * Only install event listener when `follow_system_theme` is enabled * Remove dark-light feature gate * Detect system theme using winit * Update documentation * Fix typos * fix warning about unused argument --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/Cargo.toml | 1 + crates/eframe/src/epi.rs | 6 +++-- crates/eframe/src/native/epi_integration.rs | 16 +++++++++++++ crates/eframe/src/native/run.rs | 25 +++++++++++++++++++-- crates/eframe/src/web/backend.rs | 9 ++++++-- crates/eframe/src/web/events.rs | 21 +++++++++++++++++ crates/eframe/src/web/mod.rs | 19 ++++++++++++---- 7 files changed, 87 insertions(+), 10 deletions(-) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 9e0ef786..3f62b0c8 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -152,6 +152,7 @@ web-sys = { version = "0.3.58", features = [ "KeyboardEvent", "Location", "MediaQueryList", + "MediaQueryListEvent", "MouseEvent", "Navigator", "Performance", diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 983f523a..6f3d8d85 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -323,13 +323,15 @@ pub struct NativeOptions { #[cfg(any(feature = "glow", feature = "wgpu"))] pub renderer: Renderer, - /// Only used if the `dark-light` feature is enabled: - /// /// Try to detect and follow the system preferred setting for dark vs light mode. /// /// By default, this is `true` on Mac and Windows, but `false` on Linux /// due to . /// + /// On Mac and Windows the theme will automatically change when the dark vs light mode preference is changed. + /// + /// This only works on Linux if the `dark-light` feature is enabled. + /// /// See also [`Self::default_theme`]. pub follow_system_theme: bool, diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 69641699..800943b6 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -310,14 +310,17 @@ pub struct EpiIntegration { close: bool, can_drag_window: bool, window_state: WindowState, + follow_system_theme: bool, } impl EpiIntegration { + #[allow(clippy::too_many_arguments)] pub fn new( event_loop: &EventLoopWindowTarget, max_texture_side: usize, window: &winit::window::Window, system_theme: Option, + follow_system_theme: bool, storage: Option>, #[cfg(feature = "glow")] gl: Option>, #[cfg(feature = "wgpu")] wgpu_render_state: Option, @@ -366,6 +369,7 @@ impl EpiIntegration { close: false, can_drag_window: false, window_state, + follow_system_theme, } } @@ -429,6 +433,11 @@ impl EpiIntegration { WindowEvent::ScaleFactorChanged { scale_factor, .. } => { self.frame.info.native_pixels_per_point = Some(*scale_factor as _); } + WindowEvent::ThemeChanged(winit_theme) if self.follow_system_theme => { + let theme = theme_from_winit_theme(*winit_theme); + self.frame.info.system_theme = Some(theme); + self.egui_ctx.set_visuals(theme.egui_visuals()); + } _ => {} } @@ -569,3 +578,10 @@ pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option Theme { + match theme { + winit::window::Theme::Dark => Theme::Dark, + winit::window::Theme::Light => Theme::Light, + } +} diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 0a686299..c9a4ebaa 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -675,12 +675,13 @@ mod glow_integration { egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version) .unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error)); - let system_theme = self.native_options.system_theme(); + let system_theme = system_theme(gl_window.window(), &self.native_options); let mut integration = epi_integration::EpiIntegration::new( event_loop, painter.max_texture_side(), gl_window.window(), system_theme, + self.native_options.follow_system_theme, storage, Some(gl.clone()), #[cfg(feature = "wgpu")] @@ -1129,12 +1130,13 @@ mod wgpu_integration { let wgpu_render_state = painter.render_state(); - let system_theme = self.native_options.system_theme(); + let system_theme = system_theme(&window, &self.native_options); let mut integration = epi_integration::EpiIntegration::new( event_loop, painter.max_texture_side().unwrap_or(2048), &window, system_theme, + self.native_options.follow_system_theme, storage, #[cfg(feature = "glow")] None, @@ -1438,3 +1440,22 @@ mod wgpu_integration { #[cfg(feature = "wgpu")] pub use wgpu_integration::run_wgpu; + +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Option { + if options.follow_system_theme { + window + .theme() + .map(super::epi_integration::theme_from_winit_theme) + } else { + None + } +} + +// Winit only reads the system theme on macOS and Windows. +// On Linux we have to fall back on dark-light (if enabled). +// See: https://github.com/rust-windowing/winit/issues/1549 +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +fn system_theme(_window: &winit::window::Window, options: &NativeOptions) -> Option { + options.system_theme() +} diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 76d05af5..0d2301fc 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -530,15 +530,16 @@ pub async fn start( tracing::warn!( "eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work." ); + let follow_system_theme = web_options.follow_system_theme; let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?; runner.warm_up()?; - start_runner(runner) + start_runner(runner, follow_system_theme) } /// Install event listeners to register different input events /// and starts running the given [`AppRunner`]. -fn start_runner(app_runner: AppRunner) -> Result { +fn start_runner(app_runner: AppRunner, follow_system_theme: bool) -> Result { let mut runner_container = AppRunnerContainer { runner: Arc::new(Mutex::new(app_runner)), panicked: Arc::new(AtomicBool::new(false)), @@ -549,6 +550,10 @@ fn start_runner(app_runner: AppRunner) -> Result { super::events::install_document_events(&mut runner_container)?; text_agent::install_text_agent(&mut runner_container)?; + if follow_system_theme { + super::events::install_color_scheme_change_event(&mut runner_container)?; + } + super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?; // Disable all event handlers on panic diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index d85adaa3..b2ee7bc4 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -207,6 +207,27 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res Ok(()) } +pub fn install_color_scheme_change_event( + runner_container: &mut AppRunnerContainer, +) -> Result<(), JsValue> { + let window = web_sys::window().unwrap(); + + if let Some(media_query_list) = prefers_color_scheme_dark(&window)? { + runner_container.add_event_listener::( + &media_query_list, + "change", + |event, mut runner_lock| { + let theme = theme_from_dark_mode(event.matches()); + runner_lock.frame.info.system_theme = Some(theme); + runner_lock.egui_ctx().set_visuals(theme.egui_visuals()); + runner_lock.needs_repaint.repaint_asap(); + }, + )?; + } + + Ok(()) +} + pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> { let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap(); diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index eca53743..a6ac0143 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -36,7 +36,7 @@ use std::sync::{ use egui::Vec2; use wasm_bindgen::prelude::*; -use web_sys::EventTarget; +use web_sys::{EventTarget, MediaQueryList}; use input::*; @@ -75,11 +75,22 @@ pub fn native_pixels_per_point() -> f32 { } pub fn system_theme() -> Option { - let dark_mode = web_sys::window()? - .match_media("(prefers-color-scheme: dark)") + let dark_mode = prefers_color_scheme_dark(&web_sys::window()?) .ok()?? .matches(); - Some(if dark_mode { Theme::Dark } else { Theme::Light }) + Some(theme_from_dark_mode(dark_mode)) +} + +fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result, JsValue> { + window.match_media("(prefers-color-scheme: dark)") +} + +fn theme_from_dark_mode(dark_mode: bool) -> Theme { + if dark_mode { + Theme::Dark + } else { + Theme::Light + } } pub fn canvas_element(canvas_id: &str) -> Option {