Follow the System Theme in egui (#4860)

* Some initial progress towards #4490

This PR just moves `Theme` and the "follow system theme" settings to
egui and adds `RawInput.system_theme`.
A follow-up PR can then introduce the two separate `dark_mode_style` and
`light_mode_style` fields on `Options`.


<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->


* [x] I have followed the instructions in the PR template


### Breaking changes

The options `follow_system_theme` and `default_theme` has been moved
from `eframe` into `egui::Options`, settable with `ctx.options_mut`

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Tau Gärtli 2024-08-06 20:17:51 +02:00 committed by GitHub
parent ed0254288a
commit 2dac4a4fc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 129 additions and 137 deletions

View File

@ -297,21 +297,6 @@ pub struct NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu"))] #[cfg(any(feature = "glow", feature = "wgpu"))]
pub renderer: Renderer, pub renderer: Renderer,
/// Try to detect and follow the system preferred setting for dark vs light mode.
///
/// The theme will automatically change when the dark vs light mode preference is changed.
///
/// Does not work on Linux (see <https://github.com/rust-windowing/winit/issues/1549>).
///
/// See also [`Self::default_theme`].
pub follow_system_theme: bool,
/// Which theme to use in case [`Self::follow_system_theme`] is `false`
/// or eframe fails to detect the system theme.
///
/// Default: [`Theme::Dark`].
pub default_theme: Theme,
/// This controls what happens when you close the main eframe window. /// This controls what happens when you close the main eframe window.
/// ///
/// If `true`, execution will continue after the eframe window is closed. /// If `true`, execution will continue after the eframe window is closed.
@ -417,8 +402,6 @@ impl Default for NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu"))] #[cfg(any(feature = "glow", feature = "wgpu"))]
renderer: Renderer::default(), renderer: Renderer::default(),
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
default_theme: Theme::Dark,
run_and_return: true, run_and_return: true,
#[cfg(any(feature = "glow", feature = "wgpu"))] #[cfg(any(feature = "glow", feature = "wgpu"))]
@ -449,19 +432,6 @@ impl Default for NativeOptions {
/// Options when using `eframe` in a web page. /// Options when using `eframe` in a web page.
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub struct WebOptions { pub struct WebOptions {
/// Try to detect and follow the system preferred setting for dark vs light mode.
///
/// See also [`Self::default_theme`].
///
/// Default: `true`.
pub follow_system_theme: bool,
/// Which theme to use in case [`Self::follow_system_theme`] is `false`
/// or system theme detection fails.
///
/// Default: `Theme::Dark`.
pub default_theme: Theme,
/// Sets the number of bits in the depth buffer. /// Sets the number of bits in the depth buffer.
/// ///
/// `egui` doesn't need the depth buffer, so the default value is 0. /// `egui` doesn't need the depth buffer, so the default value is 0.
@ -492,8 +462,6 @@ pub struct WebOptions {
impl Default for WebOptions { impl Default for WebOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
follow_system_theme: true,
default_theme: Theme::Dark,
depth_buffer: 0, depth_buffer: 0,
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
@ -509,31 +477,6 @@ impl Default for WebOptions {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Dark or Light theme.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Theme {
/// Dark mode: light text on a dark background.
Dark,
/// Light mode: dark text on a light background.
Light,
}
impl Theme {
/// Get the egui visuals corresponding to this theme.
///
/// Use with [`egui::Context::set_visuals`].
pub fn egui_visuals(self) -> egui::Visuals {
match self {
Self::Dark => egui::Visuals::dark(),
Self::Light => egui::Visuals::light(),
}
}
}
// ----------------------------------------------------------------------------
/// WebGL Context options /// WebGL Context options
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@ -814,11 +757,6 @@ pub struct IntegrationInfo {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub web_info: WebInfo, pub web_info: WebInfo,
/// Does the OS use dark or light mode?
///
/// `None` means "don't know".
pub system_theme: Option<Theme>,
/// Seconds of cpu usage (in seconds) on the previous frame. /// Seconds of cpu usage (in seconds) on the previous frame.
/// ///
/// This includes [`App::update`] as well as rendering (except for vsync waiting). /// This includes [`App::update`] as well as rendering (except for vsync waiting).

View File

@ -10,7 +10,7 @@ use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
use egui::{DeferredViewportUiCallback, NumExt as _, ViewportBuilder, ViewportId}; use egui::{DeferredViewportUiCallback, NumExt as _, ViewportBuilder, ViewportId};
use egui_winit::{EventResponse, WindowSettings}; use egui_winit::{EventResponse, WindowSettings};
use crate::{epi, Theme}; use crate::epi;
pub fn viewport_builder( pub fn viewport_builder(
egui_zoom_factor: f32, egui_zoom_factor: f32,
@ -158,7 +158,6 @@ pub struct EpiIntegration {
close: bool, close: bool,
can_drag_window: bool, can_drag_window: bool,
follow_system_theme: bool,
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
persist_window: bool, persist_window: bool,
app_icon_setter: super::app_icon::AppTitleIconSetter, app_icon_setter: super::app_icon::AppTitleIconSetter,
@ -169,7 +168,6 @@ impl EpiIntegration {
pub fn new( pub fn new(
egui_ctx: egui::Context, egui_ctx: egui::Context,
window: &winit::window::Window, window: &winit::window::Window,
system_theme: Option<Theme>,
app_name: &str, app_name: &str,
native_options: &crate::NativeOptions, native_options: &crate::NativeOptions,
storage: Option<Box<dyn epi::Storage>>, storage: Option<Box<dyn epi::Storage>>,
@ -180,10 +178,7 @@ impl EpiIntegration {
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>, #[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
) -> Self { ) -> Self {
let frame = epi::Frame { let frame = epi::Frame {
info: epi::IntegrationInfo { info: epi::IntegrationInfo { cpu_usage: None },
system_theme,
cpu_usage: None,
},
storage, storage,
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl, gl,
@ -217,7 +212,6 @@ impl EpiIntegration {
pending_full_output: Default::default(), pending_full_output: Default::default(),
close: false, close: false,
can_drag_window: false, can_drag_window: false,
follow_system_theme: native_options.follow_system_theme,
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
persist_window: native_options.persist_window, persist_window: native_options.persist_window,
app_icon_setter, app_icon_setter,
@ -251,11 +245,6 @@ impl EpiIntegration {
state: ElementState::Pressed, state: ElementState::Pressed,
.. ..
} => self.can_drag_window = true, } => self.can_drag_window = true,
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());
}
_ => {} _ => {}
} }
@ -398,10 +387,3 @@ 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,
}
}

View File

@ -228,14 +228,11 @@ impl GlowWinitApp {
} }
} }
let system_theme =
winit_integration::system_theme(&glutin.window(ViewportId::ROOT), &self.native_options);
let painter = Rc::new(RefCell::new(painter)); let painter = Rc::new(RefCell::new(painter));
let integration = EpiIntegration::new( let integration = EpiIntegration::new(
egui_ctx, egui_ctx,
&glutin.window(ViewportId::ROOT), &glutin.window(ViewportId::ROOT),
system_theme,
&self.app_name, &self.app_name,
&self.native_options, &self.native_options,
storage, storage,
@ -281,9 +278,6 @@ impl GlowWinitApp {
} }
} }
let theme = system_theme.unwrap_or(self.native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
if self if self
.native_options .native_options
.viewport .viewport
@ -1120,6 +1114,7 @@ impl GlutinWindowContext {
viewport_id, viewport_id,
event_loop, event_loop,
Some(window.scale_factor() as f32), Some(window.scale_factor() as f32),
window.theme(),
self.max_texture_side, self.max_texture_side,
) )
}); });

View File

@ -203,11 +203,9 @@ impl WgpuWinitApp {
let wgpu_render_state = painter.render_state(); let wgpu_render_state = painter.render_state();
let system_theme = winit_integration::system_theme(&window, &self.native_options);
let integration = EpiIntegration::new( let integration = EpiIntegration::new(
egui_ctx.clone(), egui_ctx.clone(),
&window, &window,
system_theme,
&self.app_name, &self.app_name,
&self.native_options, &self.native_options,
storage, storage,
@ -243,6 +241,7 @@ impl WgpuWinitApp {
ViewportId::ROOT, ViewportId::ROOT,
event_loop, event_loop,
Some(window.scale_factor() as f32), Some(window.scale_factor() as f32),
window.theme(),
painter.max_texture_side(), painter.max_texture_side(),
); );
@ -251,8 +250,6 @@ impl WgpuWinitApp {
let event_loop_proxy = self.repaint_proxy.lock().clone(); let event_loop_proxy = self.repaint_proxy.lock().clone();
egui_winit.init_accesskit(&window, event_loop_proxy); egui_winit.init_accesskit(&window, event_loop_proxy);
} }
let theme = system_theme.unwrap_or(self.native_options.default_theme);
egui_ctx.set_visuals(theme.egui_visuals());
let app_creator = std::mem::take(&mut self.app_creator) let app_creator = std::mem::take(&mut self.app_creator)
.expect("Single-use AppCreator has unexpectedly already been taken"); .expect("Single-use AppCreator has unexpectedly already been taken");
@ -872,6 +869,7 @@ impl Viewport {
viewport_id, viewport_id,
event_loop, event_loop,
Some(window.scale_factor() as f32), Some(window.scale_factor() as f32),
window.theme(),
painter.max_texture_side(), painter.max_texture_side(),
)); ));

View File

@ -118,16 +118,6 @@ pub enum EventResult {
Exit, Exit,
} }
pub fn system_theme(window: &Window, options: &crate::NativeOptions) -> Option<crate::Theme> {
if options.follow_system_theme {
window
.theme()
.map(super::epi_integration::theme_from_winit_theme)
} else {
None
}
}
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
pub(crate) fn on_accesskit_window_event( pub(crate) fn on_accesskit_window_event(
egui_winit: &mut egui_winit::State, egui_winit: &mut egui_winit::State,

View File

@ -38,18 +38,11 @@ impl AppRunner {
) -> Result<Self, String> { ) -> Result<Self, String> {
let painter = super::ActiveWebPainter::new(canvas, &web_options).await?; let painter = super::ActiveWebPainter::new(canvas, &web_options).await?;
let system_theme = if web_options.follow_system_theme {
super::system_theme()
} else {
None
};
let info = epi::IntegrationInfo { let info = epi::IntegrationInfo {
web_info: epi::WebInfo { web_info: epi::WebInfo {
user_agent: super::user_agent().unwrap_or_default(), user_agent: super::user_agent().unwrap_or_default(),
location: super::web_location(), location: super::web_location(),
}, },
system_theme,
cpu_usage: None, cpu_usage: None,
}; };
let storage = LocalStorage::default(); let storage = LocalStorage::default();
@ -68,9 +61,6 @@ impl AppRunner {
o.zoom_factor = 1.0; o.zoom_factor = 1.0;
}); });
let theme = system_theme.unwrap_or(web_options.default_theme);
egui_ctx.set_visuals(theme.egui_visuals());
let cc = epi::CreationContext { let cc = epi::CreationContext {
egui_ctx: egui_ctx.clone(), egui_ctx: egui_ctx.clone(),
integration_info: info.clone(), integration_info: info.clone(),
@ -132,6 +122,7 @@ impl AppRunner {
.entry(egui::ViewportId::ROOT) .entry(egui::ViewportId::ROOT)
.or_default() .or_default()
.native_pixels_per_point = Some(super::native_pixels_per_point()); .native_pixels_per_point = Some(super::native_pixels_per_point());
runner.input.raw.system_theme = super::system_theme();
Ok(runner) Ok(runner)
} }

View File

@ -94,6 +94,7 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
install_wheel(runner_ref, &canvas)?; install_wheel(runner_ref, &canvas)?;
install_drag_and_drop(runner_ref, &canvas)?; install_drag_and_drop(runner_ref, &canvas)?;
install_window_events(runner_ref, &window)?; install_window_events(runner_ref, &window)?;
install_color_scheme_change_event(runner_ref, &window)?;
Ok(()) Ok(())
} }
@ -353,17 +354,17 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result
Ok(()) Ok(())
} }
pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Result<(), JsValue> { fn install_color_scheme_change_event(
let window = web_sys::window().unwrap(); runner_ref: &WebRunner,
window: &web_sys::Window,
if let Some(media_query_list) = prefers_color_scheme_dark(&window)? { ) -> Result<(), JsValue> {
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>( runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list, &media_query_list,
"change", "change",
|event, runner| { |event, runner| {
let theme = theme_from_dark_mode(event.matches()); let theme = theme_from_dark_mode(event.matches());
runner.frame.info.system_theme = Some(theme); runner.input.raw.system_theme = Some(theme);
runner.egui_ctx().set_visuals(theme.egui_visuals());
runner.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
}, },
)?; )?;

View File

@ -45,8 +45,6 @@ use web_sys::MediaQueryList;
use input::*; use input::*;
use crate::Theme;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
pub(crate) fn string_from_js_value(value: &JsValue) -> String { pub(crate) fn string_from_js_value(value: &JsValue) -> String {
@ -103,7 +101,7 @@ pub fn native_pixels_per_point() -> f32 {
/// Ask the browser about the preferred system theme. /// Ask the browser about the preferred system theme.
/// ///
/// `None` means unknown. /// `None` means unknown.
pub fn system_theme() -> Option<Theme> { pub fn system_theme() -> Option<egui::Theme> {
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?) let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
.ok()?? .ok()??
.matches(); .matches();
@ -114,11 +112,11 @@ fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQue
window.match_media("(prefers-color-scheme: dark)") window.match_media("(prefers-color-scheme: dark)")
} }
fn theme_from_dark_mode(dark_mode: bool) -> Theme { fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
if dark_mode { if dark_mode {
Theme::Dark egui::Theme::Dark
} else { } else {
Theme::Light egui::Theme::Light
} }
} }

View File

@ -63,8 +63,6 @@ impl WebRunner {
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
self.destroy(); self.destroy();
let follow_system_theme = web_options.follow_system_theme;
let text_agent = TextAgent::attach(self)?; let text_agent = TextAgent::attach(self)?;
let runner = AppRunner::new(canvas, web_options, app_creator, text_agent).await?; let runner = AppRunner::new(canvas, web_options, app_creator, text_agent).await?;
@ -83,10 +81,6 @@ impl WebRunner {
{ {
events::install_event_handlers(self)?; events::install_event_handlers(self)?;
if follow_system_theme {
events::install_color_scheme_change_event(self)?;
}
// The resize observer handles calling `request_animation_frame` to start the render loop. // The resize observer handles calling `request_animation_frame` to start the render loop.
events::install_resize_observer(self)?; events::install_resize_observer(self)?;
} }

View File

@ -14,7 +14,7 @@ pub use accesskit_winit;
pub use egui; pub use egui;
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
use egui::accesskit; use egui::accesskit;
use egui::{Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo}; use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo};
pub use winit; pub use winit;
pub mod clipboard; pub mod clipboard;
@ -111,6 +111,7 @@ impl State {
viewport_id: ViewportId, viewport_id: ViewportId,
display_target: &dyn HasDisplayHandle, display_target: &dyn HasDisplayHandle,
native_pixels_per_point: Option<f32>, native_pixels_per_point: Option<f32>,
theme: Option<winit::window::Theme>,
max_texture_side: Option<usize>, max_texture_side: Option<usize>,
) -> Self { ) -> Self {
crate::profile_function!(); crate::profile_function!();
@ -150,6 +151,7 @@ impl State {
.entry(ViewportId::ROOT) .entry(ViewportId::ROOT)
.or_default() .or_default()
.native_pixels_per_point = native_pixels_per_point; .native_pixels_per_point = native_pixels_per_point;
slf.egui_input.system_theme = theme.map(to_egui_theme);
if let Some(max_texture_side) = max_texture_side { if let Some(max_texture_side) = max_texture_side {
slf.set_max_texture_side(max_texture_side); slf.set_max_texture_side(max_texture_side);
@ -403,6 +405,13 @@ impl State {
consumed: false, consumed: false,
} }
} }
WindowEvent::ThemeChanged(winit_theme) => {
self.egui_input.system_theme = Some(to_egui_theme(*winit_theme));
EventResponse {
repaint: true,
consumed: false,
}
}
WindowEvent::HoveredFile(path) => { WindowEvent::HoveredFile(path) => {
self.egui_input.hovered_files.push(egui::HoveredFile { self.egui_input.hovered_files.push(egui::HoveredFile {
path: Some(path.clone()), path: Some(path.clone()),
@ -462,7 +471,6 @@ impl State {
| WindowEvent::Occluded(_) | WindowEvent::Occluded(_)
| WindowEvent::Resized(_) | WindowEvent::Resized(_)
| WindowEvent::Moved(_) | WindowEvent::Moved(_)
| WindowEvent::ThemeChanged(_)
| WindowEvent::TouchpadPressure { .. } | WindowEvent::TouchpadPressure { .. }
| WindowEvent::CloseRequested => EventResponse { | WindowEvent::CloseRequested => EventResponse {
repaint: true, repaint: true,
@ -890,6 +898,13 @@ impl State {
} }
} }
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
match theme {
winit::window::Theme::Dark => Theme::Dark,
winit::window::Theme::Light => Theme::Light,
}
}
pub fn inner_rect_in_points(window: &Window, pixels_per_point: f32) -> Option<Rect> { pub fn inner_rect_in_points(window: &Window, pixels_per_point: f32) -> Option<Rect> {
let inner_pos_px = window.inner_position().ok()?; let inner_pos_px = window.inner_position().ok()?;
let inner_pos_px = egui::pos2(inner_pos_px.x as f32, inner_pos_px.y as f32); let inner_pos_px = egui::pos2(inner_pos_px.x as f32, inner_pos_px.y as f32);

View File

@ -2,7 +2,7 @@
use epaint::ColorImage; use epaint::ColorImage;
use crate::{emath::*, Key, ViewportId, ViewportIdMap}; use crate::{emath::*, Key, Theme, ViewportId, ViewportIdMap};
/// What the integrations provides to egui at the start of each frame. /// What the integrations provides to egui at the start of each frame.
/// ///
@ -73,6 +73,11 @@ pub struct RawInput {
/// ///
/// False when the user alt-tab away from the application, for instance. /// False when the user alt-tab away from the application, for instance.
pub focused: bool, pub focused: bool,
/// Does the OS use dark or light mode?
///
/// `None` means "don't know".
pub system_theme: Option<Theme>,
} }
impl Default for RawInput { impl Default for RawInput {
@ -89,6 +94,7 @@ impl Default for RawInput {
hovered_files: Default::default(), hovered_files: Default::default(),
dropped_files: Default::default(), dropped_files: Default::default(),
focused: true, // integrations opt into global focus tracking focused: true, // integrations opt into global focus tracking
system_theme: None,
} }
} }
} }
@ -117,6 +123,7 @@ impl RawInput {
hovered_files: self.hovered_files.clone(), hovered_files: self.hovered_files.clone(),
dropped_files: std::mem::take(&mut self.dropped_files), dropped_files: std::mem::take(&mut self.dropped_files),
focused: self.focused, focused: self.focused,
system_theme: self.system_theme,
} }
} }
@ -134,6 +141,7 @@ impl RawInput {
mut hovered_files, mut hovered_files,
mut dropped_files, mut dropped_files,
focused, focused,
system_theme,
} = newer; } = newer;
self.viewport_id = viewport_ids; self.viewport_id = viewport_ids;
@ -147,6 +155,7 @@ impl RawInput {
self.hovered_files.append(&mut hovered_files); self.hovered_files.append(&mut hovered_files);
self.dropped_files.append(&mut dropped_files); self.dropped_files.append(&mut dropped_files);
self.focused = focused; self.focused = focused;
self.system_theme = system_theme;
} }
} }
@ -189,7 +198,7 @@ pub struct ViewportInfo {
/// This should always be set, if known. /// This should always be set, if known.
/// ///
/// On web this takes browser scaling into account, /// On web this takes browser scaling into account,
/// and orresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. /// and corresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript.
pub native_pixels_per_point: Option<f32>, pub native_pixels_per_point: Option<f32>,
/// Current monitor size in egui points. /// Current monitor size in egui points.
@ -1044,6 +1053,7 @@ impl RawInput {
hovered_files, hovered_files,
dropped_files, dropped_files,
focused, focused,
system_theme,
} = self; } = self;
ui.label(format!("Active viwport: {viewport_id:?}")); ui.label(format!("Active viwport: {viewport_id:?}"));
@ -1068,6 +1078,7 @@ impl RawInput {
ui.label(format!("hovered_files: {}", hovered_files.len())); ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len())); ui.label(format!("dropped_files: {}", dropped_files.len()));
ui.label(format!("focused: {focused}")); ui.label(format!("focused: {focused}"));
ui.label(format!("system_theme: {system_theme:?}"));
ui.scope(|ui| { ui.scope(|ui| {
ui.set_min_height(150.0); ui.set_min_height(150.0);
ui.label(format!("events: {events:#?}")) ui.label(format!("events: {events:#?}"))

View File

@ -460,7 +460,7 @@ pub use {
layers::{LayerId, Order}, layers::{LayerId, Order},
layout::*, layout::*,
load::SizeHint, load::SizeHint,
memory::{Memory, Options}, memory::{Memory, Options, Theme},
painter::Painter, painter::Painter,
response::{InnerResponse, Response}, response::{InnerResponse, Response},
sense::Sense, sense::Sense,

View File

@ -8,6 +8,9 @@ use crate::{
ViewportId, ViewportIdMap, ViewportIdSet, ViewportId, ViewportIdMap, ViewportIdSet,
}; };
mod theme;
pub use theme::Theme;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// The data that egui persists between frames. /// The data that egui persists between frames.
@ -169,6 +172,21 @@ pub struct Options {
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub(crate) style: std::sync::Arc<Style>, pub(crate) style: std::sync::Arc<Style>,
/// Whether to update the visuals according to the system theme or not.
///
/// Default: `true`.
pub follow_system_theme: bool,
/// Which theme to use in case [`Self::follow_system_theme`] is set
/// and egui fails to detect the system theme.
///
/// Default: [`crate::Theme::Dark`].
pub fallback_theme: Theme,
/// Used to detect changes in system theme
#[cfg_attr(feature = "serde", serde(skip))]
system_theme: Option<Theme>,
/// Global zoom factor of the UI. /// Global zoom factor of the UI.
/// ///
/// This is used to calculate the `pixels_per_point` /// This is used to calculate the `pixels_per_point`
@ -262,6 +280,9 @@ impl Default for Options {
Self { Self {
style: Default::default(), style: Default::default(),
follow_system_theme: true,
fallback_theme: Theme::Dark,
system_theme: None,
zoom_factor: 1.0, zoom_factor: 1.0,
zoom_with_keyboard: true, zoom_with_keyboard: true,
tessellation_options: Default::default(), tessellation_options: Default::default(),
@ -278,11 +299,35 @@ impl Default for Options {
} }
} }
impl Options {
pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput) {
if self.follow_system_theme {
let theme_from_visuals = Theme::from_dark_mode(self.style.visuals.dark_mode);
let current_system_theme = self.system_theme.unwrap_or(theme_from_visuals);
let new_system_theme = new_raw_input.system_theme.unwrap_or(self.fallback_theme);
// Only update the visuals if the system theme has changed.
// This allows users to change the visuals without them
// getting reset on the next frame.
if current_system_theme != new_system_theme || self.system_theme.is_none() {
self.system_theme = Some(new_system_theme);
if theme_from_visuals != new_system_theme {
let visuals = new_system_theme.default_visuals();
std::sync::Arc::make_mut(&mut self.style).visuals = visuals;
}
}
}
}
}
impl Options { impl Options {
/// Show the options in the ui. /// Show the options in the ui.
pub fn ui(&mut self, ui: &mut crate::Ui) { pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self { let Self {
style, // covered above style, // covered above
follow_system_theme: _,
fallback_theme: _,
system_theme: _,
zoom_factor: _, // TODO(emilk) zoom_factor: _, // TODO(emilk)
zoom_with_keyboard, zoom_with_keyboard,
tessellation_options, tessellation_options,
@ -665,6 +710,8 @@ impl Memory {
// self.interactions is handled elsewhere // self.interactions is handled elsewhere
self.options.begin_frame(new_raw_input);
self.focus self.focus
.entry(self.viewport_id) .entry(self.viewport_id)
.or_default() .or_default()

View File

@ -0,0 +1,29 @@
/// Dark or Light theme.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Theme {
/// Dark mode: light text on a dark background.
Dark,
/// Light mode: dark text on a light background.
Light,
}
impl Theme {
/// Default visuals for this theme.
pub fn default_visuals(self) -> crate::Visuals {
match self {
Self::Dark => crate::Visuals::dark(),
Self::Light => crate::Visuals::light(),
}
}
/// Chooses between [`Self::Dark`] or [`Self::Light`] based on a boolean value.
pub fn from_dark_mode(dark_mode: bool) -> Self {
if dark_mode {
Self::Dark
} else {
Self::Light
}
}
}

View File

@ -192,7 +192,8 @@ impl winit::application::ApplicationHandler<UserEvent> for GlowApp {
let gl = std::sync::Arc::new(gl); let gl = std::sync::Arc::new(gl);
gl_window.window().set_visible(true); gl_window.window().set_visible(true);
let egui_glow = egui_glow::EguiGlow::new(event_loop, gl.clone(), None, None, true); let egui_glow =
egui_glow::EguiGlow::new(event_loop, &gl_window.window, gl.clone(), None, None, true);
let event_loop_proxy = egui::mutex::Mutex::new(self.proxy.clone()); let event_loop_proxy = egui::mutex::Mutex::new(self.proxy.clone());
egui_glow egui_glow

View File

@ -24,6 +24,7 @@ impl EguiGlow {
/// For automatic shader version detection set `shader_version` to `None`. /// For automatic shader version detection set `shader_version` to `None`.
pub fn new( pub fn new(
event_loop: &winit::event_loop::ActiveEventLoop, event_loop: &winit::event_loop::ActiveEventLoop,
window: &winit::window::Window,
gl: std::sync::Arc<glow::Context>, gl: std::sync::Arc<glow::Context>,
shader_version: Option<ShaderVersion>, shader_version: Option<ShaderVersion>,
native_pixels_per_point: Option<f32>, native_pixels_per_point: Option<f32>,
@ -42,6 +43,7 @@ impl EguiGlow {
ViewportId::ROOT, ViewportId::ROOT,
event_loop, event_loop,
native_pixels_per_point, native_pixels_per_point,
window.theme(),
Some(painter.max_texture_side()), Some(painter.max_texture_side()),
); );