mirror of
https://github.com/danbulant/cushy
synced 2026-06-15 12:31:11 +00:00
Added WIP theming system
This commit is contained in:
parent
724f6d7b18
commit
d7384b63d8
11 changed files with 873 additions and 167 deletions
108
Cargo.lock
generated
108
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
451
src/styles.rs
451
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);
|
||||
<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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -434,7 +434,7 @@ impl TreeData {
|
|||
let Some(parent) = node.parent else { break };
|
||||
perspective = parent;
|
||||
}
|
||||
query.default_value()
|
||||
query.default_value(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
248
src/window.rs
248
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<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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue