Added WIP theming system

This commit is contained in:
Jonathan Johnson 2023-11-10 09:39:33 -08:00
parent 724f6d7b18
commit d7384b63d8
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
11 changed files with 873 additions and 167 deletions

108
Cargo.lock generated
View file

@ -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"

View file

@ -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"]

View file

@ -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 {

View file

@ -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);
<Named::ComponentType>::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<T> 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<Edges<Dimension>> 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()),
)
}
}

View file

@ -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::<Global>("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::<Global>("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()
}
}

View file

@ -434,7 +434,7 @@ impl TreeData {
let Some(parent) = node.parent else { break };
perspective = parent;
}
query.default_value()
query.default_value(context)
}
}

View file

@ -456,6 +456,13 @@ impl<T> DynamicReader<T> {
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.

View file

@ -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<Dynamic<Color>>,
background_color_animation: AnimationHandle,
text_color: Option<Dynamic<Color>>,
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<Px>, 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
}
}

View file

@ -58,15 +58,17 @@ impl Widget for Label {
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
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);

View file

@ -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))
}
}

View file

@ -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<ThemePair>,
occluded: Option<Dynamic<bool>>,
focused: Option<Dynamic<bool>>,
}
@ -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<T> {
keyboard_activated: Option<ManagedWidget>,
min_inner_size: Option<Size<UPx>>,
max_inner_size: Option<Size<UPx>>,
theme: Option<DynamicReader<ThemePair>>,
current_theme: ThemePair,
}
impl<T> GooeyWindow<T>
@ -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::<Resize>() {
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::<Px>::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::<Px>::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<T> kludgine::app::WindowBehavior<WindowCommand> for GooeyWindow<T>
@ -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::<Resize>() {
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::<Px>::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::<Px>::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<C> {
@ -843,6 +966,7 @@ pub(crate) mod sealed {
pub attributes: Option<WindowAttributes>,
pub occluded: Option<Dynamic<bool>>,
pub focused: Option<Dynamic<bool>>,
pub theme: Option<Value<ThemePair>>,
}
pub enum WindowCommand {