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 <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
870264b005
commit
977749b0e0
|
|
@ -152,6 +152,7 @@ web-sys = { version = "0.3.58", features = [
|
||||||
"KeyboardEvent",
|
"KeyboardEvent",
|
||||||
"Location",
|
"Location",
|
||||||
"MediaQueryList",
|
"MediaQueryList",
|
||||||
|
"MediaQueryListEvent",
|
||||||
"MouseEvent",
|
"MouseEvent",
|
||||||
"Navigator",
|
"Navigator",
|
||||||
"Performance",
|
"Performance",
|
||||||
|
|
|
||||||
|
|
@ -323,13 +323,15 @@ pub struct NativeOptions {
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub renderer: Renderer,
|
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.
|
/// 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
|
/// By default, this is `true` on Mac and Windows, but `false` on Linux
|
||||||
/// due to <https://github.com/frewsxcv/rust-dark-light/issues/17>.
|
/// due to <https://github.com/frewsxcv/rust-dark-light/issues/17>.
|
||||||
///
|
///
|
||||||
|
/// 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`].
|
/// See also [`Self::default_theme`].
|
||||||
pub follow_system_theme: bool,
|
pub follow_system_theme: bool,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -310,14 +310,17 @@ pub struct EpiIntegration {
|
||||||
close: bool,
|
close: bool,
|
||||||
can_drag_window: bool,
|
can_drag_window: bool,
|
||||||
window_state: WindowState,
|
window_state: WindowState,
|
||||||
|
follow_system_theme: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EpiIntegration {
|
impl EpiIntegration {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new<E>(
|
pub fn new<E>(
|
||||||
event_loop: &EventLoopWindowTarget<E>,
|
event_loop: &EventLoopWindowTarget<E>,
|
||||||
max_texture_side: usize,
|
max_texture_side: usize,
|
||||||
window: &winit::window::Window,
|
window: &winit::window::Window,
|
||||||
system_theme: Option<Theme>,
|
system_theme: Option<Theme>,
|
||||||
|
follow_system_theme: bool,
|
||||||
storage: Option<Box<dyn epi::Storage>>,
|
storage: Option<Box<dyn epi::Storage>>,
|
||||||
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
|
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
|
||||||
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||||
|
|
@ -366,6 +369,7 @@ impl EpiIntegration {
|
||||||
close: false,
|
close: false,
|
||||||
can_drag_window: false,
|
can_drag_window: false,
|
||||||
window_state,
|
window_state,
|
||||||
|
follow_system_theme,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,6 +433,11 @@ impl EpiIntegration {
|
||||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||||
self.frame.info.native_pixels_per_point = Some(*scale_factor as _);
|
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<egui::Mem
|
||||||
#[cfg(not(feature = "persistence"))]
|
#[cfg(not(feature = "persistence"))]
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn theme_from_winit_theme(theme: winit::window::Theme) -> Theme {
|
||||||
|
match theme {
|
||||||
|
winit::window::Theme::Dark => Theme::Dark,
|
||||||
|
winit::window::Theme::Light => Theme::Light,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -675,12 +675,13 @@ mod glow_integration {
|
||||||
egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version)
|
egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version)
|
||||||
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
|
.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(
|
let mut integration = epi_integration::EpiIntegration::new(
|
||||||
event_loop,
|
event_loop,
|
||||||
painter.max_texture_side(),
|
painter.max_texture_side(),
|
||||||
gl_window.window(),
|
gl_window.window(),
|
||||||
system_theme,
|
system_theme,
|
||||||
|
self.native_options.follow_system_theme,
|
||||||
storage,
|
storage,
|
||||||
Some(gl.clone()),
|
Some(gl.clone()),
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
|
|
@ -1129,12 +1130,13 @@ mod wgpu_integration {
|
||||||
|
|
||||||
let wgpu_render_state = painter.render_state();
|
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(
|
let mut integration = epi_integration::EpiIntegration::new(
|
||||||
event_loop,
|
event_loop,
|
||||||
painter.max_texture_side().unwrap_or(2048),
|
painter.max_texture_side().unwrap_or(2048),
|
||||||
&window,
|
&window,
|
||||||
system_theme,
|
system_theme,
|
||||||
|
self.native_options.follow_system_theme,
|
||||||
storage,
|
storage,
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
None,
|
None,
|
||||||
|
|
@ -1438,3 +1440,22 @@ mod wgpu_integration {
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
pub use wgpu_integration::run_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<crate::Theme> {
|
||||||
|
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<crate::Theme> {
|
||||||
|
options.system_theme()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -530,15 +530,16 @@ pub async fn start(
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
|
"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?;
|
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
|
||||||
runner.warm_up()?;
|
runner.warm_up()?;
|
||||||
start_runner(runner)
|
start_runner(runner, follow_system_theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install event listeners to register different input events
|
/// Install event listeners to register different input events
|
||||||
/// and starts running the given [`AppRunner`].
|
/// and starts running the given [`AppRunner`].
|
||||||
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
|
fn start_runner(app_runner: AppRunner, follow_system_theme: bool) -> Result<AppRunnerRef, JsValue> {
|
||||||
let mut runner_container = AppRunnerContainer {
|
let mut runner_container = AppRunnerContainer {
|
||||||
runner: Arc::new(Mutex::new(app_runner)),
|
runner: Arc::new(Mutex::new(app_runner)),
|
||||||
panicked: Arc::new(AtomicBool::new(false)),
|
panicked: Arc::new(AtomicBool::new(false)),
|
||||||
|
|
@ -549,6 +550,10 @@ fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
|
||||||
super::events::install_document_events(&mut runner_container)?;
|
super::events::install_document_events(&mut runner_container)?;
|
||||||
text_agent::install_text_agent(&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())?;
|
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
|
||||||
|
|
||||||
// Disable all event handlers on panic
|
// Disable all event handlers on panic
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,27 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||||
Ok(())
|
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::<web_sys::MediaQueryListEvent>(
|
||||||
|
&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> {
|
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||||
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ use std::sync::{
|
||||||
|
|
||||||
use egui::Vec2;
|
use egui::Vec2;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::EventTarget;
|
use web_sys::{EventTarget, MediaQueryList};
|
||||||
|
|
||||||
use input::*;
|
use input::*;
|
||||||
|
|
||||||
|
|
@ -75,11 +75,22 @@ pub fn native_pixels_per_point() -> f32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn system_theme() -> Option<Theme> {
|
pub fn system_theme() -> Option<Theme> {
|
||||||
let dark_mode = web_sys::window()?
|
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
|
||||||
.match_media("(prefers-color-scheme: dark)")
|
|
||||||
.ok()??
|
.ok()??
|
||||||
.matches();
|
.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<Option<MediaQueryList>, 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<web_sys::HtmlCanvasElement> {
|
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue