From 27d5baef5d3a6efadbd5b3d1307670c365c9180f Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 11 Nov 2023 13:41:34 -0800 Subject: [PATCH] ThemeMode --- Cargo.lock | 2 +- examples/theme.rs | 58 ++++++++++++++++------- src/animation.rs | 20 ++++++++ src/context.rs | 74 ++++++++++++++++++++--------- src/tree.rs | 80 ++++++++++++++++++++++++-------- src/widget.rs | 20 ++++++-- src/widgets.rs | 4 ++ src/widgets/mode_switch.rs | 31 +++++++++++++ src/widgets/style.rs | 7 +-- src/widgets/themed.rs | 31 +++++++++++++ src/window.rs | 95 ++++++++++++++++++++++++++++++++++++-- 11 files changed, 354 insertions(+), 68 deletions(-) create mode 100644 src/widgets/mode_switch.rs create mode 100644 src/widgets/themed.rs diff --git a/Cargo.lock b/Cargo.lock index b7855da..7fa337a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -481,7 +481,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "figures" version = "0.1.0" -source = "git+https://github.com/khonsulabs/figures#3e12470aa10d36d0b38f0441a0e89ae03ccd448b" +source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4" dependencies = [ "bytemuck", "euclid", diff --git a/examples/theme.rs b/examples/theme.rs index bf85b7b..ee605c9 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -3,7 +3,8 @@ use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair}; use gooey::value::{Dynamic, MapEach}; use gooey::widget::MakeWidget; -use gooey::widgets::{Label, Scroll, Slider, Stack}; +use gooey::widgets::{Label, Scroll, Slider, Stack, Themed}; +use gooey::window::ThemeMode; use gooey::Run; use kludgine::Color; @@ -20,6 +21,7 @@ fn main() -> gooey::Result { let (neutral, neutral_editor) = color_editor(PRIMARY_HUE, 0.001, "Neutral"); let (neutral_variant, neutral_variant_editor) = color_editor(PRIMARY_HUE, 0.001, "Neutral Variant"); + let (theme_mode, theme_switcher) = dark_mode_slider(); let default_theme = ( &primary, @@ -42,27 +44,49 @@ fn main() -> gooey::Result { }, ); - Stack::columns( - Scroll::vertical(Stack::rows( - primary_editor - .and(secondary_editor) - .and(tertiary_editor) - .and(error_editor) - .and(neutral_editor) - .and(neutral_variant_editor), - )) - .and(theme(default_theme.map_each(|theme| theme.dark), "Dark")) - .and(theme(default_theme.map_each(|theme| theme.light), "Light")) - .and(fixed_themes( - default_theme.map_each(|theme| theme.primary_fixed), - default_theme.map_each(|theme| theme.secondary_fixed), - default_theme.map_each(|theme| theme.tertiary_fixed), - )), + Themed::new( + default_theme.clone(), + Stack::columns( + Scroll::vertical(Stack::rows( + theme_switcher + .and(primary_editor) + .and(secondary_editor) + .and(tertiary_editor) + .and(error_editor) + .and(neutral_editor) + .and(neutral_variant_editor), + )) + .and(theme(default_theme.map_each(|theme| theme.dark), "Dark")) + .and(theme(default_theme.map_each(|theme| theme.light), "Light")) + .and(fixed_themes( + default_theme.map_each(|theme| theme.primary_fixed), + default_theme.map_each(|theme| theme.secondary_fixed), + default_theme.map_each(|theme| theme.tertiary_fixed), + )), + ), ) .expand() + .into_window() + .with_theme_mode(theme_mode) .run() } +fn dark_mode_slider() -> (Dynamic, impl MakeWidget) { + let on_off = Dynamic::new(true); + let theme_mode = on_off.map_each(|dark| { + if *dark { + ThemeMode::Dark + } else { + ThemeMode::Light + } + }); + + ( + theme_mode, + Stack::rows(Label::new("Theme Mode").and(Slider::::from_value(on_off))), + ) +} + fn color_editor( initial_hue: f32, initial_saturation: impl Into, diff --git a/src/animation.rs b/src/animation.rs index 97234d9..11e7915 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -674,6 +674,26 @@ impl LinearInterpolate for f64 { } } +impl LinearInterpolate for bool { + fn lerp(&self, target: &Self, percent: f32) -> Self { + if percent >= 0.5 { + *target + } else { + *self + } + } +} + +impl PercentBetween for bool { + fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne { + if *min == *max || *self == *min { + ZeroToOne::ZERO + } else { + ZeroToOne::ONE + } + } +} + #[test] fn integer_lerps() { #[track_caller] diff --git a/src/context.rs b/src/context.rs index ca50068..4f53ea8 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,5 @@ //! Types that provide access to the Gooey runtime. +use std::borrow::Cow; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -6,7 +7,6 @@ use std::sync::Arc; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; -use kludgine::app::winit::window; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size}; use kludgine::shapes::{Shape, StrokeOptions}; @@ -15,10 +15,10 @@ use kludgine::{Color, Kludgine}; use crate::graphics::Graphics; use crate::styles::components::{HighlightColor, VisualOrder, WidgetBackground}; use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles, Theme, ThemePair}; -use crate::value::Dynamic; +use crate::value::{Dynamic, Value}; use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef}; use crate::window::sealed::WindowCommand; -use crate::window::RunningWindow; +use crate::window::{RunningWindow, ThemeMode}; use crate::ConstraintLimit; /// A context to an event function. @@ -674,8 +674,9 @@ pub struct WidgetContext<'context, 'window> { current_node: ManagedWidget, redraw_status: &'context RedrawStatus, window: &'context mut RunningWindow<'window>, - theme: &'context ThemePair, + theme: Cow<'context, ThemePair>, pending_state: PendingState<'context>, + theme_mode: ThemeMode, } impl<'context, 'window> WidgetContext<'context, 'window> { @@ -684,6 +685,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { redraw_status: &'context RedrawStatus, theme: &'context ThemePair, window: &'context mut RunningWindow<'window>, + theme_mode: ThemeMode, ) -> Self { Self { pending_state: PendingState::Owned(PendingWidgetState { @@ -698,7 +700,8 @@ impl<'context, 'window> WidgetContext<'context, 'window> { }), current_node, redraw_status, - theme, + theme: Cow::Borrowed(theme), + theme_mode, window, } } @@ -709,8 +712,9 @@ impl<'context, 'window> WidgetContext<'context, 'window> { current_node: self.current_node.clone(), redraw_status: self.redraw_status, window: &mut *self.window, - theme: self.theme, + theme: Cow::Borrowed(self.theme.as_ref()), pending_state: self.pending_state.borrowed(), + theme_mode: self.theme_mode, } } @@ -723,12 +727,26 @@ impl<'context, 'window> WidgetContext<'context, 'window> { Widget: ManageWidget, Widget::Managed: MapManagedWidget>, { - widget.manage(self).map(|current_node| WidgetContext { - current_node, - redraw_status: self.redraw_status, - window: &mut *self.window, - theme: self.theme, - pending_state: self.pending_state.borrowed(), + widget.manage(self).map(|current_node| { + let (theme, theme_mode) = current_node.overidden_theme(); + let theme = if let Some(theme) = theme { + Cow::Owned(theme.get_tracked(self)) + } else { + Cow::Borrowed(self.theme.as_ref()) + }; + let theme_mode = if let Some(mode) = theme_mode { + mode.get_tracked(self) + } else { + self.theme_mode + }; + WidgetContext { + current_node, + redraw_status: self.redraw_status, + window: &mut *self.window, + theme, + pending_state: self.pending_state.borrowed(), + theme_mode, + } }) } @@ -872,10 +890,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// /// Style queries for children will return any values matching this /// collection. - pub fn attach_styles(&self, styles: Styles) { + pub fn attach_styles(&self, styles: Value) { self.current_node.attach_styles(styles); } + /// Attaches `theme` to the widget hierarchy for this widget. + /// + /// All children nodes will access this theme in their contexts. + pub fn attach_theme(&self, theme: Value) { + self.current_node.attach_theme(theme); + } + + /// Attaches `theme_mode` to the widget hierarchy for this widget. + /// + /// All children nodes will use this theme mode. + pub fn attach_theme_mode(&self, theme_mode: Value) { + self.current_node.attach_theme_mode(theme_mode); + } + /// Queries the widget hierarchy for matching style components. /// /// This function traverses up the widget hierarchy looking for the @@ -890,7 +922,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles { self.current_node .tree - .query_styles(&self.current_node, query) + .query_styles(&self.current_node, query, self) } /// Queries the widget hierarchy for a single style component. @@ -931,24 +963,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// Returns the theme pair for the window. #[must_use] pub fn theme_pair(&self) -> &ThemePair { - self.theme + self.theme.as_ref() } /// Returns the current theme in either light or dark mode. #[must_use] pub fn theme(&self) -> &Theme { - match self.window.theme() { - window::Theme::Light => &self.theme.light, - window::Theme::Dark => &self.theme.dark, + match self.theme_mode { + ThemeMode::Light => &self.theme.light, + ThemeMode::Dark => &self.theme.dark, } } /// Returns the opposite theme of [`Self::theme()`]. #[must_use] pub fn inverse_theme(&self) -> &Theme { - match self.window.theme() { - window::Theme::Light => &self.theme.dark, - window::Theme::Dark => &self.theme.light, + match self.theme_mode { + ThemeMode::Light => &self.theme.dark, + ThemeMode::Dark => &self.theme.light, } } } diff --git a/src/tree.rs b/src/tree.rs index a7e8d22..aca381c 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -7,8 +7,10 @@ use kludgine::figures::{Point, Rect}; use crate::context::WidgetContext; use crate::styles::components::VisualOrder; -use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles}; +use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles, ThemePair}; +use crate::value::Value; use crate::widget::{ManagedWidget, WidgetId, WidgetInstance}; +use crate::window::ThemeMode; #[derive(Clone, Default)] pub struct Tree { @@ -31,6 +33,8 @@ impl Tree { parent: parent.map(ManagedWidget::id), layout: None, styles: None, + theme: None, + theme_mode: None, }, ); if widget.is_default() { @@ -277,20 +281,47 @@ impl Tree { data.nodes.get(&id).expect("missing widget").parent } - pub(crate) fn attach_styles(&self, id: WidgetId, styles: Styles) { + pub(crate) fn attach_styles(&self, id: WidgetId, styles: Value) { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); data.nodes.get_mut(&id).expect("missing widget").styles = Some(styles); } + pub(crate) fn attach_theme(&self, id: WidgetId, theme: Value) { + let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + data.nodes.get_mut(&id).expect("missing widget").theme = Some(theme); + } + + pub(crate) fn attach_theme_mode(&self, id: WidgetId, theme: Value) { + let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + data.nodes.get_mut(&id).expect("missing widget").theme_mode = Some(theme); + } + + pub(crate) fn overriden_theme( + &self, + id: WidgetId, + ) -> (Option>, Option>) { + let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + + ( + data.nodes.get(&id).expect("missing widget").theme.clone(), + data.nodes + .get(&id) + .expect("missing widget") + .theme_mode + .clone(), + ) + } + pub fn query_styles( &self, perspective: &ManagedWidget, query: &[&dyn ComponentDefaultvalue], + context: &WidgetContext<'_, '_>, ) -> Styles { self.data .lock() .map_or_else(PoisonError::into_inner, |g| g) - .query_styles(perspective.id(), query) + .query_styles(perspective.id(), query, context) } pub fn query_style( @@ -391,19 +422,22 @@ impl TreeData { &self, mut perspective: WidgetId, query: &[&dyn ComponentDefaultvalue], + context: &WidgetContext<'_, '_>, ) -> Styles { let mut query = query.iter().map(|n| n.name()).collect::>(); let mut resolved = Styles::new(); while !query.is_empty() { let node = &self.nodes[&perspective]; if let Some(styles) = &node.styles { - query.retain(|name| { - if let Some(component) = styles.get_named(name) { - resolved.insert(name, component.clone()); - false - } else { - true - } + styles.map_tracked(context, |styles| { + query.retain(|name| { + if let Some(component) = styles.get_named(name) { + resolved.insert(name, component.clone()); + false + } else { + true + } + }); }); } let Some(parent) = node.parent else { break }; @@ -422,13 +456,21 @@ impl TreeData { loop { let node = &self.nodes[&perspective]; if let Some(styles) = &node.styles { - if let Some(component) = styles.get_named(&name) { - let Ok(value) = ::try_from_component(component.get()) - else { - break; - }; - component.redraw_when_changed(context); - return value; + match styles.map_tracked(context, |styles| { + if let Some(component) = styles.get_named(&name) { + let Ok(value) = + ::try_from_component(component.get()) + else { + return Err(()); + }; + component.redraw_when_changed(context); + return Ok(Some(value)); + } + Ok(None) + }) { + Ok(Some(value)) => return value, + Ok(None) => {} + Err(()) => break, } } let Some(parent) = node.parent else { break }; @@ -443,5 +485,7 @@ pub struct Node { pub children: Vec, pub parent: Option, pub layout: Option>, - pub styles: Option, + pub styles: Option>, + pub theme: Option>, + pub theme_mode: Option>, } diff --git a/src/widget.rs b/src/widget.rs index 0ec309c..90c9668 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -16,11 +16,11 @@ use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::components::VisualOrder; -use crate::styles::{IntoComponentValue, NamedComponent, Styles}; +use crate::styles::{IntoComponentValue, NamedComponent, Styles, ThemePair}; use crate::tree::Tree; use crate::value::{IntoValue, Value}; use crate::widgets::{Align, Expand, Scroll, Style}; -use crate::window::{RunningWindow, Window, WindowBehavior}; +use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior}; use crate::{ConstraintLimit, Run}; /// A type that makes up a graphical user interface. @@ -448,7 +448,7 @@ pub trait MakeWidget: Sized { /// Associates `styles` with this widget. /// /// This is equivalent to `Style::new(styles, self)`. - fn with_styles(self, styles: impl Into) -> Style + fn with_styles(self, styles: impl IntoValue) -> Style where Self: Sized, { @@ -951,10 +951,22 @@ impl ManagedWidget { self.tree.parent(self.id()).is_some() } - pub(crate) fn attach_styles(&self, styles: Styles) { + pub(crate) fn attach_styles(&self, styles: Value) { self.tree.attach_styles(self.id(), styles); } + pub(crate) fn attach_theme(&self, theme: Value) { + self.tree.attach_theme(self.id(), theme); + } + + pub(crate) fn attach_theme_mode(&self, theme: Value) { + self.tree.attach_theme_mode(self.id(), theme); + } + + pub(crate) fn overidden_theme(&self) -> (Option>, Option>) { + self.tree.overriden_theme(self.id()) + } + pub(crate) fn reset_child_layouts(&self) { self.tree.reset_child_layouts(self.id()); } diff --git a/src/widgets.rs b/src/widgets.rs index 4047986..e1a0adf 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -6,12 +6,14 @@ mod canvas; mod expand; mod input; pub mod label; +mod mode_switch; mod resize; pub mod scroll; mod slider; mod space; pub mod stack; mod style; +mod themed; mod tilemap; pub use align::Align; @@ -20,10 +22,12 @@ pub use canvas::Canvas; pub use expand::Expand; pub use input::Input; pub use label::Label; +pub use mode_switch::ModeSwitch; pub use resize::Resize; pub use scroll::Scroll; pub use slider::Slider; pub use space::Space; pub use stack::Stack; pub use style::Style; +pub use themed::Themed; pub use tilemap::TileMap; diff --git a/src/widgets/mode_switch.rs b/src/widgets/mode_switch.rs new file mode 100644 index 0000000..a4bd1be --- /dev/null +++ b/src/widgets/mode_switch.rs @@ -0,0 +1,31 @@ +use crate::context::EventContext; +use crate::value::{IntoValue, Value}; +use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; +use crate::window::ThemeMode; + +/// A widget that applies a set of [`Styles`] to all contained widgets. +#[derive(Debug)] +pub struct ModeSwitch { + mode: Value, + child: WidgetRef, +} + +impl ModeSwitch { + /// Returns a new widget that applies `mode` to all of its children. + pub fn new(mode: impl IntoValue, child: impl MakeWidget) -> Self { + Self { + mode: mode.into_value(), + child: WidgetRef::new(child), + } + } +} + +impl WrapperWidget for ModeSwitch { + fn child_mut(&mut self) -> &mut WidgetRef { + &mut self.child + } + + fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + context.attach_theme_mode(self.mode.clone()); + } +} diff --git a/src/widgets/style.rs b/src/widgets/style.rs index 870e7c4..d1c714a 100644 --- a/src/widgets/style.rs +++ b/src/widgets/style.rs @@ -1,20 +1,21 @@ use crate::context::EventContext; use crate::styles::Styles; +use crate::value::{IntoValue, Value}; use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; /// A widget that applies a set of [`Styles`] to all contained widgets. #[derive(Debug)] pub struct Style { - styles: Styles, + styles: Value, child: WidgetRef, } impl Style { /// Returns a new widget that applies `styles` to `child` and any children /// it may have. - pub fn new(styles: impl Into, child: impl MakeWidget) -> Self { + pub fn new(styles: impl IntoValue, child: impl MakeWidget) -> Self { Self { - styles: styles.into(), + styles: styles.into_value(), child: WidgetRef::new(child), } } diff --git a/src/widgets/themed.rs b/src/widgets/themed.rs new file mode 100644 index 0000000..4a60442 --- /dev/null +++ b/src/widgets/themed.rs @@ -0,0 +1,31 @@ +use crate::context::EventContext; +use crate::styles::ThemePair; +use crate::value::{IntoValue, Value}; +use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; + +/// A widget that applies a set of [`Styles`] to all contained widgets. +#[derive(Debug)] +pub struct Themed { + theme: Value, + child: WidgetRef, +} + +impl Themed { + /// Returns a new widget that applies `theme` to all of its children. + pub fn new(theme: impl IntoValue, child: impl MakeWidget) -> Self { + Self { + theme: theme.into_value(), + child: WidgetRef::new(child), + } + } +} + +impl WrapperWidget for Themed { + fn child_mut(&mut self) -> &mut WidgetRef { + &mut self.child + } + + fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + context.attach_theme(self.theme.clone()); + } +} diff --git a/src/window.rs b/src/window.rs index 4128090..615a56f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -15,6 +15,7 @@ use kludgine::app::winit::event::{ DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::app::winit::keyboard::Key; +use kludgine::app::winit::window; use kludgine::app::WindowBehavior as _; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; @@ -32,7 +33,7 @@ use crate::styles::components::LayoutOrder; use crate::styles::ThemePair; use crate::tree::Tree; use crate::utils::ModifiersExt; -use crate::value::{Dynamic, DynamicReader, IntoDynamic, Value}; +use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; use crate::widget::{ EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, }; @@ -105,6 +106,7 @@ where pub theme: Value, occluded: Option>, focused: Option>, + theme_mode: Option>, } impl Default for Window @@ -156,6 +158,24 @@ impl Window { self.occluded = Some(occluded); self } + + /// Sets the [`ThemeMode`] for this window. + /// + /// If a [`ThemeMode`] is provided, the window will be set to this theme + /// mode upon creation and will not be updated while the window is running. + /// + /// If a [`Dynamic`] is provided, the initial value will be ignored and the + /// dynamic will be updated when the window opens with the user's current + /// theme mode. The dynamic will also be updated any time the user's theme + /// mode changes. + /// + /// Setting the [`Dynamic`]'s value will also update the window with the new + /// mode until a mode change is detected, upon which the new mode will be + /// stored. + pub fn with_theme_mode(mut self, theme_mode: impl IntoValue) -> Self { + self.theme_mode = Some(theme_mode.into_value()); + self + } } impl Window @@ -189,6 +209,7 @@ where theme: Value::default(), occluded: None, focused: None, + theme_mode: None, } } } @@ -207,6 +228,7 @@ where occluded: self.occluded, focused: self.focused, theme: Some(self.theme), + theme_mode: self.theme_mode, }), })) } @@ -260,6 +282,7 @@ struct GooeyWindow { max_inner_size: Option>, theme: Option>, current_theme: ThemePair, + theme_mode: Dynamic, transparent: bool, } @@ -289,6 +312,7 @@ where &self.redraw_status, &self.current_theme, window, + self.theme_mode.get(), ), kludgine, ) @@ -300,6 +324,7 @@ where &self.redraw_status, &self.current_theme, window, + self.theme_mode.get(), ), kludgine, ) @@ -313,6 +338,7 @@ where &self.redraw_status, &self.current_theme, window, + self.theme_mode.get(), ), kludgine, ) @@ -408,6 +434,14 @@ where .theme .take() .expect("theme always present"); + + let theme_mode = match context.settings.borrow_mut().theme_mode.take() { + Some(Value::Dynamic(dynamic)) => { + dynamic.update(window.theme().into()); + dynamic + } + Some(Value::Constant(_)) | None => Dynamic::new(window.theme().into()), + }; let transparent = context.settings.borrow().transparent; let mut behavior = T::initialize( &mut RunningWindow::new(window, &focused, &occluded), @@ -439,6 +473,7 @@ where max_inner_size: None, current_theme, theme, + theme_mode, transparent, } } @@ -472,9 +507,11 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), gfx: Exclusive::Owned(Graphics::new(graphics)), }; + context.redraw_when_changed(&self.theme_mode); let mut layout_context = LayoutContext::new(&mut context); let window_size = layout_context.gfx.size(); @@ -557,12 +594,16 @@ where fn initial_window_attributes( context: &Self::Context, ) -> kludgine::app::WindowAttributes { - context + let mut attrs = context .settings .borrow_mut() .attributes .take() - .expect("called more than once") + .expect("called more than once"); + if let Some(Value::Constant(theme_mode)) = &context.settings.borrow().theme_mode { + attrs.preferred_theme = Some((*theme_mode).into()); + } + attrs } fn close_requested( @@ -636,6 +677,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -665,6 +707,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -731,6 +774,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -765,6 +809,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -793,6 +838,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -807,6 +853,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -847,6 +894,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -871,6 +919,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ) @@ -886,6 +935,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ), @@ -920,6 +970,7 @@ where &self.redraw_status, &self.current_theme, &mut window, + self.theme_mode.get(), ), kludgine, ); @@ -937,6 +988,14 @@ where } } + fn theme_changed( + &mut self, + window: kludgine::app::Window<'_, WindowCommand>, + _kludgine: &mut Kludgine, + ) { + self.theme_mode.update(window.theme().into()); + } + fn event( &mut self, mut window: kludgine::app::Window<'_, WindowCommand>, @@ -975,7 +1034,7 @@ pub(crate) mod sealed { use crate::styles::ThemePair; use crate::value::{Dynamic, Value}; - use crate::window::WindowAttributes; + use crate::window::{ThemeMode, WindowAttributes}; pub struct Context { pub user: C, @@ -987,6 +1046,7 @@ pub(crate) mod sealed { pub occluded: Option>, pub focused: Option>, pub theme: Option>, + pub theme_mode: Option>, pub transparent: bool, } @@ -995,3 +1055,30 @@ pub(crate) mod sealed { // RequestClose, } } + +/// Controls whether the light or dark theme is applied. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum ThemeMode { + /// Applies the light theme + Light, + /// Applies the dark theme + Dark, +} + +impl From for ThemeMode { + fn from(value: window::Theme) -> Self { + match value { + window::Theme::Light => Self::Light, + window::Theme::Dark => Self::Dark, + } + } +} + +impl From for window::Theme { + fn from(value: ThemeMode) -> Self { + match value { + ThemeMode::Light => Self::Light, + ThemeMode::Dark => Self::Dark, + } + } +}