Skip to content

Commit

Permalink
Make Light & Dark Visuals Customizable When Following The System Theme (
Browse files Browse the repository at this point in the history
emilk#4744)

* Closes <emilk#4490>
* [x] I have followed the instructions in the PR template

---

Unfortunately, this PR contains a bunch of breaking changes because
`Context` no longer has one style, but two. I could try to add some of
the methods back if that's desired.

The most subtle change is probably that `style_mut` mutates both the
dark and the light style (which from the usage in egui itself felt like
the right choice but might be surprising to users).

I decided to deviate a bit from the data structure suggested in the
linked issue.
Instead of this:
```rust
pub theme: Theme, // Dark or Light
pub follow_system_theme: bool, // Change [`Self::theme`] based on `RawInput::system_theme`?
```

I decided to add a `ThemePreference` enum and track the current system
theme separately.
This has a couple of benefits:
* The user's theme choice is not magically overwritten on the next
frame.
* A widget for changing the theme preference only needs to know the
`ThemePreference` and not two values.
* Persisting the `theme_preference` is fine (as opposed to persisting
the `theme` field which may actually be the system theme).

The `small_toggle_button` currently only toggles between dark and light
(so you can never get back to following the system). I think it's easy
to improve on this in a follow-up PR :)
I made the function `pub(crate)` for now because it should eventually be
a method on `ThemePreference`, not `Theme`.

To showcase the new capabilities I added a new example that uses
different "accent" colors in dark and light mode:

<img
src="https://github.com/user-attachments/assets/0bf728c6-2720-47b0-a908-18bd250d15a6"
width="250" alt="A screenshot of egui's widget gallery demo in dark mode
using a purple accent color instead of the default blue accent">

<img
src="https://github.com/user-attachments/assets/e816b380-3e59-4f11-b841-8c20285988d6"
width="250" alt="A screenshot of egui's widget gallery demo in light
mode using a green accent color instead of the default blue accent">

---------

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
2 people authored and hacknus committed Oct 30, 2024
1 parent 43e4d00 commit bbecc35
Show file tree
Hide file tree
Showing 22 changed files with 381 additions and 122 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,17 @@ dependencies = [
"env_logger",
]

[[package]]
name = "custom_style"
version = "0.1.0"
dependencies = [
"eframe",
"egui_demo_lib",
"egui_extras",
"env_logger",
"image",
]

[[package]]
name = "custom_window_frame"
version = "0.1.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub struct CreationContext<'s> {
/// The egui Context.
///
/// You can use this to customize the look of egui, e.g to call [`egui::Context::set_fonts`],
/// [`egui::Context::set_visuals`] etc.
/// [`egui::Context::set_visuals_of`] etc.
pub egui_ctx: egui::Context,

/// Information about the surrounding environment.
Expand Down
121 changes: 101 additions & 20 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
layers::GraphicLayers,
load,
load::{Bytes, Loaders, SizedTexture},
memory::Options,
memory::{Options, Theme},
menu,
os::OperatingSystem,
output::FullOutput,
Expand Down Expand Up @@ -487,7 +487,7 @@ impl ContextImpl {
});

viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
let interact_radius = self.memory.options.style.interaction.interact_radius;
let interact_radius = self.memory.options.style().interaction.interact_radius;

crate::hit_test::hit_test(
&viewport.prev_frame.widgets,
Expand Down Expand Up @@ -583,7 +583,7 @@ impl ContextImpl {
crate::profile_scope!("preload_font_glyphs");
// Preload the most common characters for the most common fonts.
// This is not very important to do, but may save a few GPU operations.
for font_id in self.memory.options.style.text_styles.values() {
for font_id in self.memory.options.style().text_styles.values() {
fonts.lock().fonts.font(font_id).preload_common_characters();
}
}
Expand Down Expand Up @@ -1245,7 +1245,7 @@ impl Context {
pub fn register_widget_info(&self, id: Id, make_info: impl Fn() -> crate::WidgetInfo) {
#[cfg(debug_assertions)]
self.write(|ctx| {
if ctx.memory.options.style.debug.show_interactive_widgets {
if ctx.memory.options.style().debug.show_interactive_widgets {
ctx.viewport().this_frame.widgets.set_info(id, make_info());
}
});
Expand Down Expand Up @@ -1612,12 +1612,37 @@ impl Context {
}
}

/// The [`Style`] used by all subsequent windows, panels etc.
/// Does the OS use dark or light mode?
/// This is used when the theme preference is set to [`crate::ThemePreference::System`].
pub fn system_theme(&self) -> Option<Theme> {
self.memory(|mem| mem.options.system_theme)
}

/// The [`Theme`] used to select the appropriate [`Style`] (dark or light)
/// used by all subsequent windows, panels etc.
pub fn theme(&self) -> Theme {
self.options(|opt| opt.theme())
}

/// The [`Theme`] used to select between dark and light [`Self::style`]
/// as the active style used by all subsequent windows, panels etc.
///
/// Example:
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.set_theme(egui::Theme::Light); // Switch to light mode
/// ```
pub fn set_theme(&self, theme_preference: impl Into<crate::ThemePreference>) {
self.options_mut(|opt| opt.theme_preference = theme_preference.into());
}

/// The currently active [`Style`] used by all subsequent windows, panels etc.
pub fn style(&self) -> Arc<Style> {
self.options(|opt| opt.style.clone())
self.options(|opt| opt.style().clone())
}

/// Mutate the [`Style`] used by all subsequent windows, panels etc.
/// Mutate the currently active [`Style`] used by all subsequent windows, panels etc.
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
/// Example:
/// ```
Expand All @@ -1627,16 +1652,72 @@ impl Context {
/// });
/// ```
pub fn style_mut(&self, mutate_style: impl FnOnce(&mut Style)) {
self.options_mut(|opt| mutate_style(std::sync::Arc::make_mut(&mut opt.style)));
self.options_mut(|opt| mutate_style(Arc::make_mut(opt.style_mut())));
}

/// The [`Style`] used by all new windows, panels etc.
/// The currently active [`Style`] used by all new windows, panels etc.
///
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
/// You can also change this using [`Self::style_mut`]
/// You can also change this using [`Self::style_mut`].
///
/// You can use [`Ui::style_mut`] to change the style of a single [`Ui`].
pub fn set_style(&self, style: impl Into<Arc<Style>>) {
self.options_mut(|opt| opt.style = style.into());
self.options_mut(|opt| *opt.style_mut() = style.into());
}

/// Mutate the [`Style`]s used by all subsequent windows, panels etc. in both dark and light mode.
///
/// Example:
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.all_styles_mut(|style| {
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
/// });
/// ```
pub fn all_styles_mut(&self, mut mutate_style: impl FnMut(&mut Style)) {
self.options_mut(|opt| {
mutate_style(Arc::make_mut(&mut opt.dark_style));
mutate_style(Arc::make_mut(&mut opt.light_style));
});
}

/// The [`Style`] used by all subsequent windows, panels etc.
pub fn style_of(&self, theme: Theme) -> Arc<Style> {
self.options(|opt| match theme {
Theme::Dark => opt.dark_style.clone(),
Theme::Light => opt.light_style.clone(),
})
}

/// Mutate the [`Style`] used by all subsequent windows, panels etc.
///
/// Example:
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.style_mut_of(egui::Theme::Dark, |style| {
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
/// });
/// ```
pub fn style_mut_of(&self, theme: Theme, mutate_style: impl FnOnce(&mut Style)) {
self.options_mut(|opt| match theme {
Theme::Dark => mutate_style(Arc::make_mut(&mut opt.dark_style)),
Theme::Light => mutate_style(Arc::make_mut(&mut opt.light_style)),
});
}

/// The [`Style`] used by all new windows, panels etc.
/// Use [`Self::set_theme`] to choose between dark and light mode.
///
/// You can also change this using [`Self::style_mut_of`].
///
/// You can use [`Ui::style_mut`] to change the style of a single [`Ui`].
pub fn set_style_of(&self, theme: Theme, style: impl Into<Arc<Style>>) {
let style = style.into();
self.options_mut(|opt| match theme {
Theme::Dark => opt.dark_style = style,
Theme::Light => opt.light_style = style,
});
}

/// The [`crate::Visuals`] used by all subsequent windows, panels etc.
Expand All @@ -1646,10 +1727,10 @@ impl Context {
/// Example:
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.set_visuals(egui::Visuals::light()); // Switch to light mode
/// ctx.set_visuals_of(egui::Theme::Dark, egui::Visuals { panel_fill: egui::Color32::RED, ..Default::default() });
/// ```
pub fn set_visuals(&self, visuals: crate::Visuals) {
self.options_mut(|opt| std::sync::Arc::make_mut(&mut opt.style).visuals = visuals);
pub fn set_visuals_of(&self, theme: Theme, visuals: crate::Visuals) {
self.style_mut_of(theme, |style| style.visuals = visuals);
}

/// The number of physical pixels for each logical point.
Expand Down Expand Up @@ -2481,13 +2562,13 @@ impl Context {
/// Whether or not to debug widget layout on hover.
#[cfg(debug_assertions)]
pub fn debug_on_hover(&self) -> bool {
self.options(|opt| opt.style.debug.debug_on_hover)
self.options(|opt| opt.style().debug.debug_on_hover)
}

/// Turn on/off whether or not to debug widget layout on hover.
#[cfg(debug_assertions)]
pub fn set_debug_on_hover(&self, debug_on_hover: bool) {
self.style_mut(|style| style.debug.debug_on_hover = debug_on_hover);
self.all_styles_mut(|style| style.debug.debug_on_hover = debug_on_hover);
}
}

Expand Down Expand Up @@ -2871,11 +2952,11 @@ impl Context {
}

impl Context {
/// Edit the active [`Style`].
pub fn style_ui(&self, ui: &mut Ui) {
let mut style: Style = (*self.style()).clone();
/// Edit the [`Style`].
pub fn style_ui(&self, ui: &mut Ui, theme: Theme) {
let mut style: Style = (*self.style_of(theme)).clone();
style.ui(ui);
self.set_style(style);
self.set_style_of(theme, style);
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ pub use self::{
layers::{LayerId, Order},
layout::*,
load::SizeHint,
memory::{Memory, Options, Theme},
memory::{Memory, Options, Theme, ThemePreference},
painter::Painter,
response::{InnerResponse, Response},
sense::Sense,
Expand Down
81 changes: 52 additions & 29 deletions crates/egui/src/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
};

mod theme;
pub use theme::Theme;
pub use theme::{Theme, ThemePreference};

// ----------------------------------------------------------------------------

Expand Down Expand Up @@ -168,24 +168,30 @@ impl FocusDirection {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Options {
/// The default style for new [`Ui`](crate::Ui):s.
/// The default style for new [`Ui`](crate::Ui):s in dark mode.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) style: std::sync::Arc<Style>,
pub dark_style: std::sync::Arc<Style>,

/// Whether to update the visuals according to the system theme or not.
/// The default style for new [`Ui`](crate::Ui):s in light mode.
#[cfg_attr(feature = "serde", serde(skip))]
pub light_style: std::sync::Arc<Style>,

/// A preference for how to select between dark and light [`crate::Context::style`]
/// as the active style used by all subsequent windows, panels etc.
///
/// Default: `true`.
pub follow_system_theme: bool,
/// Default: `ThemePreference::System`.
pub theme_preference: ThemePreference,

/// Which theme to use in case [`Self::follow_system_theme`] is set
/// Which theme to use in case [`Self::theme_preference`] is [`ThemePreference::System`]
/// and egui fails to detect the system theme.
///
/// Default: [`crate::Theme::Dark`].
pub fallback_theme: Theme,

/// Used to detect changes in system theme
/// The current system theme, used to choose between
/// dark and light style in case [`Self::theme_preference`] is [`ThemePreference::System`].
#[cfg_attr(feature = "serde", serde(skip))]
system_theme: Option<Theme>,
pub(crate) system_theme: Option<Theme>,

/// Global zoom factor of the UI.
///
Expand Down Expand Up @@ -282,8 +288,9 @@ impl Default for Options {
};

Self {
style: Default::default(),
follow_system_theme: true,
dark_style: std::sync::Arc::new(Theme::Dark.default_style()),
light_style: std::sync::Arc::new(Theme::Light.default_style()),
theme_preference: ThemePreference::System,
fallback_theme: Theme::Dark,
system_theme: None,
zoom_factor: 1.0,
Expand All @@ -305,21 +312,29 @@ 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;
}
}
self.system_theme = new_raw_input.system_theme;
}

/// The currently active theme (may depend on the system theme).
pub(crate) fn theme(&self) -> Theme {
match self.theme_preference {
ThemePreference::Dark => Theme::Dark,
ThemePreference::Light => Theme::Light,
ThemePreference::System => self.system_theme.unwrap_or(self.fallback_theme),
}
}

pub(crate) fn style(&self) -> &std::sync::Arc<Style> {
match self.theme() {
Theme::Dark => &self.dark_style,
Theme::Light => &self.light_style,
}
}

pub(crate) fn style_mut(&mut self) -> &mut std::sync::Arc<Style> {
match self.theme() {
Theme::Dark => &mut self.dark_style,
Theme::Light => &mut self.light_style,
}
}
}
Expand All @@ -328,8 +343,9 @@ impl Options {
/// Show the options in the ui.
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
style, // covered above
follow_system_theme: _,
dark_style, // covered above
light_style,
theme_preference,
fallback_theme: _,
system_theme: _,
zoom_factor: _, // TODO(emilk)
Expand Down Expand Up @@ -370,7 +386,14 @@ impl Options {
CollapsingHeader::new("🎑 Style")
.default_open(true)
.show(ui, |ui| {
std::sync::Arc::make_mut(style).ui(ui);
theme_preference.radio_buttons(ui);

CollapsingHeader::new("Dark")
.default_open(true)
.show(ui, |ui| std::sync::Arc::make_mut(dark_style).ui(ui));
CollapsingHeader::new("Light")
.default_open(true)
.show(ui, |ui| std::sync::Arc::make_mut(light_style).ui(ui));
});

CollapsingHeader::new("✒ Painting")
Expand Down
Loading

0 comments on commit bbecc35

Please sign in to comment.