//! Types for styling widgets. use std::any::Any; use std::borrow::Cow; use std::collections::hash_map; use std::fmt::Debug; use std::ops::{ Add, Bound, Div, Mul, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, }; use std::panic::{RefUnwindSafe, UnwindSafe}; use std::sync::Arc; use ahash::AHashMap; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{Fraction, IntoUnsigned, Rect, ScreenScale, Size}; use kludgine::Color; use palette::{IntoColor, Okhsl, OklabHue, Srgb}; use crate::animation::{EasingFunction, ZeroToOne}; use crate::context::WidgetContext; use crate::names::Name; use crate::utils::Lazy; use crate::value::{Dynamic, IntoValue, Value}; #[macro_use] pub mod components; /// A collection of style components organized by their name. #[derive(Clone, Debug, Default)] pub struct Styles(Arc>>); impl Styles { /// Returns an empty collection. #[must_use] pub fn new() -> Self { Self::default() } /// Returns a collection with the capacity to hold up to `capacity` elements /// without reallocating. #[must_use] pub fn with_capacity(capacity: usize) -> Self { Self(Arc::new(AHashMap::with_capacity(capacity))) } /// Inserts a [`Component`] with a given name. pub fn insert_named(&mut self, name: ComponentName, component: impl IntoComponentValue) { Arc::make_mut(&mut self.0).insert(name, component.into_component_value()); } /// Inserts a [`Component`] using then name provided. pub fn insert(&mut self, name: &impl NamedComponent, component: impl IntoComponentValue) { let name = name.name().into_owned(); self.insert_named(name, component); } /// Adds a [`Component`] for the name provided and returns self. #[must_use] pub fn with(mut self, name: &impl NamedComponent, component: impl IntoComponentValue) -> Self { self.insert(name, component); self } /// Returns the associated component for the given name, if found. #[must_use] pub fn get_named(&self, component: &Named) -> Option<&Value> where Named: NamedComponent + ?Sized, { let name = component.name(); self.0.get(&name) } /// Returns the component associated with the given name, or if not found, /// returns the default value provided by the definition. #[must_use] pub fn get( &self, component: &Named, context: &WidgetContext<'_, '_>, ) -> Named::ComponentType where Named: ComponentDefinition + ?Sized, { let name = component.name(); self.0 .get(&name) .and_then(|component| { component.redraw_when_changed(context); ::try_from_component(component.get()).ok() }) .unwrap_or_else(|| component.default_value(context)) } /// Inserts all components from `other`, overwriting any existing entries /// with the same [`ComponentName`]. pub fn append(&mut self, other: Styles) { for (name, value) in other { self.insert_named(name, value); } } } /// A value that can be converted into a `Value`. pub trait IntoComponentValue { /// Returns `self` stored in a component value. fn into_component_value(self) -> Value; } impl IntoComponentValue for T where T: Into, { fn into_component_value(self) -> Value { Value::Constant(self.into()) } } impl IntoComponentValue for Value { fn into_component_value(self) -> Value { self } } impl IntoComponentValue for Dynamic where T: Clone, Component: From, { fn into_component_value(self) -> Value { Value::Dynamic(self.map_each_into()) } } impl FromIterator<(ComponentName, Component)> for Styles { fn from_iter>(iter: T) -> Self { let iter = iter.into_iter(); let mut styles = Self::with_capacity(iter.size_hint().0); for (name, component) in iter { styles.insert_named(name, component); } styles } } impl IntoIterator for Styles { type IntoIter = hash_map::IntoIter>; type Item = (ComponentName, Value); fn into_iter(self) -> Self::IntoIter { Arc::try_unwrap(self.0) .unwrap_or_else(|err| err.as_ref().clone()) .into_iter() } } // /// An iterator over the owned contents of a [`Styles`] instance. // pub struct StylesIntoIter { // main: hash_map::IntoIter>, // } // impl Iterator for StylesIntoIter { // type Item = (ComponentName, Value); // fn next(&mut self) -> Option { // loop { // if let Some((group, names)) = &mut self.names { // if let Some((name, component)) = names.next() { // return Some((ComponentName::new(group.clone(), name), component)); // } // self.names = None; // } // let (group, names) = self.main.next()?; // self.names = Some((group, names.into_iter())); // } // } // } /// A value of a style component. #[derive(Debug, Clone)] pub enum Component { /// A color. Color(Color), /// A single-dimension measurement. Dimension(Dimension), /// A single-dimension measurement. DimensionRange(DimensionRange), /// A percentage between 0.0 and 1.0. Percent(ZeroToOne), /// An easing function for animations. Easing(EasingFunction), /// A visual ordering to use for layout. VisualOrder(VisualOrder), /// A description of what widgets should be focusable. FocusableWidgets(FocusableWidgets), /// A description of the depth of a /// [`Container`](crate::widgets::Container). ContainerLevel(ContainerLevel), /// A custom component type. Custom(CustomComponent), } impl From for Component { fn from(value: Color) -> Self { Self::Color(value) } } impl TryFrom for Color { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::Color(color) => Ok(color), other => Err(other), } } } impl From for Component { fn from(value: Dimension) -> Self { Self::Dimension(value) } } impl TryFrom for Dimension { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::Dimension(color) => Ok(color), other => Err(other), } } } impl From for Component { fn from(value: Px) -> Self { Self::from(Dimension::from(value)) } } impl TryFrom for Px { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::Dimension(Dimension::Px(px)) => Ok(px), other => Err(other), } } } impl From for Component { fn from(value: Lp) -> Self { Self::from(Dimension::from(value)) } } impl TryFrom for Lp { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::Dimension(Dimension::Lp(px)) => Ok(px), other => Err(other), } } } /// A 1-dimensional measurement that may be automatically calculated. #[derive(Debug, Clone, Copy)] pub enum FlexibleDimension { /// Automatically calculate this dimension. Auto, /// Use this dimension. Dimension(Dimension), } impl FlexibleDimension { /// A dimension of 0 pixels. pub const ZERO: Self = Self::Dimension(Dimension::ZERO); } impl Default for FlexibleDimension { fn default() -> Self { Self::ZERO } } impl From for FlexibleDimension { fn from(dimension: Dimension) -> Self { Self::Dimension(dimension) } } impl From for FlexibleDimension { fn from(value: Px) -> Self { Self::from(Dimension::from(value)) } } impl From for FlexibleDimension { fn from(value: Lp) -> Self { Self::from(Dimension::from(value)) } } /// A 1-dimensional measurement. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum Dimension { /// Physical Pixels Px(Px), /// Logical Pixels Lp(Lp), } impl Dimension { /// A dimension of 0 pixels. pub const ZERO: Self = Self::Px(Px(0)); } impl Default for Dimension { fn default() -> Self { Self::ZERO } } impl From for Dimension { fn from(value: Px) -> Self { Self::Px(value) } } impl From for Dimension { fn from(value: Lp) -> Self { Self::Lp(value) } } impl ScreenScale for Dimension { type Lp = Lp; type Px = Px; fn into_px(self, scale: kludgine::figures::Fraction) -> Px { match self { Dimension::Px(px) => px, Dimension::Lp(lp) => lp.into_px(scale), } } fn from_px(px: Px, _scale: kludgine::figures::Fraction) -> Self { Self::from(px) } fn into_lp(self, scale: kludgine::figures::Fraction) -> Lp { match self { Dimension::Px(px) => px.into_lp(scale), Dimension::Lp(lp) => lp, } } fn from_lp(lp: Lp, _scale: kludgine::figures::Fraction) -> Self { Self::from(lp) } } impl Mul for Dimension { type Output = Dimension; fn mul(self, rhs: i32) -> Self::Output { match self { Self::Px(val) => Self::Px(val * rhs), Self::Lp(val) => Self::Lp(val * rhs), } } } impl Mul for Dimension { type Output = Dimension; fn mul(self, rhs: f32) -> Self::Output { match self { Self::Px(val) => Self::Px(val * rhs), Self::Lp(val) => Self::Lp(val * rhs), } } } impl Div for Dimension { type Output = Dimension; fn div(self, rhs: i32) -> Self::Output { match self { Self::Px(val) => Self::Px(val / rhs), Self::Lp(val) => Self::Lp(val / rhs), } } } impl Div for Dimension { type Output = Dimension; fn div(self, rhs: f32) -> Self::Output { match self { Self::Px(val) => Self::Px(val / rhs), Self::Lp(val) => Self::Lp(val / rhs), } } } /// A range of [`Dimension`]s. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct DimensionRange { /// The start bound of the range. pub start: Bound, /// The end bound of the range. pub end: Bound, } impl DimensionRange { /// Returns this range's dimension if the range represents a single /// dimension. #[must_use] pub fn exact_dimension(&self) -> Option { match (self.start, self.end) { (Bound::Excluded(start), Bound::Included(end)) if start == end => Some(start), _ => None, } } /// Clamps `size` to the dimensions of this range, converting to unsigned /// pixels in the process. #[must_use] pub fn clamp(&self, mut size: UPx, scale: Fraction) -> UPx { if let Some(min) = self.minimum() { size = size.max(min.into_px(scale).into_unsigned()); } if let Some(max) = self.maximum() { size = size.min(max.into_px(scale).into_unsigned()); } size } /// Returns the minimum measurement, if the start is bounded. #[must_use] pub fn minimum(&self) -> Option { match self.start { Bound::Unbounded => None, Bound::Excluded(Dimension::Lp(lp)) => Some(Dimension::Lp(lp + 1)), Bound::Excluded(Dimension::Px(px)) => Some(Dimension::Px(px + 1)), Bound::Included(value) => Some(value), } } /// Returns the maximum measurement, if the end is bounded. #[must_use] pub fn maximum(&self) -> Option { match self.end { Bound::Unbounded => None, Bound::Excluded(Dimension::Lp(lp)) => Some(Dimension::Lp(lp - 1)), Bound::Excluded(Dimension::Px(px)) => Some(Dimension::Px(px - 1)), Bound::Included(value) => Some(value), } } } impl From for DimensionRange where T: Into, { fn from(value: T) -> Self { let dimension = value.into(); Self::from(dimension..=dimension) } } impl From> for DimensionRange where T: Into, { fn from(value: Range) -> Self { Self { start: Bound::Included(value.start.into()), end: Bound::Excluded(value.end.into()), } } } impl From for DimensionRange { fn from(_: RangeFull) -> Self { Self { start: Bound::Unbounded, end: Bound::Unbounded, } } } impl From> for DimensionRange where T: Into + Clone, { fn from(value: RangeInclusive) -> Self { Self { start: Bound::Included(value.start().clone().into()), end: Bound::Included(value.end().clone().into()), } } } impl From> for DimensionRange where T: Into, { fn from(value: RangeFrom) -> Self { Self { start: Bound::Included(value.start.into()), end: Bound::Unbounded, } } } impl From> for DimensionRange where T: Into, { fn from(value: RangeTo) -> Self { Self { start: Bound::Unbounded, end: Bound::Excluded(value.end.into()), } } } impl From> for DimensionRange where T: Into, { fn from(value: RangeToInclusive) -> Self { Self { start: Bound::Unbounded, end: Bound::Included(value.end.into()), } } } impl From for Component { fn from(value: DimensionRange) -> Self { Component::DimensionRange(value) } } impl TryFrom for DimensionRange { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::DimensionRange(value) => Ok(value), other => Err(other), } } } /// A custom component value. #[derive(Debug, Clone)] pub struct CustomComponent(Arc); impl CustomComponent { /// Wraps an arbitrary value so that it can be used as a [`Component`]. pub fn new(value: T) -> Self where T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, { Self(Arc::new(value)) } /// Return the contained value cast as `T`. Returns `None` if `T` does is /// not the same type that was provided when this component was created. #[must_use] pub fn downcast(&self) -> Option<&T> where T: Debug + Send + Sync + 'static, { self.0.as_ref().as_any().downcast_ref() } } impl ComponentType for CustomComponent { fn into_component(self) -> Component { Component::Custom(self) } fn try_from_component(component: Component) -> Result { match component { Component::Custom(custom) => Ok(custom), other => Err(other), } } } trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug { fn as_any(&self) -> &dyn Any; } impl AnyComponent for T where T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static, { fn as_any(&self) -> &dyn Any { self } } /// A fully-qualified style component name. #[derive(Clone, Eq, PartialEq, Debug, Hash)] pub struct ComponentName { /// The group name. pub group: Name, /// The name of the component within the group. pub name: Name, } impl ComponentName { /// Returns a new instance using `group` and `name`. pub fn new(group: impl Into, name: impl Into) -> Self { Self { group: group.into(), name: name.into(), } } } impl From<&'static Lazy> for ComponentName { fn from(value: &'static Lazy) -> Self { (**value).clone() } } /// A type that represents a named style component. pub trait NamedComponent { /// Returns the name of the style component. fn name(&self) -> Cow<'_, ComponentName>; } /// A type that represents a named component with a default value of a specific /// Rust type. pub trait ComponentDefinition: NamedComponent { /// The type that will be contained in the [`Component`]. type ComponentType: ComponentType; /// Returns the default value to use for this component. fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType; } /// A type that can be converted to and from [`Component`]. pub trait ComponentType: Sized { /// Returns this type, wrapped in a [`Component`]. fn into_component(self) -> Component; /// Attempts to extract this type from `component`. If `component` does not /// contain this type, `Err(component)` is returned. fn try_from_component(component: Component) -> Result; } impl ComponentType for T where T: Into + TryFrom, { fn into_component(self) -> Component { self.into() } fn try_from_component(component: Component) -> Result { Self::try_from(component) } } /// 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, context: &WidgetContext<'_, '_>) -> Component; } impl ComponentDefaultvalue for T where T: ComponentDefinition, { fn default_component_value(&self, context: &WidgetContext<'_, '_>) -> Component { self.default_value(context).into_component() } } impl NamedComponent for ComponentName { fn name(&self) -> Cow<'_, ComponentName> { Cow::Borrowed(self) } } impl NamedComponent for Cow<'_, ComponentName> { fn name(&self) -> Cow<'_, ComponentName> { Cow::Borrowed(self) } } /// A type describing characteristics about the edges of a rectangle. #[derive(Clone, Copy, Debug)] pub struct Edges { /// The left edge pub left: T, /// The top edge pub top: T, /// The right edge pub right: T, /// The bottom edge pub bottom: T, } impl Edges { /// Returns the sum of the parts as a [`Size`]. pub fn size(self) -> Size where T: Add + Copy, { Size::new(self.width(), self.height()) } /// Returns a new set of edges produced by calling `map` with each of the /// edges. pub fn map(self, mut map: impl FnMut(T) -> U) -> Edges { Edges { left: map(self.left), top: map(self.top), right: map(self.right), bottom: map(self.bottom), } } /// Returns the sum of the left and right edges. pub fn width(self) -> T where T: Add, { self.left + self.right } /// Returns the sum of the top and bottom edges. pub fn height(self) -> T where T: Add, { self.top + self.bottom } } impl Default for Edges where T: Default, { fn default() -> Self { Self { left: T::default(), right: T::default(), top: T::default(), bottom: Default::default(), } } } impl Edges { /// Updates `top` and returns self. #[must_use] pub fn with_top(mut self, top: impl Into) -> Self { self.top = top.into(); self } /// Updates `bottom` and returns self. #[must_use] pub fn with_bottom(mut self, bottom: impl Into) -> Self { self.bottom = bottom.into(); self } /// Updates `right` and returns self. #[must_use] pub fn with_right(mut self, right: impl Into) -> Self { self.right = right.into(); self } /// Updates `left` and returns self. #[must_use] pub fn with_left(mut self, left: impl Into) -> Self { self.left = left.into(); self } /// Updates left and right to be `horizontal` and returns self. #[must_use] pub fn with_horizontal(mut self, horizontal: impl Into) -> Self where T: Clone, { self.left = horizontal.into(); self.right = self.left.clone(); self } /// Updates top and bottom to be `vertical` and returns self. #[must_use] pub fn with_vertical(mut self, vertical: impl Into) -> Self where T: Clone, { self.top = vertical.into(); self.bottom = self.top.clone(); self } } impl Edges { /// Returns a new instance with `dimension` for every edge. #[must_use] pub fn uniform(dimension: D) -> Self where D: Into, { let dimension = dimension.into(); Self::from(dimension) } } impl From for Edges where T: Clone, { fn from(value: T) -> Self { Self { left: value.clone(), right: value.clone(), top: value.clone(), bottom: value, } } } impl IntoValue> for FlexibleDimension { fn into_value(self) -> Value> { Value::Constant(Edges::from(self)) } } impl IntoValue> for Dimension { fn into_value(self) -> Value> { FlexibleDimension::Dimension(self).into_value() } } impl IntoValue> for Px { fn into_value(self) -> Value> { Dimension::from(self).into_value() } } impl IntoValue> for Lp { fn into_value(self) -> Value> { Dimension::from(self).into_value() } } impl IntoValue> for Dimension { fn into_value(self) -> Value> { Value::Constant(Edges::from(self)) } } impl IntoValue> for Px { fn into_value(self) -> Value> { Dimension::from(self).into_value() } } impl IntoValue> for Lp { fn into_value(self) -> Value> { Dimension::from(self).into_value() } } /// 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), } } } impl Default for ThemePair { fn default() -> Self { const PRIMARY_HUE: f32 = -120.; const SECONDARY_HUE: f32 = 0.; const TERTIARY_HUE: f32 = -30.; const ERROR_HUE: f32 = 30.; Self::from_sources( ColorSource::new(PRIMARY_HUE, 0.8), ColorSource::new(SECONDARY_HUE, 0.3), ColorSource::new(TERTIARY_HUE, 0.3), ColorSource::new(ERROR_HUE, 0.8), ColorSource::new(0., 0.001), ColorSource::new(30., 0.), ) } } /// 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, } 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), } } /// 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), } } } /// 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 background color for widgets that are opaque. pub opaque_widget: 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(97), dim_color: neutral.color(70), bright_color: neutral.color(99), opaque_widget: neutral_variant.color(75), lowest_container: neutral.color(95), low_container: neutral.color(92), container: neutral.color(90), high_container: neutral.color(85), 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.color(2), bright_color: neutral.color(11), opaque_widget: neutral_variant.color(40), 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 primary color, dimmed for de-emphasized or disabled content. pub color_dim: 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), color_dim: source.color(30), 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(70), color_dim: source.color(60), on_color: source.color(10), container: source.color(30), on_container: source.color(90), } } } /// 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), } } } /// 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: impl Into, saturation: impl Into) -> Self { Self { hue: hue.into(), saturation: saturation.into(), } } /// 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) } /// Calculates an approximate ratio between 0.0 and 1.0 of how contrasting /// these colors are, with perfect constrast being two clors that are /// opposite of each other on the hue circle and one fully desaturated and /// the other fully saturated. #[must_use] pub fn contrast_between(self, other: Self) -> ZeroToOne { let saturation_delta = self.saturation.difference_between(other.saturation); let self_hue = self.hue.into_positive_degrees(); let other_hue = other.hue.into_positive_degrees(); // Calculate the shortest distance between the hues, taking into account // that 0 and 359 are one degree apart. let hue_delta = ZeroToOne::new( if self_hue < other_hue { let hue_delta_a = other_hue - self_hue; let hue_delta_b = self_hue + 360. - other_hue; hue_delta_a.min(hue_delta_b) } else { let hue_delta_a = self_hue - other_hue; let hue_delta_b = other_hue + 360. - self_hue; hue_delta_a.min(hue_delta_b) } / 180., ); saturation_delta * hue_delta } } /// 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 contrast between this color and the components provided. /// /// To achieve a contrast of 1.0: /// /// - `self`'s hue and `check_source.hue` must be 180 degrees apart. /// - `self`'s saturation and `check_source.saturation` must be different by /// 1.0. /// - `self`'s lightness and `check_lightness` must be different by 1.0. /// - `self`'s alpha and `check_alpha` must be different by 1.0. /// /// The algorithm currently used is purposely left undocumented as it will /// likely change. It should be a reasonable heuristic until someone smarter /// than @ecton comes along. fn contrast_between( self, check_source: ColorSource, check_lightness: ZeroToOne, check_alpha: ZeroToOne, ) -> ZeroToOne; /// Returns the color in `others` that contrasts the most from `self`. #[must_use] fn most_contrasting(self, others: &[Self]) -> Self where Self: Copy; } 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()), ) } fn contrast_between( self, check_source: ColorSource, check_lightness: ZeroToOne, check_alpha: ZeroToOne, ) -> ZeroToOne { let (other_source, other_lightness) = self.into_source_and_lightness(); let lightness_delta = other_lightness.difference_between(check_lightness); let source_change = check_source.contrast_between(other_source); let other_alpha = ZeroToOne::new(self.alpha_f32()); let alpha_delta = check_alpha.difference_between(other_alpha); ZeroToOne::new((*lightness_delta + *source_change + *alpha_delta) / 3.) } fn most_contrasting(self, others: &[Self]) -> Self where Self: Copy, { let (check_source, check_lightness) = self.into_source_and_lightness(); let check_alpha = ZeroToOne::new(self.alpha_f32()); let mut others = others.iter().copied(); let mut most_contrasting = others.next().expect("at least one comparison"); let mut most_contrast_amount = most_contrasting.contrast_between(check_source, check_lightness, check_alpha); for other in others { let contrast_amount = other.contrast_between(check_source, check_lightness, check_alpha); if contrast_amount > most_contrast_amount { most_contrasting = other; most_contrast_amount = contrast_amount; } } most_contrasting } } /// A 2d ordering configuration. #[derive(Copy, Debug, Clone, Eq, PartialEq)] pub struct VisualOrder { /// The ordering to apply horizontally. pub horizontal: HorizontalOrder, /// The ordering to apply vertically. pub vertical: VerticalOrder, } impl VisualOrder { /// Returns a right-to-left ordering. #[must_use] pub const fn right_to_left() -> Self { Self { horizontal: HorizontalOrder::RightToLeft, vertical: VerticalOrder::TopToBottom, } } /// Returns a left-to-right ordering. #[must_use] pub const fn left_to_right() -> Self { Self { horizontal: HorizontalOrder::LeftToRight, vertical: VerticalOrder::TopToBottom, } } /// Returns the reverse ordering of `self`. #[must_use] pub fn rev(self) -> Self { Self { horizontal: self.horizontal.rev(), vertical: self.vertical.rev(), } } } impl From for Component { fn from(value: VisualOrder) -> Self { Self::VisualOrder(value) } } impl TryFrom for VisualOrder { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::VisualOrder(order) => Ok(order), other => Err(other), } } } /// A horizontal direction. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum HorizontalOrder { /// Describes an order starting at the left and proceeding to the right. LeftToRight, /// Describes an order starting at the right and proceeding to the left. RightToLeft, } impl HorizontalOrder { /// Returns the reverse order of `self`. #[must_use] pub fn rev(self) -> Self { match self { Self::LeftToRight => Self::RightToLeft, Self::RightToLeft => Self::LeftToRight, } } pub(crate) fn sort_key(self, rect: &Rect) -> Px { match self { HorizontalOrder::LeftToRight => rect.origin.x, HorizontalOrder::RightToLeft => -(rect.origin.x + rect.size.width), } } } /// A vertical direction. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum VerticalOrder { /// Describes an order starting at the top and proceeding to the bottom. TopToBottom, /// Describes an order starting at the bottom and proceeding to the top. BottomToTop, } impl VerticalOrder { /// Returns the reverse order of `self`. #[must_use] pub fn rev(self) -> Self { match self { Self::TopToBottom => VerticalOrder::BottomToTop, Self::BottomToTop => VerticalOrder::TopToBottom, } } pub(crate) fn max_px(self) -> Px { match self { VerticalOrder::TopToBottom => Px::MAX, VerticalOrder::BottomToTop => Px::MIN, } } pub(crate) fn smallest_px(self, a: Px, b: Px) -> Px { match self { VerticalOrder::TopToBottom => a.min(b), VerticalOrder::BottomToTop => b.max(a), } } } /// A configuration option to control which controls should be able to receive /// focus through keyboard focus handling or initial focus handling. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] pub enum FocusableWidgets { /// Allow all widgets that can respond to keyboard input to accept focus. #[default] All, /// Only allow widgets that expect textual input to accept focus. OnlyTextual, } impl FocusableWidgets { /// Returns true if all controls should be focusable. #[must_use] pub const fn is_all(self) -> bool { matches!(self, Self::All) } /// Returns true if only textual should be focusable. #[must_use] pub const fn is_only_textual(self) -> bool { matches!(self, Self::OnlyTextual) } } impl From for Component { fn from(value: FocusableWidgets) -> Self { Self::FocusableWidgets(value) } } impl TryFrom for FocusableWidgets { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::FocusableWidgets(focus) => Ok(focus), other => Err(other), } } } /// A description of the level of depth a /// [`Container`](crate::widgets::Container) is nested at. #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] pub enum ContainerLevel { /// The lowest container level. #[default] Lowest, /// The second lowest container level. Low, /// The mid-level container level. Mid, /// The second-highest container level. High, /// The highest container level. Highest, } impl ContainerLevel { /// Returns the next container level, or None if already at the highet /// level. #[must_use] pub const fn next(self) -> Option { match self { Self::Lowest => Some(Self::Low), Self::Low => Some(Self::Mid), Self::Mid => Some(Self::High), Self::High => Some(Self::Highest), Self::Highest => None, } } } impl From for Component { fn from(value: ContainerLevel) -> Self { Self::ContainerLevel(value) } } impl TryFrom for ContainerLevel { type Error = Component; fn try_from(value: Component) -> Result { match value { Component::ContainerLevel(level) => Ok(level), other => Err(other), } } }