mirror of
https://github.com/danbulant/cushy
synced 2026-06-20 15:01:11 +00:00
ThemeMode
This commit is contained in:
parent
68339dfb62
commit
27d5baef5d
11 changed files with 354 additions and 68 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<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(
|
||||
initial_hue: f32,
|
||||
initial_saturation: impl Into<ZeroToOne>,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<WidgetContext<'child, 'window>>,
|
||||
{
|
||||
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<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.
|
||||
///
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
src/tree.rs
80
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<Styles>) {
|
||||
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<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(
|
||||
&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<Component: ComponentDefinition>(
|
||||
|
|
@ -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::<Vec<_>>();
|
||||
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) = <Component::ComponentType>::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) =
|
||||
<Component::ComponentType>::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<WidgetId>,
|
||||
pub parent: Option<WidgetId>,
|
||||
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>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Styles>) -> Style
|
||||
fn with_styles(self, styles: impl IntoValue<Styles>) -> 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<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) {
|
||||
self.tree.reset_child_layouts(self.id());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
31
src/widgets/mode_switch.rs
Normal file
31
src/widgets/mode_switch.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Styles>,
|
||||
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<Styles>, child: impl MakeWidget) -> Self {
|
||||
pub fn new(styles: impl IntoValue<Styles>, child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
styles: styles.into(),
|
||||
styles: styles.into_value(),
|
||||
child: WidgetRef::new(child),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/widgets/themed.rs
Normal file
31
src/widgets/themed.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ThemePair>,
|
||||
occluded: Option<Dynamic<bool>>,
|
||||
focused: Option<Dynamic<bool>>,
|
||||
theme_mode: Option<Value<ThemeMode>>,
|
||||
}
|
||||
|
||||
impl<Behavior> Default for Window<Behavior>
|
||||
|
|
@ -156,6 +158,24 @@ impl Window<WidgetInstance> {
|
|||
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<ThemeMode>) -> Self {
|
||||
self.theme_mode = Some(theme_mode.into_value());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Behavior> Window<Behavior>
|
||||
|
|
@ -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<T> {
|
|||
max_inner_size: Option<Size<UPx>>,
|
||||
theme: Option<DynamicReader<ThemePair>>,
|
||||
current_theme: ThemePair,
|
||||
theme_mode: Dynamic<ThemeMode>,
|
||||
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<WindowCommand> {
|
||||
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<C> {
|
||||
pub user: C,
|
||||
|
|
@ -987,6 +1046,7 @@ pub(crate) mod sealed {
|
|||
pub occluded: Option<Dynamic<bool>>,
|
||||
pub focused: Option<Dynamic<bool>>,
|
||||
pub theme: Option<Value<ThemePair>>,
|
||||
pub theme_mode: Option<Value<ThemeMode>>,
|
||||
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<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue