cushy/src/styles.rs
Jonathan Johnson 07b93397c5
Optimizations
2023-11-12 19:54:10 -08:00

1576 lines
44 KiB
Rust

//! 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<AHashMap<ComponentName, Value<Component>>>);
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<Named>(&self, component: &Named) -> Option<&Value<Component>>
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<Named>(
&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);
<Named::ComponentType>::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<Component>`.
pub trait IntoComponentValue {
/// Returns `self` stored in a component value.
fn into_component_value(self) -> Value<Component>;
}
impl<T> IntoComponentValue for T
where
T: Into<Component>,
{
fn into_component_value(self) -> Value<Component> {
Value::Constant(self.into())
}
}
impl IntoComponentValue for Value<Component> {
fn into_component_value(self) -> Value<Component> {
self
}
}
impl<T> IntoComponentValue for Dynamic<T>
where
T: Clone,
Component: From<T>,
{
fn into_component_value(self) -> Value<Component> {
Value::Dynamic(self.map_each_into())
}
}
impl FromIterator<(ComponentName, Component)> for Styles {
fn from_iter<T: IntoIterator<Item = (ComponentName, Component)>>(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<ComponentName, Value<Component>>;
type Item = (ComponentName, Value<Component>);
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<ComponentName, Value<Component>>,
// }
// impl Iterator for StylesIntoIter {
// type Item = (ComponentName, Value<Component>);
// fn next(&mut self) -> Option<Self::Item> {
// 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<Color> for Component {
fn from(value: Color) -> Self {
Self::Color(value)
}
}
impl TryFrom<Component> for Color {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Color(color) => Ok(color),
other => Err(other),
}
}
}
impl From<Dimension> for Component {
fn from(value: Dimension) -> Self {
Self::Dimension(value)
}
}
impl TryFrom<Component> for Dimension {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Dimension(color) => Ok(color),
other => Err(other),
}
}
}
impl From<Px> for Component {
fn from(value: Px) -> Self {
Self::from(Dimension::from(value))
}
}
impl TryFrom<Component> for Px {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Dimension(Dimension::Px(px)) => Ok(px),
other => Err(other),
}
}
}
impl From<Lp> for Component {
fn from(value: Lp) -> Self {
Self::from(Dimension::from(value))
}
}
impl TryFrom<Component> for Lp {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
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<Dimension> for FlexibleDimension {
fn from(dimension: Dimension) -> Self {
Self::Dimension(dimension)
}
}
impl From<Px> for FlexibleDimension {
fn from(value: Px) -> Self {
Self::from(Dimension::from(value))
}
}
impl From<Lp> 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<Px> for Dimension {
fn from(value: Px) -> Self {
Self::Px(value)
}
}
impl From<Lp> 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<i32> 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<f32> 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<i32> 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<f32> 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<Dimension>,
/// The end bound of the range.
pub end: Bound<Dimension>,
}
impl DimensionRange {
/// Returns this range's dimension if the range represents a single
/// dimension.
#[must_use]
pub fn exact_dimension(&self) -> Option<Dimension> {
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<Dimension> {
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<Dimension> {
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<T> From<T> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: T) -> Self {
let dimension = value.into();
Self::from(dimension..=dimension)
}
}
impl<T> From<Range<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: Range<T>) -> Self {
Self {
start: Bound::Included(value.start.into()),
end: Bound::Excluded(value.end.into()),
}
}
}
impl From<RangeFull> for DimensionRange {
fn from(_: RangeFull) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Unbounded,
}
}
}
impl<T> From<RangeInclusive<T>> for DimensionRange
where
T: Into<Dimension> + Clone,
{
fn from(value: RangeInclusive<T>) -> Self {
Self {
start: Bound::Included(value.start().clone().into()),
end: Bound::Included(value.end().clone().into()),
}
}
}
impl<T> From<RangeFrom<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: RangeFrom<T>) -> Self {
Self {
start: Bound::Included(value.start.into()),
end: Bound::Unbounded,
}
}
}
impl<T> From<RangeTo<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: RangeTo<T>) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Excluded(value.end.into()),
}
}
}
impl<T> From<RangeToInclusive<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: RangeToInclusive<T>) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Included(value.end.into()),
}
}
}
impl From<DimensionRange> for Component {
fn from(value: DimensionRange) -> Self {
Component::DimensionRange(value)
}
}
impl TryFrom<Component> for DimensionRange {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::DimensionRange(value) => Ok(value),
other => Err(other),
}
}
}
/// A custom component value.
#[derive(Debug, Clone)]
pub struct CustomComponent(Arc<dyn AnyComponent>);
impl CustomComponent {
/// Wraps an arbitrary value so that it can be used as a [`Component`].
pub fn new<T>(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<T>(&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<Self, Component> {
match component {
Component::Custom(custom) => Ok(custom),
other => Err(other),
}
}
}
trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug {
fn as_any(&self) -> &dyn Any;
}
impl<T> 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>, name: impl Into<Name>) -> Self {
Self {
group: group.into(),
name: name.into(),
}
}
}
impl From<&'static Lazy<ComponentName>> for ComponentName {
fn from(value: &'static Lazy<ComponentName>) -> 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<Self, Component>;
}
impl<T> ComponentType for T
where
T: Into<Component> + TryFrom<Component, Error = Component>,
{
fn into_component(self) -> Component {
self.into()
}
fn try_from_component(component: Component) -> Result<Self, Component> {
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<T> 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<T = FlexibleDimension> {
/// 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<T> Edges<T> {
/// Returns the sum of the parts as a [`Size`].
pub fn size(self) -> Size<T>
where
T: Add<Output = T> + 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<U>(self, mut map: impl FnMut(T) -> U) -> Edges<U> {
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<Output = T>,
{
self.left + self.right
}
/// Returns the sum of the top and bottom edges.
pub fn height(self) -> T
where
T: Add<Output = T>,
{
self.top + self.bottom
}
}
impl<T> Default for Edges<T>
where
T: Default,
{
fn default() -> Self {
Self {
left: T::default(),
right: T::default(),
top: T::default(),
bottom: Default::default(),
}
}
}
impl<T> Edges<T> {
/// Updates `top` and returns self.
#[must_use]
pub fn with_top(mut self, top: impl Into<T>) -> Self {
self.top = top.into();
self
}
/// Updates `bottom` and returns self.
#[must_use]
pub fn with_bottom(mut self, bottom: impl Into<T>) -> Self {
self.bottom = bottom.into();
self
}
/// Updates `right` and returns self.
#[must_use]
pub fn with_right(mut self, right: impl Into<T>) -> Self {
self.right = right.into();
self
}
/// Updates `left` and returns self.
#[must_use]
pub fn with_left(mut self, left: impl Into<T>) -> 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<T>) -> 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<T>) -> Self
where
T: Clone,
{
self.top = vertical.into();
self.bottom = self.top.clone();
self
}
}
impl Edges<Dimension> {
/// Returns a new instance with `dimension` for every edge.
#[must_use]
pub fn uniform<D>(dimension: D) -> Self
where
D: Into<Dimension>,
{
let dimension = dimension.into();
Self::from(dimension)
}
}
impl<T> From<T> for Edges<T>
where
T: Clone,
{
fn from(value: T) -> Self {
Self {
left: value.clone(),
right: value.clone(),
top: value.clone(),
bottom: value,
}
}
}
impl IntoValue<Edges<FlexibleDimension>> for FlexibleDimension {
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
Value::Constant(Edges::from(self))
}
}
impl IntoValue<Edges<FlexibleDimension>> for Dimension {
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
FlexibleDimension::Dimension(self).into_value()
}
}
impl IntoValue<Edges<FlexibleDimension>> for Px {
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
Dimension::from(self).into_value()
}
}
impl IntoValue<Edges<FlexibleDimension>> for Lp {
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
Dimension::from(self).into_value()
}
}
impl IntoValue<Edges<Dimension>> for Dimension {
fn into_value(self) -> Value<Edges<Dimension>> {
Value::Constant(Edges::from(self))
}
}
impl IntoValue<Edges<Dimension>> for Px {
fn into_value(self) -> Value<Edges<Dimension>> {
Dimension::from(self).into_value()
}
}
impl IntoValue<Edges<Dimension>> for Lp {
fn into_value(self) -> Value<Edges<Dimension>> {
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<OklabHue>, saturation: impl Into<ZeroToOne>) -> 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<VisualOrder> for Component {
fn from(value: VisualOrder) -> Self {
Self::VisualOrder(value)
}
}
impl TryFrom<Component> for VisualOrder {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
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>) -> 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<FocusableWidgets> for Component {
fn from(value: FocusableWidgets) -> Self {
Self::FocusableWidgets(value)
}
}
impl TryFrom<Component> for FocusableWidgets {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
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<Self> {
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<ContainerLevel> for Component {
fn from(value: ContainerLevel) -> Self {
Self::ContainerLevel(value)
}
}
impl TryFrom<Component> for ContainerLevel {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::ContainerLevel(level) => Ok(level),
other => Err(other),
}
}
}