ThemeMode

This commit is contained in:
Jonathan Johnson 2023-11-11 13:41:34 -08:00
parent 68339dfb62
commit 27d5baef5d
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
11 changed files with 354 additions and 68 deletions

2
Cargo.lock generated
View file

@ -481,7 +481,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]] [[package]]
name = "figures" name = "figures"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/khonsulabs/figures#3e12470aa10d36d0b38f0441a0e89ae03ccd448b" source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"euclid", "euclid",

View file

@ -3,7 +3,8 @@ use gooey::styles::components::{TextColor, WidgetBackground};
use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair}; use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair};
use gooey::value::{Dynamic, MapEach}; use gooey::value::{Dynamic, MapEach};
use gooey::widget::MakeWidget; 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 gooey::Run;
use kludgine::Color; use kludgine::Color;
@ -20,6 +21,7 @@ fn main() -> gooey::Result {
let (neutral, neutral_editor) = color_editor(PRIMARY_HUE, 0.001, "Neutral"); let (neutral, neutral_editor) = color_editor(PRIMARY_HUE, 0.001, "Neutral");
let (neutral_variant, neutral_variant_editor) = let (neutral_variant, neutral_variant_editor) =
color_editor(PRIMARY_HUE, 0.001, "Neutral Variant"); color_editor(PRIMARY_HUE, 0.001, "Neutral Variant");
let (theme_mode, theme_switcher) = dark_mode_slider();
let default_theme = ( let default_theme = (
&primary, &primary,
@ -42,27 +44,49 @@ fn main() -> gooey::Result {
}, },
); );
Stack::columns( Themed::new(
Scroll::vertical(Stack::rows( default_theme.clone(),
primary_editor Stack::columns(
.and(secondary_editor) Scroll::vertical(Stack::rows(
.and(tertiary_editor) theme_switcher
.and(error_editor) .and(primary_editor)
.and(neutral_editor) .and(secondary_editor)
.and(neutral_variant_editor), .and(tertiary_editor)
)) .and(error_editor)
.and(theme(default_theme.map_each(|theme| theme.dark), "Dark")) .and(neutral_editor)
.and(theme(default_theme.map_each(|theme| theme.light), "Light")) .and(neutral_variant_editor),
.and(fixed_themes( ))
default_theme.map_each(|theme| theme.primary_fixed), .and(theme(default_theme.map_each(|theme| theme.dark), "Dark"))
default_theme.map_each(|theme| theme.secondary_fixed), .and(theme(default_theme.map_each(|theme| theme.light), "Light"))
default_theme.map_each(|theme| theme.tertiary_fixed), .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() .expand()
.into_window()
.with_theme_mode(theme_mode)
.run() .run()
} }
fn dark_mode_slider() -> (Dynamic<ThemeMode>, 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::<bool>::from_value(on_off))),
)
}
fn color_editor( fn color_editor(
initial_hue: f32, initial_hue: f32,
initial_saturation: impl Into<ZeroToOne>, initial_saturation: impl Into<ZeroToOne>,

View file

@ -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] #[test]
fn integer_lerps() { fn integer_lerps() {
#[track_caller] #[track_caller]

View file

@ -1,4 +1,5 @@
//! Types that provide access to the Gooey runtime. //! Types that provide access to the Gooey runtime.
use std::borrow::Cow;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
@ -6,7 +7,6 @@ use std::sync::Arc;
use kludgine::app::winit::event::{ use kludgine::app::winit::event::{
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
}; };
use kludgine::app::winit::window;
use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size}; use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size};
use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::shapes::{Shape, StrokeOptions};
@ -15,10 +15,10 @@ use kludgine::{Color, Kludgine};
use crate::graphics::Graphics; use crate::graphics::Graphics;
use crate::styles::components::{HighlightColor, VisualOrder, WidgetBackground}; use crate::styles::components::{HighlightColor, VisualOrder, WidgetBackground};
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles, Theme, ThemePair}; 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::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef};
use crate::window::sealed::WindowCommand; use crate::window::sealed::WindowCommand;
use crate::window::RunningWindow; use crate::window::{RunningWindow, ThemeMode};
use crate::ConstraintLimit; use crate::ConstraintLimit;
/// A context to an event function. /// A context to an event function.
@ -674,8 +674,9 @@ pub struct WidgetContext<'context, 'window> {
current_node: ManagedWidget, current_node: ManagedWidget,
redraw_status: &'context RedrawStatus, redraw_status: &'context RedrawStatus,
window: &'context mut RunningWindow<'window>, window: &'context mut RunningWindow<'window>,
theme: &'context ThemePair, theme: Cow<'context, ThemePair>,
pending_state: PendingState<'context>, pending_state: PendingState<'context>,
theme_mode: ThemeMode,
} }
impl<'context, 'window> WidgetContext<'context, 'window> { impl<'context, 'window> WidgetContext<'context, 'window> {
@ -684,6 +685,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
redraw_status: &'context RedrawStatus, redraw_status: &'context RedrawStatus,
theme: &'context ThemePair, theme: &'context ThemePair,
window: &'context mut RunningWindow<'window>, window: &'context mut RunningWindow<'window>,
theme_mode: ThemeMode,
) -> Self { ) -> Self {
Self { Self {
pending_state: PendingState::Owned(PendingWidgetState { pending_state: PendingState::Owned(PendingWidgetState {
@ -698,7 +700,8 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
}), }),
current_node, current_node,
redraw_status, redraw_status,
theme, theme: Cow::Borrowed(theme),
theme_mode,
window, window,
} }
} }
@ -709,8 +712,9 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
current_node: self.current_node.clone(), current_node: self.current_node.clone(),
redraw_status: self.redraw_status, redraw_status: self.redraw_status,
window: &mut *self.window, window: &mut *self.window,
theme: self.theme, theme: Cow::Borrowed(self.theme.as_ref()),
pending_state: self.pending_state.borrowed(), pending_state: self.pending_state.borrowed(),
theme_mode: self.theme_mode,
} }
} }
@ -723,12 +727,26 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
Widget: ManageWidget, Widget: ManageWidget,
Widget::Managed: MapManagedWidget<WidgetContext<'child, 'window>>, Widget::Managed: MapManagedWidget<WidgetContext<'child, 'window>>,
{ {
widget.manage(self).map(|current_node| WidgetContext { widget.manage(self).map(|current_node| {
current_node, let (theme, theme_mode) = current_node.overidden_theme();
redraw_status: self.redraw_status, let theme = if let Some(theme) = theme {
window: &mut *self.window, Cow::Owned(theme.get_tracked(self))
theme: self.theme, } else {
pending_state: self.pending_state.borrowed(), 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 /// Style queries for children will return any values matching this
/// collection. /// collection.
pub fn attach_styles(&self, styles: Styles) { pub fn attach_styles(&self, styles: Value<Styles>) {
self.current_node.attach_styles(styles); 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<ThemePair>) {
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<ThemeMode>) {
self.current_node.attach_theme_mode(theme_mode);
}
/// Queries the widget hierarchy for matching style components. /// Queries the widget hierarchy for matching style components.
/// ///
/// This function traverses up the widget hierarchy looking for the /// 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 { pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles {
self.current_node self.current_node
.tree .tree
.query_styles(&self.current_node, query) .query_styles(&self.current_node, query, self)
} }
/// Queries the widget hierarchy for a single style component. /// 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. /// Returns the theme pair for the window.
#[must_use] #[must_use]
pub fn theme_pair(&self) -> &ThemePair { pub fn theme_pair(&self) -> &ThemePair {
self.theme self.theme.as_ref()
} }
/// Returns the current theme in either light or dark mode. /// Returns the current theme in either light or dark mode.
#[must_use] #[must_use]
pub fn theme(&self) -> &Theme { pub fn theme(&self) -> &Theme {
match self.window.theme() { match self.theme_mode {
window::Theme::Light => &self.theme.light, ThemeMode::Light => &self.theme.light,
window::Theme::Dark => &self.theme.dark, ThemeMode::Dark => &self.theme.dark,
} }
} }
/// Returns the opposite theme of [`Self::theme()`]. /// Returns the opposite theme of [`Self::theme()`].
#[must_use] #[must_use]
pub fn inverse_theme(&self) -> &Theme { pub fn inverse_theme(&self) -> &Theme {
match self.window.theme() { match self.theme_mode {
window::Theme::Light => &self.theme.dark, ThemeMode::Light => &self.theme.dark,
window::Theme::Dark => &self.theme.light, ThemeMode::Dark => &self.theme.light,
} }
} }
} }

View file

@ -7,8 +7,10 @@ use kludgine::figures::{Point, Rect};
use crate::context::WidgetContext; use crate::context::WidgetContext;
use crate::styles::components::VisualOrder; 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::widget::{ManagedWidget, WidgetId, WidgetInstance};
use crate::window::ThemeMode;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Tree { pub struct Tree {
@ -31,6 +33,8 @@ impl Tree {
parent: parent.map(ManagedWidget::id), parent: parent.map(ManagedWidget::id),
layout: None, layout: None,
styles: None, styles: None,
theme: None,
theme_mode: None,
}, },
); );
if widget.is_default() { if widget.is_default() {
@ -277,20 +281,47 @@ impl Tree {
data.nodes.get(&id).expect("missing widget").parent 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<Styles>) {
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); 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); data.nodes.get_mut(&id).expect("missing widget").styles = Some(styles);
} }
pub(crate) fn attach_theme(&self, id: WidgetId, theme: Value<ThemePair>) {
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<ThemeMode>) {
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<Value<ThemePair>>, Option<Value<ThemeMode>>) {
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( pub fn query_styles(
&self, &self,
perspective: &ManagedWidget, perspective: &ManagedWidget,
query: &[&dyn ComponentDefaultvalue], query: &[&dyn ComponentDefaultvalue],
context: &WidgetContext<'_, '_>,
) -> Styles { ) -> Styles {
self.data self.data
.lock() .lock()
.map_or_else(PoisonError::into_inner, |g| g) .map_or_else(PoisonError::into_inner, |g| g)
.query_styles(perspective.id(), query) .query_styles(perspective.id(), query, context)
} }
pub fn query_style<Component: ComponentDefinition>( pub fn query_style<Component: ComponentDefinition>(
@ -391,19 +422,22 @@ impl TreeData {
&self, &self,
mut perspective: WidgetId, mut perspective: WidgetId,
query: &[&dyn ComponentDefaultvalue], query: &[&dyn ComponentDefaultvalue],
context: &WidgetContext<'_, '_>,
) -> Styles { ) -> Styles {
let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>(); let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>();
let mut resolved = Styles::new(); let mut resolved = Styles::new();
while !query.is_empty() { while !query.is_empty() {
let node = &self.nodes[&perspective]; let node = &self.nodes[&perspective];
if let Some(styles) = &node.styles { if let Some(styles) = &node.styles {
query.retain(|name| { styles.map_tracked(context, |styles| {
if let Some(component) = styles.get_named(name) { query.retain(|name| {
resolved.insert(name, component.clone()); if let Some(component) = styles.get_named(name) {
false resolved.insert(name, component.clone());
} else { false
true } else {
} true
}
});
}); });
} }
let Some(parent) = node.parent else { break }; let Some(parent) = node.parent else { break };
@ -422,13 +456,21 @@ impl TreeData {
loop { loop {
let node = &self.nodes[&perspective]; let node = &self.nodes[&perspective];
if let Some(styles) = &node.styles { if let Some(styles) = &node.styles {
if let Some(component) = styles.get_named(&name) { match styles.map_tracked(context, |styles| {
let Ok(value) = <Component::ComponentType>::try_from_component(component.get()) if let Some(component) = styles.get_named(&name) {
else { let Ok(value) =
break; <Component::ComponentType>::try_from_component(component.get())
}; else {
component.redraw_when_changed(context); return Err(());
return value; };
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 }; let Some(parent) = node.parent else { break };
@ -443,5 +485,7 @@ pub struct Node {
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
pub layout: Option<Rect<Px>>, pub layout: Option<Rect<Px>>,
pub styles: Option<Styles>, pub styles: Option<Value<Styles>>,
pub theme: Option<Value<ThemePair>>,
pub theme_mode: Option<Value<ThemeMode>>,
} }

View file

@ -16,11 +16,11 @@ use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
use crate::styles::components::VisualOrder; use crate::styles::components::VisualOrder;
use crate::styles::{IntoComponentValue, NamedComponent, Styles}; use crate::styles::{IntoComponentValue, NamedComponent, Styles, ThemePair};
use crate::tree::Tree; use crate::tree::Tree;
use crate::value::{IntoValue, Value}; use crate::value::{IntoValue, Value};
use crate::widgets::{Align, Expand, Scroll, Style}; use crate::widgets::{Align, Expand, Scroll, Style};
use crate::window::{RunningWindow, Window, WindowBehavior}; use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
use crate::{ConstraintLimit, Run}; use crate::{ConstraintLimit, Run};
/// A type that makes up a graphical user interface. /// A type that makes up a graphical user interface.
@ -448,7 +448,7 @@ pub trait MakeWidget: Sized {
/// Associates `styles` with this widget. /// Associates `styles` with this widget.
/// ///
/// This is equivalent to `Style::new(styles, self)`. /// This is equivalent to `Style::new(styles, self)`.
fn with_styles(self, styles: impl Into<Styles>) -> Style fn with_styles(self, styles: impl IntoValue<Styles>) -> Style
where where
Self: Sized, Self: Sized,
{ {
@ -951,10 +951,22 @@ impl ManagedWidget {
self.tree.parent(self.id()).is_some() self.tree.parent(self.id()).is_some()
} }
pub(crate) fn attach_styles(&self, styles: Styles) { pub(crate) fn attach_styles(&self, styles: Value<Styles>) {
self.tree.attach_styles(self.id(), styles); self.tree.attach_styles(self.id(), styles);
} }
pub(crate) fn attach_theme(&self, theme: Value<ThemePair>) {
self.tree.attach_theme(self.id(), theme);
}
pub(crate) fn attach_theme_mode(&self, theme: Value<ThemeMode>) {
self.tree.attach_theme_mode(self.id(), theme);
}
pub(crate) fn overidden_theme(&self) -> (Option<Value<ThemePair>>, Option<Value<ThemeMode>>) {
self.tree.overriden_theme(self.id())
}
pub(crate) fn reset_child_layouts(&self) { pub(crate) fn reset_child_layouts(&self) {
self.tree.reset_child_layouts(self.id()); self.tree.reset_child_layouts(self.id());
} }

View file

@ -6,12 +6,14 @@ mod canvas;
mod expand; mod expand;
mod input; mod input;
pub mod label; pub mod label;
mod mode_switch;
mod resize; mod resize;
pub mod scroll; pub mod scroll;
mod slider; mod slider;
mod space; mod space;
pub mod stack; pub mod stack;
mod style; mod style;
mod themed;
mod tilemap; mod tilemap;
pub use align::Align; pub use align::Align;
@ -20,10 +22,12 @@ pub use canvas::Canvas;
pub use expand::Expand; pub use expand::Expand;
pub use input::Input; pub use input::Input;
pub use label::Label; pub use label::Label;
pub use mode_switch::ModeSwitch;
pub use resize::Resize; pub use resize::Resize;
pub use scroll::Scroll; pub use scroll::Scroll;
pub use slider::Slider; pub use slider::Slider;
pub use space::Space; pub use space::Space;
pub use stack::Stack; pub use stack::Stack;
pub use style::Style; pub use style::Style;
pub use themed::Themed;
pub use tilemap::TileMap; pub use tilemap::TileMap;

View file

@ -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<ThemeMode>,
child: WidgetRef,
}
impl ModeSwitch {
/// Returns a new widget that applies `mode` to all of its children.
pub fn new(mode: impl IntoValue<ThemeMode>, 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());
}
}

View file

@ -1,20 +1,21 @@
use crate::context::EventContext; use crate::context::EventContext;
use crate::styles::Styles; use crate::styles::Styles;
use crate::value::{IntoValue, Value};
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
/// A widget that applies a set of [`Styles`] to all contained widgets. /// A widget that applies a set of [`Styles`] to all contained widgets.
#[derive(Debug)] #[derive(Debug)]
pub struct Style { pub struct Style {
styles: Styles, styles: Value<Styles>,
child: WidgetRef, child: WidgetRef,
} }
impl Style { impl Style {
/// Returns a new widget that applies `styles` to `child` and any children /// Returns a new widget that applies `styles` to `child` and any children
/// it may have. /// it may have.
pub fn new(styles: impl Into<Styles>, child: impl MakeWidget) -> Self { pub fn new(styles: impl IntoValue<Styles>, child: impl MakeWidget) -> Self {
Self { Self {
styles: styles.into(), styles: styles.into_value(),
child: WidgetRef::new(child), child: WidgetRef::new(child),
} }
} }

31
src/widgets/themed.rs Normal file
View file

@ -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<ThemePair>,
child: WidgetRef,
}
impl Themed {
/// Returns a new widget that applies `theme` to all of its children.
pub fn new(theme: impl IntoValue<ThemePair>, 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());
}
}

View file

@ -15,6 +15,7 @@ use kludgine::app::winit::event::{
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
}; };
use kludgine::app::winit::keyboard::Key; use kludgine::app::winit::keyboard::Key;
use kludgine::app::winit::window;
use kludgine::app::WindowBehavior as _; use kludgine::app::WindowBehavior as _;
use kludgine::figures::units::{Px, UPx}; use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
@ -32,7 +33,7 @@ use crate::styles::components::LayoutOrder;
use crate::styles::ThemePair; use crate::styles::ThemePair;
use crate::tree::Tree; use crate::tree::Tree;
use crate::utils::ModifiersExt; use crate::utils::ModifiersExt;
use crate::value::{Dynamic, DynamicReader, IntoDynamic, Value}; use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
use crate::widget::{ use crate::widget::{
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
}; };
@ -105,6 +106,7 @@ where
pub theme: Value<ThemePair>, pub theme: Value<ThemePair>,
occluded: Option<Dynamic<bool>>, occluded: Option<Dynamic<bool>>,
focused: Option<Dynamic<bool>>, focused: Option<Dynamic<bool>>,
theme_mode: Option<Value<ThemeMode>>,
} }
impl<Behavior> Default for Window<Behavior> impl<Behavior> Default for Window<Behavior>
@ -156,6 +158,24 @@ impl Window<WidgetInstance> {
self.occluded = Some(occluded); self.occluded = Some(occluded);
self 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<ThemeMode>) -> Self {
self.theme_mode = Some(theme_mode.into_value());
self
}
} }
impl<Behavior> Window<Behavior> impl<Behavior> Window<Behavior>
@ -189,6 +209,7 @@ where
theme: Value::default(), theme: Value::default(),
occluded: None, occluded: None,
focused: None, focused: None,
theme_mode: None,
} }
} }
} }
@ -207,6 +228,7 @@ where
occluded: self.occluded, occluded: self.occluded,
focused: self.focused, focused: self.focused,
theme: Some(self.theme), theme: Some(self.theme),
theme_mode: self.theme_mode,
}), }),
})) }))
} }
@ -260,6 +282,7 @@ struct GooeyWindow<T> {
max_inner_size: Option<Size<UPx>>, max_inner_size: Option<Size<UPx>>,
theme: Option<DynamicReader<ThemePair>>, theme: Option<DynamicReader<ThemePair>>,
current_theme: ThemePair, current_theme: ThemePair,
theme_mode: Dynamic<ThemeMode>,
transparent: bool, transparent: bool,
} }
@ -289,6 +312,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
window, window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
) )
@ -300,6 +324,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
window, window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
) )
@ -313,6 +338,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
window, window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
) )
@ -408,6 +434,14 @@ where
.theme .theme
.take() .take()
.expect("theme always present"); .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 transparent = context.settings.borrow().transparent;
let mut behavior = T::initialize( let mut behavior = T::initialize(
&mut RunningWindow::new(window, &focused, &occluded), &mut RunningWindow::new(window, &focused, &occluded),
@ -439,6 +473,7 @@ where
max_inner_size: None, max_inner_size: None,
current_theme, current_theme,
theme, theme,
theme_mode,
transparent, transparent,
} }
} }
@ -472,9 +507,11 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
gfx: Exclusive::Owned(Graphics::new(graphics)), gfx: Exclusive::Owned(Graphics::new(graphics)),
}; };
context.redraw_when_changed(&self.theme_mode);
let mut layout_context = LayoutContext::new(&mut context); let mut layout_context = LayoutContext::new(&mut context);
let window_size = layout_context.gfx.size(); let window_size = layout_context.gfx.size();
@ -557,12 +594,16 @@ where
fn initial_window_attributes( fn initial_window_attributes(
context: &Self::Context, context: &Self::Context,
) -> kludgine::app::WindowAttributes<WindowCommand> { ) -> kludgine::app::WindowAttributes<WindowCommand> {
context let mut attrs = context
.settings .settings
.borrow_mut() .borrow_mut()
.attributes .attributes
.take() .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( fn close_requested(
@ -636,6 +677,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
); );
@ -665,6 +707,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
); );
@ -731,6 +774,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
); );
@ -765,6 +809,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
); );
@ -793,6 +838,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
); );
@ -807,6 +853,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
); );
@ -847,6 +894,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
); );
@ -871,6 +919,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
) )
@ -886,6 +935,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, kludgine,
), ),
@ -920,6 +970,7 @@ where
&self.redraw_status, &self.redraw_status,
&self.current_theme, &self.current_theme,
&mut window, &mut window,
self.theme_mode.get(),
), ),
kludgine, 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( fn event(
&mut self, &mut self,
mut window: kludgine::app::Window<'_, WindowCommand>, mut window: kludgine::app::Window<'_, WindowCommand>,
@ -975,7 +1034,7 @@ pub(crate) mod sealed {
use crate::styles::ThemePair; use crate::styles::ThemePair;
use crate::value::{Dynamic, Value}; use crate::value::{Dynamic, Value};
use crate::window::WindowAttributes; use crate::window::{ThemeMode, WindowAttributes};
pub struct Context<C> { pub struct Context<C> {
pub user: C, pub user: C,
@ -987,6 +1046,7 @@ pub(crate) mod sealed {
pub occluded: Option<Dynamic<bool>>, pub occluded: Option<Dynamic<bool>>,
pub focused: Option<Dynamic<bool>>, pub focused: Option<Dynamic<bool>>,
pub theme: Option<Value<ThemePair>>, pub theme: Option<Value<ThemePair>>,
pub theme_mode: Option<Value<ThemeMode>>,
pub transparent: bool, pub transparent: bool,
} }
@ -995,3 +1055,30 @@ pub(crate) mod sealed {
// RequestClose, // 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<window::Theme> for ThemeMode {
fn from(value: window::Theme) -> Self {
match value {
window::Theme::Light => Self::Light,
window::Theme::Dark => Self::Dark,
}
}
}
impl From<ThemeMode> for window::Theme {
fn from(value: ThemeMode) -> Self {
match value {
ThemeMode::Light => Self::Light,
ThemeMode::Dark => Self::Dark,
}
}
}