From d7384b63d8426fcddfa2cd26e9ddc98cd92689c9 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Fri, 10 Nov 2023 09:39:33 -0800 Subject: [PATCH] Added WIP theming system --- Cargo.lock | 108 +++++++++- Cargo.toml | 1 + src/context.rs | 23 +- src/styles.rs | 451 +++++++++++++++++++++++++++++++++++++-- src/styles/components.rs | 61 +++--- src/tree.rs | 2 +- src/value.rs | 7 + src/widgets/button.rs | 127 ++++++----- src/widgets/label.rs | 8 +- src/widgets/scroll.rs | 4 +- src/window.rs | 248 +++++++++++++++------ 11 files changed, 873 insertions(+), 167 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d41179..4e6a390 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,6 +102,15 @@ dependencies = [ "winit", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -454,6 +463,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "figures" version = "0.1.0" @@ -637,6 +652,7 @@ dependencies = [ "interner", "kempt", "kludgine", + "palette", "pollster", "tracing", "tracing-subscriber", @@ -855,7 +871,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.1.0" -source = "git+https://github.com/khonsulabs/kludgine#5d728e775b9bf64ac30e1e673c9971fc2184cb97" +source = "git+https://github.com/khonsulabs/kludgine#a88961b726101ef9bb46bdae4737308d2dcb12a0" dependencies = [ "ahash", "alot", @@ -1292,6 +1308,29 @@ dependencies = [ "ttf-parser 0.20.0", ] +[[package]] +name = "palette" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1327,6 +1366,48 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1393,6 +1474,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "range-alloc" version = "0.1.3" @@ -1528,9 +1624,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" +checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6" [[package]] name = "serde" @@ -1561,6 +1657,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slotmap" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index 4ab496e..203f6f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ intentional = "0.1.0" tracing = "0.1.40" tracing-subscriber = { version = "0.3", optional = true } +palette = "0.7.3" # [patch."https://github.com/khonsulabs/kludgine"] diff --git a/src/context.rs b/src/context.rs index 8619a0a..594eb0b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; +use kludgine::app::winit::window; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, Point, Rect, Size}; use kludgine::shapes::{Shape, StrokeOptions}; @@ -13,7 +14,7 @@ use kludgine::Kludgine; use crate::graphics::Graphics; use crate::styles::components::{HighlightColor, VisualOrder}; -use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles}; +use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles, Theme, ThemePair}; use crate::value::Dynamic; use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef}; use crate::window::sealed::WindowCommand; @@ -661,6 +662,7 @@ pub struct WidgetContext<'context, 'window> { current_node: ManagedWidget, redraw_status: &'context RedrawStatus, window: &'context mut RunningWindow<'window>, + theme: &'context ThemePair, pending_state: PendingState<'context>, } @@ -668,6 +670,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { pub(crate) fn new( current_node: ManagedWidget, redraw_status: &'context RedrawStatus, + theme: &'context ThemePair, window: &'context mut RunningWindow<'window>, ) -> Self { Self { @@ -683,6 +686,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { }), current_node, redraw_status, + theme, window, } } @@ -693,6 +697,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { current_node: self.current_node.clone(), redraw_status: self.redraw_status, window: &mut *self.window, + theme: self.theme, pending_state: self.pending_state.borrowed(), } } @@ -710,6 +715,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { current_node, redraw_status: self.redraw_status, window: &mut *self.window, + theme: self.theme, pending_state: self.pending_state.borrowed(), }) } @@ -909,6 +915,21 @@ impl<'context, 'window> WidgetContext<'context, 'window> { pub fn window_mut(&mut self) -> &mut RunningWindow<'window> { self.window } + + /// Returns the theme pair for the window. + #[must_use] + pub fn theme_pair(&self) -> &ThemePair { + self.theme + } + + /// 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, + } + } } pub(crate) struct WindowHandle { diff --git a/src/styles.rs b/src/styles.rs index 266dca1..0684633 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -1,12 +1,20 @@ //! Types for styling widgets. +use std::any::Any; use std::borrow::Cow; use std::collections::{hash_map, HashMap}; +use std::fmt::Debug; use std::ops::{ Add, Bound, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, }; +use std::panic::{RefUnwindSafe, UnwindSafe}; use std::sync::Arc; +use kludgine::figures::units::{Lp, Px, UPx}; +use kludgine::figures::{Fraction, IntoUnsigned, ScreenScale, Size}; +use kludgine::Color; +use palette::{IntoColor, Okhsl, OklabHue, Srgb}; + use crate::animation::{EasingFunction, ZeroToOne}; use crate::context::WidgetContext; use crate::names::Name; @@ -86,7 +94,7 @@ impl Styles { component.redraw_when_changed(context); ::try_from_component(component.get()).ok() }) - .unwrap_or_else(|| component.default_value()) + .unwrap_or_else(|| component.default_value(context)) } } @@ -170,14 +178,6 @@ impl Iterator for StylesIntoIter { } } -use std::any::Any; -use std::fmt::Debug; -use std::panic::{RefUnwindSafe, UnwindSafe}; - -use kludgine::figures::units::{Lp, Px, UPx}; -use kludgine::figures::{Fraction, IntoUnsigned, ScreenScale, Size}; -use kludgine::Color; - /// A value of a style component. #[derive(Debug, Clone)] pub enum Component { @@ -648,7 +648,7 @@ pub trait ComponentDefinition: NamedComponent { type ComponentType: ComponentType; /// Returns the default value to use for this component. - fn default_value(&self) -> Self::ComponentType; + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType; } /// A type that can be converted to and from [`Component`]. @@ -676,15 +676,15 @@ where /// A type that represents a named component with a default value. pub trait ComponentDefaultvalue: NamedComponent { /// Returns the default value for this component. - fn default_component_value(&self) -> Component; + fn default_component_value(&self, context: &WidgetContext<'_, '_>) -> Component; } impl ComponentDefaultvalue for T where T: ComponentDefinition, { - fn default_component_value(&self) -> Component { - self.default_value().into_component() + fn default_component_value(&self, context: &WidgetContext<'_, '_>) -> Component { + self.default_value(context).into_component() } } @@ -826,3 +826,428 @@ impl IntoValue> for Dimension { Value::Constant(Edges::from(self)) } } + +/// A set of light and dark [`Theme`]s. +#[derive(Clone, Debug)] +pub struct ThemePair { + /// The theme to use when the user interface is in light mode. + pub light: Theme, + /// The theme to use when the user interface is in dark mode. + pub dark: Theme, + /// A theme of the primary color that remains consistent between dark and + /// light theme variants. + pub primary_fixed: FixedTheme, + /// A theme of the secondary color that remains consistent between dark and + /// light theme variants. + pub secondary_fixed: FixedTheme, + /// A theme of the tertiary color that remains consistent between dark and + /// light theme variants. + pub tertiary_fixed: FixedTheme, + + /// A color to apply to scrims, a term sometimes used to refer to the + /// translucent backdrop placed behind a modal popup. + pub scrim: Color, + + /// A color to apply to shadows. + pub shadow: Color, +} + +impl ThemePair { + /// Returns a new theme generated from the provided color sources. + #[must_use] + pub fn from_sources( + primary: ColorSource, + secondary: ColorSource, + tertiary: ColorSource, + error: ColorSource, + neutral: ColorSource, + neutral_variant: ColorSource, + ) -> Self { + Self { + light: Theme::light_from_sources( + primary, + secondary, + tertiary, + error, + neutral, + neutral_variant, + ), + dark: Theme::dark_from_sources( + primary, + secondary, + tertiary, + error, + neutral, + neutral_variant, + ), + primary_fixed: FixedTheme::from_source(primary), + secondary_fixed: FixedTheme::from_source(secondary), + tertiary_fixed: FixedTheme::from_source(tertiary), + scrim: neutral.color(1), + shadow: neutral.color(1), + } + } +} + +/// A Gooey Color theme. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Theme { + /// The primary color theme. + pub primary: ColorTheme, + /// The secondary color theme. + pub secondary: ColorTheme, + /// The tertiary color theme. + pub tertiary: ColorTheme, + /// The color theme for errors. + pub error: ColorTheme, + + /// The theme to color surfaces. + pub surface: SurfaceTheme, + + /// A theme of inverse colors to provide high contrast to other elements. + pub inverse: InverseTheme, +} + +impl Theme { + /// Returns a new light theme generated from the provided color sources. + #[must_use] + pub fn light_from_sources( + primary: ColorSource, + secondary: ColorSource, + tertiary: ColorSource, + error: ColorSource, + neutral: ColorSource, + neutral_variant: ColorSource, + ) -> Self { + Self { + primary: ColorTheme::light_from_source(primary), + secondary: ColorTheme::light_from_source(secondary), + tertiary: ColorTheme::light_from_source(tertiary), + error: ColorTheme::light_from_source(error), + surface: SurfaceTheme::light_from_sources(neutral, neutral_variant), + inverse: InverseTheme::light_from_sources(primary, neutral), + } + } + + /// Returns a new dark theme generated from the provided color sources. + #[must_use] + pub fn dark_from_sources( + primary: ColorSource, + secondary: ColorSource, + tertiary: ColorSource, + error: ColorSource, + neutral: ColorSource, + neutral_variant: ColorSource, + ) -> Self { + Self { + primary: ColorTheme::dark_from_source(primary), + secondary: ColorTheme::dark_from_source(secondary), + tertiary: ColorTheme::dark_from_source(tertiary), + error: ColorTheme::dark_from_source(error), + surface: SurfaceTheme::dark_from_sources(neutral, neutral_variant), + inverse: InverseTheme::dark_from_sources(primary, neutral), + } + } +} + +/// A theme of surface colors. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SurfaceTheme { + /// The default background color. + pub color: Color, + /// A dimmer variant of the default background color. + pub dim_color: Color, + /// A brighter variant of the default background color. + pub bright_color: Color, + + /// The background color to use for the lowest level container widget. + pub lowest_container: Color, + /// The background color to use for the low level container widgets. + pub low_container: Color, + /// The background color for middle-level container widgets. + pub container: Color, + /// The background color for high-level container widgets. + pub high_container: Color, + /// The background color for highest-level container widgets. + pub highest_container: Color, + + /// The default text/content color. + pub on_color: Color, + /// A variation of the text/content color that is de-emphasized. + pub on_color_variant: Color, + /// The color to draw important outlines. + pub outline: Color, + /// The color to use for decorative outlines. + pub outline_variant: Color, +} + +impl SurfaceTheme { + /// Returns a new light surface theme generated from the two neutral color + /// sources. + #[must_use] + pub fn light_from_sources(neutral: ColorSource, neutral_variant: ColorSource) -> Self { + Self { + color: neutral.color(98), + dim_color: neutral_variant.color(70), + bright_color: neutral.color(99), + lowest_container: neutral.color(100), + low_container: neutral.color(96), + container: neutral.color(95), + high_container: neutral.color(90), + highest_container: neutral.color(80), + on_color: neutral.color(10), + on_color_variant: neutral_variant.color(30), + outline: neutral_variant.color(50), + outline_variant: neutral.color(60), + } + } + + /// Returns a new dark surface theme generated from the two neutral color + /// sources. + #[must_use] + pub fn dark_from_sources(neutral: ColorSource, neutral_variant: ColorSource) -> Self { + Self { + color: neutral.color(10), + dim_color: neutral_variant.color(2), + bright_color: neutral.color(10), + lowest_container: neutral.color(15), + low_container: neutral.color(20), + container: neutral.color(25), + high_container: neutral.color(30), + highest_container: neutral.color(35), + on_color: neutral.color(90), + on_color_variant: neutral_variant.color(70), + outline: neutral_variant.color(60), + outline_variant: neutral.color(50), + } + } +} + +/// A pallete of a shared [`ColorSource`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ColorTheme { + /// The primary color, used for high-emphasis content. + pub color: Color, + /// The color for content that sits atop the primary color. + pub on_color: Color, + /// The backgrond color for containers. + pub container: Color, + /// The color for content that is inside of a container. + pub on_container: Color, +} + +impl ColorTheme { + /// Returns a new light color theme for `source`. + #[must_use] + pub fn light_from_source(source: ColorSource) -> Self { + Self { + color: source.color(40), + on_color: source.color(100), + container: source.color(90), + on_container: source.color(10), + } + } + + /// Returns a new dark color theme for `source`. + #[must_use] + pub fn dark_from_source(source: ColorSource) -> Self { + Self { + color: source.color(80), + on_color: source.color(10), + container: source.color(30), + on_container: source.color(80), + } + } +} + +/// A theme of colors that is shared between light and dark theme variants. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FixedTheme { + /// An accent background color. + pub color: Color, + /// An alternate background color, for less emphasized content. + pub dim_color: Color, + /// The primary color for content on either background color in this theme. + pub on_color: Color, + /// The color for de-emphasized content on either background color in this + /// theme. + pub on_color_variant: Color, +} + +impl FixedTheme { + /// Returns a new color theme from `source` whose colors are safe in both + /// light and dark themes. + #[must_use] + pub fn from_source(source: ColorSource) -> Self { + Self { + color: source.color(90), + dim_color: source.color(80), + on_color: source.color(10), + on_color_variant: source.color(40), + } + } +} + +/// An inverse color theme for displaying highly contrasted elements. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct InverseTheme { + /// An inverse surface color. + pub surface: Color, + /// The default color for content atop an inverted surface. + pub on_surface: Color, + /// The inverted primary color. + pub primary: Color, + // TODO why not inverse for the other colorthemes? +} + +impl InverseTheme { + /// Returns the light-mode, inverse theme for given sources. + #[must_use] + pub fn light_from_sources(primary: ColorSource, surface: ColorSource) -> Self { + Self { + surface: surface.color(30), + on_surface: surface.color(90), + primary: primary.color(80), + } + } + + /// Returns the dark-mode, inverse theme for given sources. + #[must_use] + pub fn dark_from_sources(primary: ColorSource, surface: ColorSource) -> Self { + Self { + surface: surface.color(90), + on_surface: surface.color(10), + primary: primary.color(40), + } + } +} + +/// A source for [`Color`]s. +/// +/// This type is a combination of an [`OklabHue`] and a saturation ranging from +/// 0.0 to 1.0. When combined with a luminance value, a [`Color`] can be +/// generated. +/// +/// The goal of this type is to allow various tones of a given hue/saturation to +/// be generated easily. +#[derive(Clone, Copy, Debug)] +pub struct ColorSource { + /// A measurement of hue, in degees, from -180 to 180. + /// + /// For fully saturated bright colors: + /// + /// - 0° corresponds to a kind of magenta-pink (RBG #ff0188), + /// - 90° to a kind of yellow (RBG RGB #ffcb00) + /// - 180° to a kind of cyan (RBG #00ffe1) and + /// - 240° to a kind of blue (RBG #00aefe). + pub hue: OklabHue, + /// A measurement of saturation. + /// + /// A saturation of 0.0 corresponds to shades of gray, while a saturation of + /// 1.0 corresponds to fully saturated colors. + pub saturation: ZeroToOne, +} + +impl ColorSource { + /// Returns a new source with the given hue (in degrees) and saturation (0.0 + /// - 1.0). + #[must_use] + pub fn new(hue: f32, saturation: f32) -> Self { + Self { + hue: OklabHue::new(hue), + saturation: ZeroToOne::new(saturation), + } + } + + /// Generates a new color by combing the hue, saturation, and lightness. + #[must_use] + pub fn color(self, lightness: impl Lightness) -> Color { + let rgb: palette::Srgb = + Okhsl::new(self.hue, *self.saturation, *lightness.into_lightness()).into_color(); + Color::new_f32(rgb.red, rgb.blue, rgb.green, 1.0) + } +} + +/// A value that can represent the lightness of a color. +/// +/// This is implemented for these types: +/// +/// - [`ZeroToOne`]: A range of 0.0 to 1.0. +/// - `f32`: Values are clamped to 0.0 and 1.0. Panics if NaN. +/// - `u8`: A range of 0 to 100. Values above 100 are clamped. +pub trait Lightness { + /// Returns this value as a floating point clamped between 0 and 1. + fn into_lightness(self) -> ZeroToOne; +} + +impl Lightness for ZeroToOne { + fn into_lightness(self) -> ZeroToOne { + self + } +} +impl Lightness for f32 { + fn into_lightness(self) -> ZeroToOne { + ZeroToOne::new(self) + } +} + +impl Lightness for u8 { + fn into_lightness(self) -> ZeroToOne { + ZeroToOne::new(f32::from(self) / 100.) + } +} + +/// Extra functionality added to the [`Color`] type from Kludgine. +pub trait ColorExt: Copy { + /// Converts this color into its hue/saturation and lightness components. + fn into_source_and_lightness(self) -> (ColorSource, ZeroToOne); + + /// Returns the hue and saturation of this color. + fn source(self) -> ColorSource { + self.into_source_and_lightness().0 + } + + /// Returns the perceived lightness of this color. + #[must_use] + fn lightness(self) -> ZeroToOne { + self.into_source_and_lightness().1 + } + + /// Returns the color in `others` that contrasts the most from `self`. + #[must_use] + fn most_contrasting(self, others: &[Self]) -> Self + where + Self: Copy, + { + // TODO this currently only checks lightness. We should probably factor + // in hue/saturation changes too. + let check = self.lightness(); + + let mut others = others.iter().copied(); + let mut most_contrasting = others.next().expect("at least one comparison"); + let mut most_contrast_amount = (*most_contrasting.lightness() - *check).abs(); + for other in others { + let contrast_amount = (*other.lightness() - *check).abs(); + if contrast_amount > most_contrast_amount { + most_contrasting = other; + most_contrast_amount = contrast_amount; + } + } + + most_contrasting + } +} + +impl ColorExt for Color { + fn into_source_and_lightness(self) -> (ColorSource, ZeroToOne) { + let hsl: palette::Okhsl = + Srgb::new(self.red_f32(), self.green_f32(), self.blue_f32()).into_color(); + ( + ColorSource { + hue: hsl.hue, + saturation: ZeroToOne::new(hsl.saturation), + }, + ZeroToOne::new(hsl.lightness * self.alpha_f32()), + ) + } +} diff --git a/src/styles/components.rs b/src/styles/components.rs index 69f8fb5..43a33bb 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -7,6 +7,7 @@ use kludgine::Color; use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic}; use crate::animation::EasingFunction; +use crate::context::WidgetContext; use crate::styles::{ Component, ComponentDefinition, ComponentName, Dimension, Global, NamedComponent, }; @@ -24,7 +25,7 @@ impl NamedComponent for TextSize { impl ComponentDefinition for TextSize { type ComponentType = Dimension; - fn default_value(&self) -> Dimension { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Dimension { Dimension::Lp(Lp::points(12)) } } @@ -42,11 +43,29 @@ impl NamedComponent for LineHeight { impl ComponentDefinition for LineHeight { type ComponentType = Dimension; - fn default_value(&self) -> Dimension { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Dimension { Dimension::Lp(Lp::points(14)) } } +/// The [`Color`] to use when rendering text. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct SurfaceColor; + +impl NamedComponent for SurfaceColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("surface_color")) + } +} + +impl ComponentDefinition for SurfaceColor { + type ComponentType = Color; + + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.color + } +} + /// The [`Color`] to use when rendering text. #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct TextColor; @@ -60,26 +79,8 @@ impl NamedComponent for TextColor { impl ComponentDefinition for TextColor { type ComponentType = Color; - fn default_value(&self) -> Color { - Color::WHITE - } -} - -/// A [`Color`] to be used as a highlight color. -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -pub struct PrimaryColor; - -impl NamedComponent for PrimaryColor { - fn name(&self) -> Cow<'_, ComponentName> { - Cow::Owned(ComponentName::named::("primary_color")) - } -} - -impl ComponentDefinition for PrimaryColor { - type ComponentType = Color; - - fn default_value(&self) -> Color { - Color::BLUE + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.on_color } } @@ -96,8 +97,8 @@ impl NamedComponent for HighlightColor { impl ComponentDefinition for HighlightColor { type ComponentType = Color; - fn default_value(&self) -> Color { - Color::AQUA + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().primary.color } } @@ -116,7 +117,7 @@ impl NamedComponent for IntrinsicPadding { impl ComponentDefinition for IntrinsicPadding { type ComponentType = Dimension; - fn default_value(&self) -> Dimension { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Dimension { Dimension::Lp(Lp::points(5)) } } @@ -135,7 +136,7 @@ impl NamedComponent for Easing { impl ComponentDefinition for Easing { type ComponentType = EasingFunction; - fn default_value(&self) -> Self::ComponentType { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType { EasingFunction::from(EaseInQuadradic) } } @@ -156,7 +157,7 @@ impl NamedComponent for EasingIn { impl ComponentDefinition for EasingIn { type ComponentType = EasingFunction; - fn default_value(&self) -> Self::ComponentType { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType { EasingFunction::from(EaseInQuadradic) } } @@ -177,7 +178,7 @@ impl NamedComponent for EasingOut { impl ComponentDefinition for EasingOut { type ComponentType = EasingFunction; - fn default_value(&self) -> Self::ComponentType { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType { EasingFunction::from(EaseOutQuadradic) } } @@ -233,7 +234,7 @@ impl NamedComponent for LayoutOrder { impl ComponentDefinition for LayoutOrder { type ComponentType = VisualOrder; - fn default_value(&self) -> Self::ComponentType { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType { VisualOrder::left_to_right() } } @@ -329,7 +330,7 @@ impl NamedComponent for AutoFocusableControls { impl ComponentDefinition for AutoFocusableControls { type ComponentType = FocusableWidgets; - fn default_value(&self) -> Self::ComponentType { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType { FocusableWidgets::default() } } diff --git a/src/tree.rs b/src/tree.rs index 11f1f5f..a7e8d22 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -434,7 +434,7 @@ impl TreeData { let Some(parent) = node.parent else { break }; perspective = parent; } - query.default_value() + query.default_value(context) } } diff --git a/src/value.rs b/src/value.rs index 2fdb67a..4434039 100644 --- a/src/value.rs +++ b/src/value.rs @@ -456,6 +456,13 @@ impl DynamicReader { map(&state.wrapped.value) } + /// Returns true if the dynamic has been modified since the last time the + /// value was accessed through this reader. + #[must_use] + pub fn has_updated(&self) -> bool { + self.source.state().wrapped.generation != self.read_generation + } + /// Returns a clone of the currently contained value. /// /// This function marks the currently stored value as being read. diff --git a/src/widgets/button.rs b/src/widgets/button.rs index b44e5e7..ed975f7 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -14,9 +14,9 @@ use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::names::Name; use crate::styles::components::{ - AutoFocusableControls, Easing, HighlightColor, IntrinsicPadding, PrimaryColor, TextColor, + AutoFocusableControls, Easing, IntrinsicPadding, SurfaceColor, TextColor, }; -use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; +use crate::styles::{ColorExt, ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; use crate::utils::ModifiersExt; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; @@ -33,7 +33,8 @@ pub struct Button { currently_enabled: bool, buttons_pressed: usize, background_color: Option>, - background_color_animation: AnimationHandle, + text_color: Option>, + color_animation: AnimationHandle, } impl Button { @@ -46,7 +47,8 @@ impl Button { currently_enabled: true, buttons_pressed: 0, background_color: None, - background_color_animation: AnimationHandle::default(), + text_color: None, + color_animation: AnimationHandle::default(), } } @@ -78,54 +80,84 @@ impl Button { } } - fn update_background_color(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) { + fn update_colors(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) { let styles = context.query_styles(&[ &ButtonActiveBackground, &ButtonBackground, &ButtonHoverBackground, &ButtonDisabledBackground, - &PrimaryColor, &Easing, + &TextColor, + &SurfaceColor, ]); - let background_color = if !self.enabled.get() { - styles.get(&ButtonDisabledBackground, context) - } else if context.active() { - styles.get(&ButtonActiveBackground, context) - } else if context.hovered() { - styles.get(&ButtonHoverBackground, context) + let text_color = styles.get(&TextColor, context); + let surface_color = styles.get(&SurfaceColor, context); + let (background_color, text_color, surface_color) = if !self.enabled.get() { + ( + styles.get(&ButtonDisabledBackground, context), + text_color, + surface_color, + ) } else if context.is_default() { - styles.get(&PrimaryColor, context) + // TODO this probably should be de-prioritized if ButtonBackground is explicitly set. + ( + context.theme().primary.color, + context.theme().primary.on_color, + context.theme().primary.color, + ) + } else if context.active() { + ( + styles.get(&ButtonActiveBackground, context), + text_color, + surface_color, + ) + } else if context.hovered() { + ( + styles.get(&ButtonHoverBackground, context), + text_color, + surface_color, + ) } else { - styles.get(&ButtonBackground, context) + ( + styles.get(&ButtonBackground, context), + text_color, + surface_color, + ) }; - match (immediate, &self.background_color) { - (false, Some(dynamic)) => { - self.background_color_animation = dynamic - .transition_to(background_color) + let text_color = background_color.most_contrasting(&[text_color, surface_color]); + + match (immediate, &self.background_color, &self.text_color) { + (false, Some(bg), Some(text)) => { + self.color_animation = ( + bg.transition_to(background_color), + text.transition_to(text_color), + ) .over(Duration::from_millis(150)) .with_easing(styles.get(&Easing, context)) .spawn(); } - (true, Some(dynamic)) => { - dynamic.update(background_color); - self.background_color_animation.clear(); + (true, Some(bg), Some(text)) => { + bg.update(background_color); + text.update(text_color); + self.color_animation.clear(); } - (_, None) => { - let dynamic = Dynamic::new(background_color); - self.background_color = Some(dynamic); + _ => { + self.background_color = Some(Dynamic::new(background_color)); + self.text_color = Some(Dynamic::new(text_color)); } } } - fn current_background_color(&mut self, context: &WidgetContext<'_, '_>) -> Color { + fn current_colors(&mut self, context: &WidgetContext<'_, '_>) -> (Color, Color) { if self.background_color.is_none() { - self.update_background_color(context, false); + self.update_colors(context, false); } let background_color = self.background_color.as_ref().expect("always initialized"); + let text_color = self.text_color.as_ref().expect("always initialized"); // TODO combine these into a single option context.redraw_when_changed(background_color); - background_color.get() + (background_color.get(), text_color.get()) } } @@ -135,7 +167,7 @@ impl Widget for Button { // TODO This seems ugly. It needs context, so it can't be moved into the // dynamic system. if self.currently_enabled != enabled { - self.update_background_color(context, false); + self.update_colors(context, false); self.currently_enabled = enabled; } @@ -144,28 +176,19 @@ impl Widget for Button { self.label.redraw_when_changed(context); self.enabled.redraw_when_changed(context); - let styles = context.query_styles(&[ - &TextColor, - &HighlightColor, - &ButtonActiveBackground, - &ButtonBackground, - &ButtonHoverBackground, - ]); - let visible_rect = Rect::from(size - (Px(1), Px(1))); - let background = self.current_background_color(context); - let background = Shape::filled_rect(visible_rect, background); + let (background_color, text_color) = self.current_colors(context); + let background = Shape::filled_rect(visible_rect, background_color); context .gfx .draw_shape(&background, Point::default(), None, None); if context.focused() { - context.draw_focus_ring_using(&styles); + context.draw_focus_ring(); } self.label.map(|label| { - let text_color = styles.get(&TextColor, context); context.gfx.draw_text( Text::new(label, text_color) .origin(kludgine::text::TextOrigin::Center) @@ -292,11 +315,11 @@ impl Widget for Button { } fn unhover(&mut self, context: &mut EventContext<'_, '_>) { - self.update_background_color(context, false); + self.update_colors(context, false); } fn hover(&mut self, _location: Point, context: &mut EventContext<'_, '_>) { - self.update_background_color(context, false); + self.update_colors(context, false); } fn focus(&mut self, context: &mut EventContext<'_, '_>) { @@ -313,11 +336,11 @@ impl Widget for Button { if self.buttons_pressed == 0 { self.invoke_on_click(); } - self.update_background_color(context, true); + self.update_colors(context, true); } fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { - self.update_background_color(context, false); + self.update_colors(context, false); } } @@ -340,8 +363,8 @@ impl NamedComponent for ButtonBackground { impl ComponentDefinition for ButtonBackground { type ComponentType = Color; - fn default_value(&self) -> Color { - Color::new(10, 10, 10, 255) + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.color } } @@ -358,8 +381,8 @@ impl NamedComponent for ButtonActiveBackground { impl ComponentDefinition for ButtonActiveBackground { type ComponentType = Color; - fn default_value(&self) -> Color { - Color::new(30, 30, 30, 255) + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.dim_color } } @@ -377,8 +400,8 @@ impl NamedComponent for ButtonHoverBackground { impl ComponentDefinition for ButtonHoverBackground { type ComponentType = Color; - fn default_value(&self) -> Color { - Color::new(40, 40, 40, 255) + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.bright_color } } @@ -396,7 +419,7 @@ impl NamedComponent for ButtonDisabledBackground { impl ComponentDefinition for ButtonDisabledBackground { type ComponentType = Color; - fn default_value(&self) -> Color { - Color::new(50, 30, 30, 255) + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color { + context.theme().surface.dim_color } } diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 0dda07b..c3bf38e 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -58,15 +58,17 @@ impl Widget for Label { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let padding = context - .query_style(&IntrinsicPadding) + let styles = context.query_styles(&[&TextColor, &IntrinsicPadding]); + let padding = styles + .get(&IntrinsicPadding, context) .into_px(context.gfx.scale()) .into_unsigned(); + let color = styles.get(&TextColor, context); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); self.text.map(|contents| { let measured = context .gfx - .measure_text(Text::from(contents).wrap_at(width)); + .measure_text(Text::new(contents, color).wrap_at(width)); let mut size = measured.size.try_cast().unwrap_or_default(); size += padding * 2; self.prepared_text = Some(measured); diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index a4158ec..58b8e09 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -12,7 +12,7 @@ use kludgine::shapes::Shape; use kludgine::Color; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; -use crate::context::{AsEventContext, EventContext, LayoutContext}; +use crate::context::{AsEventContext, EventContext, LayoutContext, WidgetContext}; use crate::styles::components::{EasingIn, EasingOut, LineHeight}; use crate::styles::{ ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent, @@ -318,7 +318,7 @@ pub struct ScrollBarThickness; impl ComponentDefinition for ScrollBarThickness { type ComponentType = Dimension; - fn default_value(&self) -> Self::ComponentType { + fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType { Dimension::Lp(Lp::points(7)) } } diff --git a/src/window.rs b/src/window.rs index 26ca088..6c0bb66 100644 --- a/src/window.rs +++ b/src/window.rs @@ -19,6 +19,7 @@ use kludgine::app::WindowBehavior as _; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; use kludgine::render::Drawing; +use kludgine::shapes::Shape; use kludgine::Kludgine; use tracing::Level; @@ -28,9 +29,10 @@ use crate::context::{ }; use crate::graphics::Graphics; use crate::styles::components::LayoutOrder; +use crate::styles::{ColorSource, ThemePair}; use crate::tree::Tree; use crate::utils::ModifiersExt; -use crate::value::{Dynamic, IntoDynamic}; +use crate::value::{Dynamic, DynamicReader, IntoDynamic, Value}; use crate::widget::{ EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, }; @@ -99,6 +101,8 @@ where context: Behavior::Context, /// The attributes of this window. pub attributes: WindowAttributes, + /// The colors to use to theme the user interface. + pub theme: Value, occluded: Option>, focused: Option>, } @@ -182,6 +186,14 @@ where ..WindowAttributes::default() }, context, + theme: Value::Constant(ThemePair::from_sources( + ColorSource::new(-120., 0.8), + ColorSource::new(0., 0.3), + ColorSource::new(-30., 0.3), + ColorSource::new(30., 0.8), + ColorSource::new(0., 0.001), + ColorSource::new(30., 0.), + )), occluded: None, focused: None, } @@ -200,6 +212,7 @@ where attributes: Some(self.attributes), occluded: self.occluded, focused: self.focused, + theme: Some(self.theme), }), })) } @@ -251,6 +264,8 @@ struct GooeyWindow { keyboard_activated: Option, min_inner_size: Option>, max_inner_size: Option>, + theme: Option>, + current_theme: ThemePair, } impl GooeyWindow @@ -274,13 +289,23 @@ where if let Some(default) = widget.and_then(|id| self.root.tree.widget(id)) { if let Some(previously_active) = self.keyboard_activated.take() { EventContext::new( - WidgetContext::new(previously_active, &self.redraw_status, window), + WidgetContext::new( + previously_active, + &self.redraw_status, + &self.current_theme, + window, + ), kludgine, ) .deactivate(); } EventContext::new( - WidgetContext::new(default.clone(), &self.redraw_status, window), + WidgetContext::new( + default.clone(), + &self.redraw_status, + &self.current_theme, + window, + ), kludgine, ) .activate(); @@ -288,12 +313,69 @@ where } } else if let Some(keyboard_activated) = self.keyboard_activated.take() { EventContext::new( - WidgetContext::new(keyboard_activated, &self.redraw_status, window), + WidgetContext::new( + keyboard_activated, + &self.redraw_status, + &self.current_theme, + window, + ), kludgine, ) .deactivate(); } } + + fn constrain_window_resizing( + &mut self, + resizable: bool, + window: &kludgine::app::Window<'_, WindowCommand>, + graphics: &mut kludgine::Graphics<'_>, + ) { + let mut root_or_child = self.root.widget.clone(); + loop { + let mut widget = root_or_child.lock(); + if let Some(resize) = widget.downcast_ref::() { + let min_width = resize + .width + .minimum() + .map_or(Px(0), |width| width.into_px(graphics.scale())); + let max_width = resize + .width + .maximum() + .map_or(Px::MAX, |width| width.into_px(graphics.scale())); + let min_height = resize + .height + .minimum() + .map_or(Px(0), |height| height.into_px(graphics.scale())); + let max_height = resize + .height + .maximum() + .map_or(Px::MAX, |height| height.into_px(graphics.scale())); + + let new_min_size = (min_width > 0 || min_height > 0) + .then_some(Size::::new(min_width, min_height).into_unsigned()); + + if new_min_size != self.min_inner_size { + window.set_min_inner_size(new_min_size); + self.min_inner_size = new_min_size; + } + let new_max_size = (max_width > 0 || max_height > 0) + .then_some(Size::::new(max_width, max_height).into_unsigned()); + + if new_max_size != self.max_inner_size && resizable { + window.set_max_inner_size(new_max_size); + } + self.max_inner_size = new_max_size; + break; + } else if let Some(wraps) = widget.as_widget().wraps().cloned() { + drop(widget); + + root_or_child = wraps; + } else { + break; + } + } + } } impl kludgine::app::WindowBehavior for GooeyWindow @@ -319,12 +401,23 @@ where .focused .take() .unwrap_or_default(); + let theme = context + .settings + .borrow_mut() + .theme + .take() + .expect("theme always present"); let mut behavior = T::initialize( &mut RunningWindow::new(window, &focused, &occluded), context.user, ); let root = Tree::default().push_boxed(behavior.make_root(), None); + let (current_theme, theme) = match theme { + Value::Constant(theme) => (theme, None), + Value::Dynamic(dynamic) => (dynamic.get(), Some(dynamic.into_reader())), + }; + Self { behavior, root, @@ -342,6 +435,8 @@ where keyboard_activated: None, min_inner_size: None, max_inner_size: None, + current_theme, + theme, } } @@ -350,66 +445,43 @@ where window: kludgine::app::Window<'_, WindowCommand>, graphics: &mut kludgine::Graphics<'_>, ) { + if let Some(theme) = &mut self.theme { + if theme.has_updated() { + self.current_theme = theme.get(); + // TODO invalidate everything, but right now we don't have much + // cached. Maybe widgets should be told the theme has changed in + // case some things like images have been cached. + } + } + self.redraw_status.refresh_received(); graphics.reset_text_attributes(); self.root.tree.reset_render_order(); let resizable = window.winit().is_resizable(); - { - let mut root_or_child = self.root.widget.clone(); - loop { - let mut widget = root_or_child.lock(); - if let Some(resize) = widget.downcast_ref::() { - let min_width = resize - .width - .minimum() - .map_or(Px(0), |width| width.into_px(graphics.scale())); - let max_width = resize - .width - .maximum() - .map_or(Px::MAX, |width| width.into_px(graphics.scale())); - let min_height = resize - .height - .minimum() - .map_or(Px(0), |height| height.into_px(graphics.scale())); - let max_height = resize - .height - .maximum() - .map_or(Px::MAX, |height| height.into_px(graphics.scale())); - - let new_min_size = (min_width > 0 || min_height > 0) - .then_some(Size::::new(min_width, min_height).into_unsigned()); - - if new_min_size != self.min_inner_size { - window.set_min_inner_size(new_min_size); - self.min_inner_size = new_min_size; - } - let new_max_size = (max_width > 0 || max_height > 0) - .then_some(Size::::new(max_width, max_height).into_unsigned()); - - if new_max_size != self.max_inner_size && resizable { - window.set_max_inner_size(new_max_size); - } - self.max_inner_size = new_max_size; - break; - } else if let Some(wraps) = widget.as_widget().wraps().cloned() { - drop(widget); - - root_or_child = wraps; - } else { - break; - } - } - } + self.constrain_window_resizing(resizable, &window, graphics); let graphics = self.contents.new_frame(graphics); let mut window = RunningWindow::new(window, &self.focused, &self.occluded); let mut context = GraphicsContext { - widget: WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), + widget: WidgetContext::new( + self.root.clone(), + &self.redraw_status, + &self.current_theme, + &mut window, + ), gfx: Exclusive::Owned(Graphics::new(graphics)), }; let mut layout_context = LayoutContext::new(&mut context); let window_size = layout_context.gfx.size(); + + let background_color = layout_context.theme().surface.color; + layout_context.graphics.gfx.draw_shape( + &Shape::filled_rect(window_size.into(), background_color), + Point::default(), + None, + None, + ); let actual_size = layout_context.layout(Size::new( ConstraintLimit::ClippedAfter(window_size.width), ConstraintLimit::ClippedAfter(window_size.height), @@ -539,7 +611,12 @@ where let target = self.root.tree.widget(target).expect("missing widget"); let mut window = RunningWindow::new(window, &self.focused, &self.occluded); let mut target = EventContext::new( - WidgetContext::new(target, &self.redraw_status, &mut window), + WidgetContext::new( + target, + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); @@ -563,7 +640,12 @@ where let target = self.root.tree.focused_widget().unwrap_or(self.root.id()); let target = self.root.tree.widget(target).expect("missing widget"); let mut target = EventContext::new( - WidgetContext::new(target, &self.redraw_status, &mut window), + WidgetContext::new( + target, + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); let mut visual_order = target.query_style(&LayoutOrder); @@ -624,7 +706,12 @@ where let mut window = RunningWindow::new(window, &self.focused, &self.occluded); let mut widget = EventContext::new( - WidgetContext::new(widget, &self.redraw_status, &mut window), + WidgetContext::new( + widget, + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); recursively_handle_event(&mut widget, |widget| { @@ -653,7 +740,12 @@ where }); let mut window = RunningWindow::new(window, &self.focused, &self.occluded); let mut target = EventContext::new( - WidgetContext::new(widget, &self.redraw_status, &mut window), + WidgetContext::new( + widget, + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); @@ -676,7 +768,12 @@ where // Mouse Drag for (button, handler) in state { let mut context = EventContext::new( - WidgetContext::new(handler.clone(), &self.redraw_status, &mut window), + WidgetContext::new( + handler.clone(), + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); let last_rendered_at = context.last_layout().expect("passed hit test"); @@ -685,7 +782,12 @@ where } else { // Hover let mut context = EventContext::new( - WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), + WidgetContext::new( + self.root.clone(), + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); self.mouse_state.widget = None; @@ -720,7 +822,12 @@ where if self.mouse_state.widget.take().is_some() { let mut window = RunningWindow::new(window, &self.focused, &self.occluded); let mut context = EventContext::new( - WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), + WidgetContext::new( + self.root.clone(), + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); context.clear_hover(); @@ -739,7 +846,12 @@ where match state { ElementState::Pressed => { EventContext::new( - WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), + WidgetContext::new( + self.root.clone(), + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ) .clear_focus(); @@ -749,7 +861,12 @@ where { if let Some(handler) = recursively_handle_event( &mut EventContext::new( - WidgetContext::new(hovered.clone(), &self.redraw_status, &mut window), + WidgetContext::new( + hovered.clone(), + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ), |context| { @@ -778,7 +895,12 @@ where } let mut context = EventContext::new( - WidgetContext::new(handler, &self.redraw_status, &mut window), + WidgetContext::new( + handler, + &self.redraw_status, + &self.current_theme, + &mut window, + ), kludgine, ); @@ -831,7 +953,8 @@ struct MouseState { pub(crate) mod sealed { use std::cell::RefCell; - use crate::value::Dynamic; + use crate::styles::ThemePair; + use crate::value::{Dynamic, Value}; use crate::window::WindowAttributes; pub struct Context { @@ -843,6 +966,7 @@ pub(crate) mod sealed { pub attributes: Option, pub occluded: Option>, pub focused: Option>, + pub theme: Option>, } pub enum WindowCommand {