mirror of
https://github.com/danbulant/cushy
synced 2026-06-14 20:11:04 +00:00
2715 lines
74 KiB
Rust
2715 lines
74 KiB
Rust
//! Types for styling widgets.
|
|
|
|
use std::any::Any;
|
|
use std::borrow::Cow;
|
|
use std::collections::hash_map;
|
|
use std::fmt::{Debug, Write};
|
|
use std::ops::{
|
|
Add, AddAssign, Bound, Deref, Div, Mul, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo,
|
|
RangeToInclusive,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
use ahash::AHashMap;
|
|
use figures::units::{Lp, Px, UPx};
|
|
use figures::{Fraction, IntoSigned, IntoUnsigned, Rect, Round, ScreenScale, Size, Zero};
|
|
use intentional::Cast;
|
|
pub use kludgine::cosmic_text::{FamilyOwned, Style, Weight};
|
|
pub use kludgine::shapes::CornerRadii;
|
|
pub use kludgine::Color;
|
|
pub use palette::OklabHue;
|
|
use palette::{IntoColor, Okhsl, Srgb};
|
|
|
|
use crate::animation::{EasingFunction, ZeroToOne};
|
|
use crate::context::{Trackable, WidgetContext};
|
|
use crate::names::Name;
|
|
use crate::utils::Lazy;
|
|
use crate::value::{Dynamic, IntoValue, Source, Value};
|
|
use crate::widgets::input::CowString;
|
|
|
|
#[macro_use]
|
|
pub mod components;
|
|
|
|
/// A collection of style components organized by their name.
|
|
#[derive(Clone, Default)]
|
|
pub struct Styles(Arc<StyleData>);
|
|
|
|
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(StyleData {
|
|
components: AHashMap::with_capacity(capacity),
|
|
}))
|
|
}
|
|
|
|
/// Inserts a [`Component`] with a given name.
|
|
pub fn insert_named(&mut self, name: ComponentName, component: impl IntoStoredComponent) {
|
|
Arc::make_mut(&mut self.0)
|
|
.components
|
|
.insert(name, component.into_stored_component());
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// Inserts a [`Component`] using then name provided, resolving the value
|
|
/// through `dynamic`.
|
|
pub fn insert_dynamic(
|
|
&mut self,
|
|
name: &impl NamedComponent,
|
|
dynamic: impl IntoDynamicComponentValue,
|
|
) {
|
|
let component = match dynamic.into_dynamic_component() {
|
|
Value::Constant(dynamic) => Value::Constant(Component::Dynamic(dynamic)),
|
|
Value::Dynamic(dynamic) => Value::Dynamic(dynamic.map_each_cloned(Component::Dynamic)),
|
|
};
|
|
self.insert(name, component);
|
|
}
|
|
|
|
/// Adds a [`Component`] for the name provided and returns self.
|
|
#[must_use]
|
|
pub fn with<C: ComponentDefinition>(
|
|
mut self,
|
|
name: &C,
|
|
component: impl IntoValue<C::ComponentType>,
|
|
) -> Self
|
|
where
|
|
Value<C::ComponentType>: IntoComponentValue,
|
|
{
|
|
self.insert_named(
|
|
name.name().into_owned(),
|
|
StoredComponent {
|
|
inherited: false,
|
|
inheritable: true,
|
|
component: component.into_value().into_component_value(),
|
|
},
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Adds a [`Component`] using then name provided, resolving the value
|
|
/// through `dynamic`. This function returns self.
|
|
#[must_use]
|
|
pub fn with_dynamic<C: ComponentDefinition>(
|
|
mut self,
|
|
name: &C,
|
|
dynamic: impl IntoDynamicComponentValue,
|
|
) -> Self {
|
|
self.insert_dynamic(name, dynamic);
|
|
self
|
|
}
|
|
|
|
/// Returns the associated component for the given name, if found.
|
|
#[must_use]
|
|
pub fn get_with_fallback<Fallback>(
|
|
&self,
|
|
component: &impl NamedComponent,
|
|
fallback: &Fallback,
|
|
context: &WidgetContext<'_>,
|
|
) -> Fallback::ComponentType
|
|
where
|
|
Fallback: ComponentDefinition + ?Sized,
|
|
{
|
|
self.0
|
|
.components
|
|
.get(&component.name())
|
|
.or_else(|| self.0.components.get(&fallback.name()))
|
|
.and_then(|stored| Self::resolve_component(&stored.component, context))
|
|
.unwrap_or_else(|| fallback.default_value(context))
|
|
}
|
|
|
|
fn resolve_component<T>(component: &Value<Component>, context: &WidgetContext<'_>) -> Option<T>
|
|
where
|
|
T: ComponentType,
|
|
{
|
|
let mut resolved = component.get();
|
|
loop {
|
|
match T::try_from_component(resolved) {
|
|
Ok(value) => {
|
|
if value.requires_invalidation() {
|
|
component.invalidate_when_changed(context);
|
|
} else {
|
|
component.redraw_when_changed(context);
|
|
}
|
|
break Some(value);
|
|
}
|
|
Err(Component::Dynamic(dynamic)) => {
|
|
let Some(new_component) = dynamic.resolve(context) else {
|
|
break None;
|
|
};
|
|
resolved = new_component;
|
|
}
|
|
Err(_) => break None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the component associated with the given name, if a value is
|
|
/// specified.
|
|
#[must_use]
|
|
pub fn try_get<Named>(
|
|
&self,
|
|
component: &Named,
|
|
context: &WidgetContext<'_>,
|
|
) -> Option<Named::ComponentType>
|
|
where
|
|
Named: ComponentDefinition + ?Sized,
|
|
{
|
|
self.0
|
|
.components
|
|
.get(&component.name())
|
|
.and_then(|stored| Self::resolve_component(&stored.component, context))
|
|
}
|
|
|
|
/// 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,
|
|
{
|
|
self.try_get(component, context)
|
|
.unwrap_or_else(|| component.default_value(context))
|
|
}
|
|
|
|
/// Inserts all components from `other`, overwriting any existing entries
|
|
/// with the same [`ComponentName`].
|
|
pub fn inherit_from(&mut self, other: Styles) {
|
|
for (name, mut value) in Arc::try_unwrap(other.0)
|
|
.unwrap_or_else(|err| err.as_ref().clone())
|
|
.components
|
|
{
|
|
if !value.inheritable || self.0.components.contains_key(&name) {
|
|
continue;
|
|
}
|
|
|
|
value.inherited = true;
|
|
self.insert_named(name, value);
|
|
}
|
|
}
|
|
|
|
/// Returns this collection of styles without any local style definitions.
|
|
#[must_use]
|
|
pub fn into_inherited(self) -> Self {
|
|
if self.0.components.values().any(|stored| !stored.inheritable) {
|
|
Self(Arc::new(StyleData {
|
|
components: Arc::try_unwrap(self.0)
|
|
.unwrap_or_else(|err| err.as_ref().clone())
|
|
.components
|
|
.into_iter()
|
|
.filter(|(_, stored)| stored.inheritable)
|
|
.collect(),
|
|
}))
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for Styles {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let mut map = f.debug_struct("Styles");
|
|
let mut component_name = String::new();
|
|
for (name, stored) in &self.0.components {
|
|
component_name.clear();
|
|
write!(&mut component_name, "{name:?}")?;
|
|
|
|
map.field(&component_name, &stored.component);
|
|
}
|
|
map.finish()
|
|
}
|
|
}
|
|
|
|
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 = StylesIntoIter;
|
|
type Item = (ComponentName, Value<Component>);
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
StylesIntoIter {
|
|
into_iter: Arc::try_unwrap(self.0)
|
|
.unwrap_or_else(|err| err.as_ref().clone())
|
|
.components
|
|
.into_iter(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An iterator that returns the contents of a [`Styles`] collection.
|
|
pub struct StylesIntoIter {
|
|
into_iter: hash_map::IntoIter<ComponentName, StoredComponent>,
|
|
}
|
|
|
|
impl Iterator for StylesIntoIter {
|
|
type Item = (ComponentName, Value<Component>);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
self.into_iter
|
|
.next()
|
|
.map(|(name, stored)| (name, stored.component))
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
struct StyleData {
|
|
components: AHashMap<ComponentName, StoredComponent>,
|
|
}
|
|
|
|
/// A [`Component`] that is stored within a [`Styles`] collection.
|
|
#[derive(Clone)]
|
|
pub struct StoredComponent {
|
|
inherited: bool,
|
|
inheritable: bool,
|
|
component: Value<Component>,
|
|
}
|
|
|
|
impl StoredComponent {
|
|
/// Returns a new component that will not be inherited to children widgets.
|
|
pub fn local(component: impl IntoComponentValue) -> Self {
|
|
Self {
|
|
inherited: false,
|
|
inheritable: false,
|
|
component: component.into_component_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>;
|
|
}
|
|
|
|
/// A type that can be converted into a [`StoredComponent`].
|
|
pub trait IntoStoredComponent {
|
|
/// Returns this value as a stored component.
|
|
fn into_stored_component(self) -> StoredComponent;
|
|
}
|
|
|
|
impl<T> IntoStoredComponent for T
|
|
where
|
|
T: IntoComponentValue,
|
|
{
|
|
fn into_stored_component(self) -> StoredComponent {
|
|
StoredComponent {
|
|
inherited: false,
|
|
inheritable: true,
|
|
component: self.into_component_value(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IntoStoredComponent for StoredComponent {
|
|
fn into_stored_component(self) -> StoredComponent {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<T> IntoComponentValue for T
|
|
where
|
|
T: Into<Component>,
|
|
{
|
|
fn into_component_value(self) -> Value<Component> {
|
|
Value::Constant(self.into())
|
|
}
|
|
}
|
|
|
|
impl<T> IntoComponentValue for Value<T>
|
|
where
|
|
T: Clone + Send + 'static,
|
|
Component: From<T>,
|
|
{
|
|
fn into_component_value(self) -> Value<Component> {
|
|
self.map_each(|v| Component::from(v.clone()))
|
|
}
|
|
}
|
|
|
|
impl<T> IntoComponentValue for Dynamic<T>
|
|
where
|
|
T: Clone + Send + 'static,
|
|
Component: From<T>,
|
|
{
|
|
fn into_component_value(self) -> Value<Component> {
|
|
Value::Dynamic(self.map_each_into())
|
|
}
|
|
}
|
|
|
|
/// A type that can convert into a [`Value`] containing a [`DynamicComponent`].
|
|
pub trait IntoDynamicComponentValue {
|
|
/// Returns this type converted into a dynamic component value.
|
|
fn into_dynamic_component(self) -> Value<DynamicComponent>;
|
|
}
|
|
|
|
impl IntoDynamicComponentValue for DynamicComponent {
|
|
fn into_dynamic_component(self) -> Value<DynamicComponent> {
|
|
Value::Constant(self)
|
|
}
|
|
}
|
|
|
|
impl<T> IntoDynamicComponentValue for T
|
|
where
|
|
T: ComponentDefinition + Clone + Send + Sync + 'static,
|
|
{
|
|
fn into_dynamic_component(self) -> Value<DynamicComponent> {
|
|
Value::Constant(DynamicComponent::from(self))
|
|
}
|
|
}
|
|
|
|
impl<T> IntoDynamicComponentValue for Dynamic<T>
|
|
where
|
|
T: ComponentDefinition + Clone + Send + Sync + 'static,
|
|
{
|
|
fn into_dynamic_component(self) -> Value<DynamicComponent> {
|
|
Value::Dynamic(self.map_each_into())
|
|
}
|
|
}
|
|
|
|
/// A value of a style component.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
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 font family.
|
|
FontFamily(FamilyOwned),
|
|
/// The weight (boldness) of a font.
|
|
FontWeight(Weight),
|
|
/// The style of a font.
|
|
FontStyle(Style),
|
|
/// A string value.
|
|
String(CowString),
|
|
|
|
/// A custom component type.
|
|
Custom(CustomComponent),
|
|
|
|
/// This component should use the associated value in the named class.
|
|
Dynamic(DynamicComponent),
|
|
}
|
|
|
|
impl Component {
|
|
/// Returns a [`CustomComponent`] created from `component`.
|
|
///
|
|
/// Custom components allow storing nearly any type in the style system.
|
|
pub fn custom<T>(component: T) -> Self
|
|
where
|
|
T: RequireInvalidation + Debug + Send + Sync + 'static,
|
|
{
|
|
Self::Custom(CustomComponent::new(component))
|
|
}
|
|
|
|
/// Returns a new [`DynamicComponent`] which allows resolving a component at
|
|
/// runtime.
|
|
#[must_use]
|
|
pub fn dynamic<T, Func>(resolve: Func) -> Self
|
|
where
|
|
Func:
|
|
for<'a, 'context> Fn(&'a WidgetContext<'context>) -> Option<T> + Send + Sync + 'static,
|
|
T: ComponentType,
|
|
{
|
|
Self::Dynamic(DynamicComponent::new(move |context| {
|
|
resolve(context).map(T::into_component)
|
|
}))
|
|
}
|
|
}
|
|
|
|
macro_rules! impl_component_from_string {
|
|
($type:ty) => {
|
|
impl From<$type> for Component {
|
|
fn from(s: $type) -> Self {
|
|
Self::String(s.into())
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
impl_component_from_string!(String);
|
|
impl_component_from_string!(CowString);
|
|
impl_component_from_string!(&'_ str);
|
|
|
|
macro_rules! impl_component_try_from_string {
|
|
($type:ty) => {
|
|
impl TryFrom<Component> for $type {
|
|
type Error = Component;
|
|
|
|
fn try_from(s: Component) -> Result<Self, Self::Error> {
|
|
match s {
|
|
Component::String(s) => Ok(s.into()),
|
|
other => Err(other),
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
impl_component_try_from_string!(String);
|
|
impl_component_try_from_string!(CowString);
|
|
|
|
impl From<FamilyOwned> for Component {
|
|
fn from(value: FamilyOwned) -> Self {
|
|
Self::FontFamily(value)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Component> for FamilyOwned {
|
|
type Error = Component;
|
|
|
|
fn try_from(value: Component) -> Result<Self, Self::Error> {
|
|
match value {
|
|
Component::FontFamily(family) => Ok(family),
|
|
other => Err(other),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for FamilyOwned {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl From<Weight> for Component {
|
|
fn from(value: Weight) -> Self {
|
|
Self::FontWeight(value)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Component> for Weight {
|
|
type Error = Component;
|
|
|
|
fn try_from(value: Component) -> Result<Self, Self::Error> {
|
|
match value {
|
|
Component::FontWeight(weight) => Ok(weight),
|
|
other => Err(other),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for Weight {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl From<Style> for Component {
|
|
fn from(value: Style) -> Self {
|
|
Self::FontStyle(value)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Component> for Style {
|
|
type Error = Component;
|
|
|
|
fn try_from(value: Component) -> Result<Self, Self::Error> {
|
|
match value {
|
|
Component::FontStyle(style) => Ok(style),
|
|
other => Err(other),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for Style {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
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 RequireInvalidation for Color {
|
|
fn requires_invalidation(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
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 RequireInvalidation for Dimension {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
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 RequireInvalidation for Px {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for Lp {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl<Unit> From<CornerRadii<Unit>> for Component
|
|
where
|
|
Dimension: From<Unit>,
|
|
Unit: Debug + Send + Sync + 'static,
|
|
{
|
|
fn from(radii: CornerRadii<Unit>) -> Self {
|
|
let radii = CornerRadii {
|
|
top_left: Dimension::from(radii.top_left),
|
|
top_right: Dimension::from(radii.top_right),
|
|
bottom_right: Dimension::from(radii.bottom_right),
|
|
bottom_left: Dimension::from(radii.bottom_left),
|
|
};
|
|
Component::custom(radii)
|
|
}
|
|
}
|
|
|
|
impl<Unit> RequireInvalidation for CornerRadii<Unit> {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Component> for CornerRadii<Dimension> {
|
|
type Error = Component;
|
|
|
|
fn try_from(value: Component) -> Result<Self, Self::Error> {
|
|
match value {
|
|
Component::Custom(custom) => custom
|
|
.downcast()
|
|
.copied()
|
|
.ok_or_else(|| Component::Custom(custom)),
|
|
other => Err(other),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A 1-dimensional measurement that may be automatically calculated.
|
|
#[derive(Clone, Copy)]
|
|
pub enum FlexibleDimension {
|
|
/// Automatically calculate this dimension.
|
|
Auto,
|
|
/// Use this dimension.
|
|
Dimension(Dimension),
|
|
}
|
|
|
|
impl Zero for FlexibleDimension {
|
|
const ZERO: Self = Self::Dimension(Dimension::ZERO);
|
|
|
|
fn is_zero(&self) -> bool {
|
|
match self {
|
|
FlexibleDimension::Auto => false,
|
|
FlexibleDimension::Dimension(dim) => dim.is_zero(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for FlexibleDimension {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Auto => f.write_str("Auto"),
|
|
Self::Dimension(arg0) => Debug::fmt(arg0, f),
|
|
}
|
|
}
|
|
}
|
|
|
|
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(Clone, Copy, Eq, PartialEq)]
|
|
pub enum Dimension {
|
|
/// Physical Pixels
|
|
Px(Px),
|
|
/// Logical Pixels
|
|
Lp(Lp),
|
|
}
|
|
|
|
impl Debug for Dimension {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Px(arg0) => Debug::fmt(arg0, f),
|
|
Self::Lp(arg0) => Debug::fmt(arg0, f),
|
|
}
|
|
}
|
|
}
|
|
|
|
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 Zero for Dimension {
|
|
const ZERO: Self = Dimension::Px(Px::ZERO);
|
|
|
|
fn is_zero(&self) -> bool {
|
|
match self {
|
|
Dimension::Px(x) => x.is_zero(),
|
|
Dimension::Lp(x) => x.is_zero(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ScreenScale for Dimension {
|
|
type Lp = Lp;
|
|
type Px = Px;
|
|
type UPx = UPx;
|
|
|
|
fn into_px(self, scale: figures::Fraction) -> Px {
|
|
match self {
|
|
Dimension::Px(px) => px,
|
|
Dimension::Lp(lp) => lp.into_px(scale),
|
|
}
|
|
}
|
|
|
|
fn from_px(px: Px, _scale: figures::Fraction) -> Self {
|
|
Self::from(px)
|
|
}
|
|
|
|
fn into_lp(self, scale: figures::Fraction) -> Lp {
|
|
match self {
|
|
Dimension::Px(px) => px.into_lp(scale),
|
|
Dimension::Lp(lp) => lp,
|
|
}
|
|
}
|
|
|
|
fn from_lp(lp: Lp, _scale: figures::Fraction) -> Self {
|
|
Self::from(lp)
|
|
}
|
|
|
|
fn into_upx(self, scale: Fraction) -> Self::UPx {
|
|
match self {
|
|
Dimension::Px(px) => px.into_unsigned(),
|
|
Dimension::Lp(lp) => lp.into_upx(scale),
|
|
}
|
|
}
|
|
|
|
fn from_upx(px: Self::UPx, _scale: Fraction) -> Self {
|
|
Self::from(px.into_signed())
|
|
}
|
|
}
|
|
|
|
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 Default for DimensionRange {
|
|
fn default() -> Self {
|
|
Self {
|
|
start: Bound::Unbounded,
|
|
end: Bound::Unbounded,
|
|
}
|
|
}
|
|
}
|
|
|
|
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_upx(scale));
|
|
}
|
|
if let Some(max) = self.maximum() {
|
|
size = size.min(max.into_upx(scale));
|
|
}
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for DimensionRange {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// 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: RequireInvalidation + 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 PartialEq for CustomComponent {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
Arc::ptr_eq(&self.0, &other.0)
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for CustomComponent {
|
|
fn requires_invalidation(&self) -> bool {
|
|
self.0.requires_invalidation()
|
|
}
|
|
}
|
|
|
|
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: RequireInvalidation + Send + Sync + Debug {
|
|
fn as_any(&self) -> &dyn Any;
|
|
}
|
|
|
|
impl<T> AnyComponent for T
|
|
where
|
|
T: RequireInvalidation + Debug + Send + Sync + 'static,
|
|
{
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
/// A fully-qualified style component name.
|
|
#[derive(Clone, Eq, PartialEq, 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 Debug for ComponentName {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{:?}.{:?}", &self.group, &self.name)
|
|
}
|
|
}
|
|
|
|
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: Sized {
|
|
/// 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;
|
|
}
|
|
|
|
/// Describes whether a type should invalidate a widget.
|
|
pub trait RequireInvalidation {
|
|
/// Cushy tracks two different states:
|
|
///
|
|
/// - Whether to repaint the window
|
|
/// - Whether to relayout a widget
|
|
///
|
|
/// If a value change of `self` may require a relayout, this should return
|
|
/// true.
|
|
fn requires_invalidation(&self) -> bool;
|
|
}
|
|
|
|
/// A type that can be converted to and from [`Component`].
|
|
pub trait ComponentType: RequireInvalidation + 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: RequireInvalidation + 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)
|
|
}
|
|
}
|
|
|
|
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> Round for Edges<T>
|
|
where
|
|
T: Round,
|
|
{
|
|
fn round(self) -> Self {
|
|
self.map(Round::round)
|
|
}
|
|
|
|
fn ceil(self) -> Self {
|
|
self.map(Round::ceil)
|
|
}
|
|
|
|
fn floor(self) -> Self {
|
|
self.map(Round::floor)
|
|
}
|
|
}
|
|
|
|
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<Unit> Zero for Edges<Unit>
|
|
where
|
|
Unit: Zero,
|
|
{
|
|
const ZERO: Self = Self {
|
|
left: Unit::ZERO,
|
|
top: Unit::ZERO,
|
|
right: Unit::ZERO,
|
|
bottom: Unit::ZERO,
|
|
};
|
|
|
|
fn is_zero(&self) -> bool {
|
|
self.left.is_zero() && self.top.is_zero() && self.right.is_zero() && self.bottom.is_zero()
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
impl IntoValue<Dimension> for Px {
|
|
fn into_value(self) -> Value<Dimension> {
|
|
Dimension::from(self).into_value()
|
|
}
|
|
}
|
|
|
|
impl IntoValue<Dimension> for Lp {
|
|
fn into_value(self) -> Value<Dimension> {
|
|
Dimension::from(self).into_value()
|
|
}
|
|
}
|
|
|
|
impl IntoValue<FlexibleDimension> for Px {
|
|
fn into_value(self) -> Value<FlexibleDimension> {
|
|
Dimension::from(self).into_value()
|
|
}
|
|
}
|
|
|
|
impl IntoValue<FlexibleDimension> for Lp {
|
|
fn into_value(self) -> Value<FlexibleDimension> {
|
|
Dimension::from(self).into_value()
|
|
}
|
|
}
|
|
|
|
impl IntoValue<FlexibleDimension> for Dimension {
|
|
fn into_value(self) -> Value<FlexibleDimension> {
|
|
FlexibleDimension::from(self).into_value()
|
|
}
|
|
}
|
|
|
|
impl<U> ScreenScale for Edges<U>
|
|
where
|
|
U: ScreenScale<Px = Px, UPx = UPx, Lp = Lp>,
|
|
{
|
|
type Lp = Edges<Lp>;
|
|
type Px = Edges<Px>;
|
|
type UPx = Edges<UPx>;
|
|
|
|
fn into_px(self, scale: Fraction) -> Self::Px {
|
|
Edges {
|
|
left: self.left.into_px(scale),
|
|
top: self.top.into_px(scale),
|
|
right: self.right.into_px(scale),
|
|
bottom: self.bottom.into_px(scale),
|
|
}
|
|
}
|
|
|
|
fn from_px(px: Self::Px, scale: Fraction) -> Self {
|
|
Self {
|
|
left: U::from_px(px.left, scale),
|
|
top: U::from_px(px.top, scale),
|
|
right: U::from_px(px.right, scale),
|
|
bottom: U::from_px(px.bottom, scale),
|
|
}
|
|
}
|
|
|
|
fn into_upx(self, scale: Fraction) -> Self::UPx {
|
|
Edges {
|
|
left: self.left.into_upx(scale),
|
|
top: self.top.into_upx(scale),
|
|
right: self.right.into_upx(scale),
|
|
bottom: self.bottom.into_upx(scale),
|
|
}
|
|
}
|
|
|
|
fn from_upx(px: Self::UPx, scale: Fraction) -> Self {
|
|
Self {
|
|
left: U::from_upx(px.left, scale),
|
|
top: U::from_upx(px.top, scale),
|
|
right: U::from_upx(px.right, scale),
|
|
bottom: U::from_upx(px.bottom, scale),
|
|
}
|
|
}
|
|
|
|
fn into_lp(self, scale: Fraction) -> Self::Lp {
|
|
Edges {
|
|
left: self.left.into_lp(scale),
|
|
top: self.top.into_lp(scale),
|
|
right: self.right.into_lp(scale),
|
|
bottom: self.bottom.into_lp(scale),
|
|
}
|
|
}
|
|
|
|
fn from_lp(lp: Self::Lp, scale: Fraction) -> Self {
|
|
Self {
|
|
left: U::from_lp(lp.left, scale),
|
|
top: U::from_lp(lp.top, scale),
|
|
right: U::from_lp(lp.right, scale),
|
|
bottom: U::from_lp(lp.bottom, scale),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<U, R> Add for Edges<U>
|
|
where
|
|
U: Add<Output = R>,
|
|
{
|
|
type Output = Edges<R>;
|
|
|
|
fn add(self, rhs: Self) -> Self::Output {
|
|
Edges {
|
|
left: self.left + rhs.left,
|
|
top: self.top + rhs.top,
|
|
right: self.right + rhs.right,
|
|
bottom: self.bottom + rhs.bottom,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<U, R> AddAssign<Edges<R>> for Edges<U>
|
|
where
|
|
U: AddAssign<R>,
|
|
{
|
|
fn add_assign(&mut self, rhs: Edges<R>) {
|
|
self.left += rhs.left;
|
|
self.top += rhs.top;
|
|
self.right += rhs.right;
|
|
self.bottom += rhs.bottom;
|
|
}
|
|
}
|
|
|
|
/// A set of light and dark [`Theme`]s.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
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_scheme(scheme: &ColorScheme) -> Self {
|
|
Self {
|
|
light: Theme::light_from_sources(
|
|
scheme.primary,
|
|
scheme.secondary,
|
|
scheme.tertiary,
|
|
scheme.error,
|
|
scheme.neutral,
|
|
scheme.neutral_variant,
|
|
),
|
|
dark: Theme::dark_from_sources(
|
|
scheme.primary,
|
|
scheme.secondary,
|
|
scheme.tertiary,
|
|
scheme.error,
|
|
scheme.neutral,
|
|
scheme.neutral_variant,
|
|
),
|
|
primary_fixed: FixedTheme::from_source(scheme.primary),
|
|
secondary_fixed: FixedTheme::from_source(scheme.secondary),
|
|
tertiary_fixed: FixedTheme::from_source(scheme.tertiary),
|
|
scrim: scheme.neutral.color(1),
|
|
shadow: scheme.neutral.color(1),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ColorScheme> for ThemePair {
|
|
fn from(scheme: ColorScheme) -> Self {
|
|
Self::from_scheme(&scheme)
|
|
}
|
|
}
|
|
|
|
impl Default for ThemePair {
|
|
fn default() -> Self {
|
|
Self::from(ColorScheme::default())
|
|
}
|
|
}
|
|
|
|
/// A Cushy 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 primary color, brightened for highlighting content.
|
|
pub color_bright: 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(20),
|
|
color_bright: source.color(45),
|
|
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),
|
|
color_dim: source.color(60),
|
|
color_bright: source.color(85),
|
|
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 PartialEq for ColorSource {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
(self.hue.into_degrees() - other.hue.into_degrees()).abs() < f32::EPSILON
|
|
&& self.saturation == other.saturation
|
|
}
|
|
}
|
|
|
|
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 average_saturation = ZeroToOne::new((*self.saturation + *other.saturation) / 2.);
|
|
|
|
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.,
|
|
);
|
|
|
|
ZeroToOne::new((*saturation_delta + *hue_delta * *average_saturation) / 2.)
|
|
}
|
|
}
|
|
|
|
/// 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_hsla(self) -> Hsla;
|
|
|
|
/// Returns the hue and saturation of this color.
|
|
fn source(self) -> ColorSource {
|
|
self.into_hsla().hsl.source
|
|
}
|
|
|
|
/// Returns the perceived lightness of this color.
|
|
#[must_use]
|
|
fn lightness(self) -> ZeroToOne {
|
|
self.into_hsla().hsl.lightness
|
|
}
|
|
|
|
/// 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_hsla(self) -> Hsla {
|
|
let mut hsl: palette::Okhsl =
|
|
Srgb::new(self.red_f32(), self.green_f32(), self.blue_f32()).into_color();
|
|
|
|
if hsl.saturation.is_nan() && self.red() == 255 && self.green() == 255 && self.blue() == 255
|
|
{
|
|
// This works around a calculation causing NaN in the saturation
|
|
// field when pure white is converted:
|
|
// <https://github.com/Ogeon/palette/issues/368>
|
|
hsl.saturation = 0.0;
|
|
}
|
|
|
|
Hsla {
|
|
hsl: Hsl {
|
|
source: ColorSource {
|
|
hue: hsl.hue,
|
|
saturation: ZeroToOne::new(hsl.saturation),
|
|
},
|
|
lightness: ZeroToOne::new(hsl.lightness),
|
|
},
|
|
alpha: ZeroToOne::new(self.alpha_f32()),
|
|
}
|
|
}
|
|
|
|
fn contrast_between(
|
|
self,
|
|
check_source: ColorSource,
|
|
check_lightness: ZeroToOne,
|
|
check_alpha: ZeroToOne,
|
|
) -> ZeroToOne {
|
|
let other = self.into_hsla();
|
|
let lightness_delta = other.hsl.lightness.difference_between(check_lightness);
|
|
|
|
let source_change = check_source.contrast_between(other.hsl.source);
|
|
|
|
let alpha_delta = check_alpha.difference_between(other.alpha);
|
|
|
|
// The lightness amount is the most important factor in contrast
|
|
// calculations. Thus, we give a higher weight to it in our average
|
|
// calculation.
|
|
ZeroToOne::new((*lightness_delta * 3. + *source_change + *alpha_delta) / 5.)
|
|
}
|
|
|
|
fn most_contrasting(self, others: &[Self]) -> Self
|
|
where
|
|
Self: Copy,
|
|
{
|
|
let check = self.into_hsla();
|
|
|
|
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.hsl.source, check.hsl.lightness, check.alpha);
|
|
for other in others {
|
|
let contrast_amount =
|
|
other.contrast_between(check.hsl.source, check.hsl.lightness, check.alpha);
|
|
if contrast_amount > most_contrast_amount {
|
|
most_contrasting = other;
|
|
most_contrast_amount = contrast_amount;
|
|
}
|
|
}
|
|
|
|
most_contrasting
|
|
}
|
|
}
|
|
|
|
/// A color composed of hue, saturation, and lightness.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Hsla {
|
|
/// The hue, saturation, and lightness of this color.
|
|
pub hsl: Hsl,
|
|
/// The alpha of this color.
|
|
pub alpha: ZeroToOne,
|
|
}
|
|
|
|
impl From<Color> for Hsla {
|
|
fn from(value: Color) -> Self {
|
|
value.into_hsla()
|
|
}
|
|
}
|
|
|
|
impl From<Hsla> for Color {
|
|
fn from(value: Hsla) -> Self {
|
|
Color::from(value.hsl).with_alpha_f32(*value.alpha)
|
|
}
|
|
}
|
|
|
|
/// A color composed of hue, saturation, and lightness.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Hsl {
|
|
/// The hue and saturation of this color.
|
|
pub source: ColorSource,
|
|
/// The lightness of this color.
|
|
pub lightness: ZeroToOne,
|
|
}
|
|
|
|
impl From<Color> for Hsl {
|
|
fn from(value: Color) -> Self {
|
|
value.into_hsla().hsl
|
|
}
|
|
}
|
|
|
|
impl From<Hsl> for Color {
|
|
fn from(value: Hsl) -> Self {
|
|
value.source.color(value.lightness)
|
|
}
|
|
}
|
|
|
|
/// 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),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for VisualOrder {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// 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),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for FocusableWidgets {
|
|
fn requires_invalidation(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// 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),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for ContainerLevel {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// A builder of [`ColorScheme`]s.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct ColorSchemeBuilder {
|
|
/// The primary color of the scheme.
|
|
pub primary: ColorSource,
|
|
/// The secondary color of the scheme. If not provided, a complimentary
|
|
/// color will be chosen.
|
|
pub secondary: Option<ColorSource>,
|
|
/// The tertiary color of the scheme. If not provided, a complimentary
|
|
/// color will be chosen.
|
|
pub tertiary: Option<ColorSource>,
|
|
/// The error color of the scheme. If not provided, red will be used unless
|
|
/// it contrasts poorly with any of the other colors.
|
|
pub error: Option<ColorSource>,
|
|
/// The neutral color of the scheme. If not provided, a nearly fully
|
|
/// desaturated variation of the primary color will be used.
|
|
pub neutral: Option<ColorSource>,
|
|
/// The neutral variant color of the scheme. If not provided, a mostly
|
|
/// desaturated variation of the primary color will be used.
|
|
pub neutral_variant: Option<ColorSource>,
|
|
hue_shift: OklabHue,
|
|
}
|
|
|
|
impl ColorSchemeBuilder {
|
|
/// Returns a builder for the provided primary color.
|
|
#[must_use]
|
|
pub fn new(primary: impl ProtoColor) -> Self {
|
|
Self {
|
|
primary: primary.into_source(ZeroToOne::new(0.8)),
|
|
secondary: None,
|
|
tertiary: None,
|
|
error: None,
|
|
neutral: None,
|
|
neutral_variant: None,
|
|
hue_shift: OklabHue::new(30.),
|
|
}
|
|
}
|
|
|
|
fn generate_secondary(&self) -> ColorSource {
|
|
ColorSource {
|
|
hue: self.primary.hue + self.hue_shift,
|
|
saturation: self.primary.saturation / 2.,
|
|
}
|
|
}
|
|
|
|
fn generate_tertiary(&self, secondary: ColorSource) -> ColorSource {
|
|
let hue_shift = (secondary.hue - self.primary.hue).into_degrees().signum()
|
|
* self.hue_shift.into_degrees();
|
|
ColorSource {
|
|
hue: self.primary.hue - hue_shift,
|
|
saturation: self.primary.saturation / 3.,
|
|
}
|
|
}
|
|
|
|
fn generate_error(&self, secondary: ColorSource, tertiary: ColorSource) -> ColorSource {
|
|
let mut error = ColorSource::new(30., self.primary.saturation);
|
|
let shift_degrees = self.hue_shift.into_positive_degrees().ceil().cast::<u32>();
|
|
let mut iters_left = (360 - (shift_degrees - 1)) / shift_degrees;
|
|
while iters_left > 0
|
|
&& [self.primary, secondary, tertiary]
|
|
.iter()
|
|
.any(|c| c.contrast_between(error) < 0.20)
|
|
{
|
|
error.hue -= self.hue_shift;
|
|
iters_left -= 1;
|
|
}
|
|
|
|
error
|
|
}
|
|
|
|
fn generate_neutral(&self) -> ColorSource {
|
|
ColorSource {
|
|
hue: self.primary.hue,
|
|
saturation: ZeroToOne::new(0.01),
|
|
}
|
|
}
|
|
|
|
fn generate_neutral_variant(&self) -> ColorSource {
|
|
ColorSource {
|
|
hue: self.primary.hue,
|
|
saturation: self.primary.saturation / 10.,
|
|
}
|
|
}
|
|
|
|
/// Sets the secondary color and returns self.
|
|
///
|
|
/// If `secondary` doesn't specify a saturation, a saturation value that is
|
|
/// 50% of the primary saturation will be picked.
|
|
#[must_use]
|
|
pub fn secondary(mut self, secondary: impl ProtoColor) -> Self {
|
|
self.secondary = Some(secondary.into_source(self.primary.saturation / 2.));
|
|
self
|
|
}
|
|
|
|
/// Sets the tertiary color and returns self.
|
|
///
|
|
/// If `tertiary` doesn't specify a saturation, a saturation value that is
|
|
/// 33% of the primary saturation will be picked.
|
|
#[must_use]
|
|
pub fn tertiary(mut self, tertiary: impl ProtoColor) -> Self {
|
|
self.tertiary = Some(tertiary.into_source(self.primary.saturation / 3.));
|
|
self
|
|
}
|
|
|
|
/// Sets the error color and returns self.
|
|
///
|
|
/// If `error` doesn't specify a saturation, the primary color's saturation
|
|
/// will be used.
|
|
#[must_use]
|
|
pub fn error(mut self, error: impl ProtoColor) -> Self {
|
|
self.error = Some(error.into_source(self.primary.saturation));
|
|
self
|
|
}
|
|
|
|
/// Sets the neutral color and returns self.
|
|
///
|
|
/// If `neutral` doesn't specify a saturation, a saturation of 1%.
|
|
#[must_use]
|
|
pub fn neutral(mut self, neutral: impl ProtoColor) -> Self {
|
|
self.neutral = Some(neutral.into_source(0.01));
|
|
self
|
|
}
|
|
|
|
/// Sets the neutral color and returns self.
|
|
///
|
|
/// If `neutral_variant` doesn't specify a saturation, a saturation value
|
|
/// that is 10% of the primary saturation will be picked.
|
|
#[must_use]
|
|
pub fn neutral_variant(mut self, neutral_variant: impl ProtoColor) -> Self {
|
|
self.neutral_variant = Some(neutral_variant.into_source(self.primary.saturation / 10.));
|
|
self
|
|
}
|
|
|
|
/// Sets the amount the hue component is shifted when auto-generating colors
|
|
/// to fill in the palette.
|
|
///
|
|
/// The default hue shift is 30 degrees.
|
|
#[must_use]
|
|
pub fn hue_shift(mut self, hue_shift: impl Into<OklabHue>) -> Self {
|
|
self.hue_shift = hue_shift.into();
|
|
self
|
|
}
|
|
|
|
/// Builds a color scheme from the provided colors, generating any
|
|
/// unspecified colors.
|
|
#[must_use]
|
|
pub fn build(self) -> ColorScheme {
|
|
let secondary = self.secondary.unwrap_or_else(|| self.generate_secondary());
|
|
let tertiary = self
|
|
.tertiary
|
|
.unwrap_or_else(|| self.generate_tertiary(secondary));
|
|
ColorScheme {
|
|
primary: self.primary,
|
|
secondary,
|
|
tertiary,
|
|
error: self
|
|
.error
|
|
.unwrap_or_else(|| self.generate_error(secondary, tertiary)),
|
|
neutral: self.neutral.unwrap_or_else(|| self.generate_neutral()),
|
|
neutral_variant: self
|
|
.neutral_variant
|
|
.unwrap_or_else(|| self.generate_neutral_variant()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A type that can be interpretted as a hue or hue and saturation.
|
|
pub trait ProtoColor: Sized {
|
|
/// Returns the hue of this prototype color.
|
|
#[must_use]
|
|
fn hue(&self) -> OklabHue;
|
|
/// Returns the saturation of this prototype color, if available.
|
|
#[must_use]
|
|
fn saturation(&self) -> Option<ZeroToOne>;
|
|
|
|
/// Returns a color source built from this prototype color
|
|
#[must_use]
|
|
fn into_source(self, saturation_if_not_provided: impl Into<ZeroToOne>) -> ColorSource {
|
|
let saturation = self
|
|
.saturation()
|
|
.unwrap_or_else(|| saturation_if_not_provided.into());
|
|
ColorSource::new(self.hue(), saturation)
|
|
}
|
|
}
|
|
|
|
impl<'a> ProtoColor for &'a ColorSource {
|
|
fn hue(&self) -> OklabHue {
|
|
self.hue
|
|
}
|
|
|
|
fn saturation(&self) -> Option<ZeroToOne> {
|
|
Some(self.saturation)
|
|
}
|
|
}
|
|
|
|
impl ProtoColor for f32 {
|
|
fn hue(&self) -> OklabHue {
|
|
(*self).into()
|
|
}
|
|
|
|
fn saturation(&self) -> Option<ZeroToOne> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl ProtoColor for OklabHue {
|
|
fn hue(&self) -> OklabHue {
|
|
*self
|
|
}
|
|
|
|
fn saturation(&self) -> Option<ZeroToOne> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl ProtoColor for ColorSource {
|
|
fn hue(&self) -> OklabHue {
|
|
self.hue
|
|
}
|
|
|
|
fn saturation(&self) -> Option<ZeroToOne> {
|
|
Some(self.saturation)
|
|
}
|
|
}
|
|
|
|
impl<Hue, Saturation> ProtoColor for (Hue, Saturation)
|
|
where
|
|
Hue: Into<OklabHue> + Copy,
|
|
Saturation: Into<ZeroToOne> + Copy,
|
|
{
|
|
fn hue(&self) -> OklabHue {
|
|
self.0.into()
|
|
}
|
|
|
|
fn saturation(&self) -> Option<ZeroToOne> {
|
|
Some(self.1.into())
|
|
}
|
|
}
|
|
|
|
/// A color scheme for a Cushy application.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct ColorScheme {
|
|
/// The primary accent color.
|
|
pub primary: ColorSource,
|
|
/// A secondary accent color.
|
|
pub secondary: ColorSource,
|
|
/// A tertiary accent color.
|
|
pub tertiary: ColorSource,
|
|
/// A color used to denote errors.
|
|
pub error: ColorSource,
|
|
/// A neutral color.
|
|
pub neutral: ColorSource,
|
|
/// A neutral color with a different tone than `neutral`.
|
|
pub neutral_variant: ColorSource,
|
|
}
|
|
|
|
impl ColorScheme {
|
|
/// Returns a generated color scheme based on a `primary` color.
|
|
#[must_use]
|
|
pub fn from_primary(primary: impl ProtoColor) -> Self {
|
|
ColorSchemeBuilder::new(primary).build()
|
|
}
|
|
}
|
|
|
|
impl Default for ColorScheme {
|
|
fn default() -> Self {
|
|
Self::from_primary(138.5)
|
|
}
|
|
}
|
|
|
|
impl From<ColorSource> for ColorScheme {
|
|
fn from(primary: ColorSource) -> Self {
|
|
ColorScheme::from_primary(primary)
|
|
}
|
|
}
|
|
|
|
/// A list of font families.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct FontFamilyList(Arc<Vec<FamilyOwned>>);
|
|
|
|
impl Default for FontFamilyList {
|
|
fn default() -> Self {
|
|
static DEFAULT: Lazy<FontFamilyList> = Lazy::new(|| FontFamilyList::from(vec![]));
|
|
DEFAULT.clone()
|
|
}
|
|
}
|
|
|
|
impl FontFamilyList {
|
|
/// Pushes `family` on the end of this list.
|
|
pub fn push(&mut self, family: FamilyOwned) {
|
|
Arc::make_mut(&mut self.0).push(family);
|
|
}
|
|
}
|
|
|
|
impl Deref for FontFamilyList {
|
|
type Target = [FamilyOwned];
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl FromIterator<FamilyOwned> for FontFamilyList {
|
|
fn from_iter<T: IntoIterator<Item = FamilyOwned>>(iter: T) -> Self {
|
|
Self(Arc::new(iter.into_iter().collect()))
|
|
}
|
|
}
|
|
|
|
impl From<FamilyOwned> for FontFamilyList {
|
|
fn from(value: FamilyOwned) -> Self {
|
|
Self::from(vec![value])
|
|
}
|
|
}
|
|
|
|
impl From<Vec<FamilyOwned>> for FontFamilyList {
|
|
fn from(value: Vec<FamilyOwned>) -> Self {
|
|
Self(Arc::new(value))
|
|
}
|
|
}
|
|
|
|
impl IntoValue<FontFamilyList> for FamilyOwned {
|
|
fn into_value(self) -> Value<FontFamilyList> {
|
|
FontFamilyList::from(self).into_value()
|
|
}
|
|
}
|
|
|
|
impl IntoValue<FontFamilyList> for Vec<FamilyOwned> {
|
|
fn into_value(self) -> Value<FontFamilyList> {
|
|
FontFamilyList::from(self).into_value()
|
|
}
|
|
}
|
|
|
|
impl From<FontFamilyList> for Component {
|
|
fn from(list: FontFamilyList) -> Self {
|
|
Component::custom(list)
|
|
}
|
|
}
|
|
|
|
impl RequireInvalidation for FontFamilyList {
|
|
fn requires_invalidation(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Component> for FontFamilyList {
|
|
type Error = Component;
|
|
|
|
fn try_from(value: Component) -> Result<Self, Self::Error> {
|
|
match value {
|
|
Component::Custom(custom) => custom
|
|
.downcast()
|
|
.cloned()
|
|
.ok_or_else(|| Component::Custom(custom)),
|
|
other => Err(other),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A [`Component`] that resolves its value at runtime.
|
|
#[derive(Clone)]
|
|
pub struct DynamicComponent(Arc<dyn DynamicComponentResolver>);
|
|
|
|
impl Debug for DynamicComponent {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_tuple("DynamicComponent").finish()
|
|
}
|
|
}
|
|
|
|
impl PartialEq for DynamicComponent {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
Arc::ptr_eq(&self.0, &other.0)
|
|
}
|
|
}
|
|
|
|
/// A type that resolves to a [`Component`] at runtime.
|
|
pub trait DynamicComponentResolver: Send + Sync + 'static {
|
|
/// Returns the effective component, if one should be applied.
|
|
fn resolve_component(&self, context: &WidgetContext<'_>) -> Option<Component>;
|
|
}
|
|
|
|
struct DynamicFunctionWrapper<F>(F);
|
|
|
|
impl<T> DynamicComponentResolver for DynamicFunctionWrapper<T>
|
|
where
|
|
T: for<'a, 'context> Fn(&'a WidgetContext<'context>) -> Option<Component>
|
|
+ Send
|
|
+ Sync
|
|
+ 'static,
|
|
{
|
|
fn resolve_component(&self, context: &WidgetContext<'_>) -> Option<Component> {
|
|
self.0(context)
|
|
}
|
|
}
|
|
|
|
impl<T> DynamicComponentResolver for T
|
|
where
|
|
T: ComponentDefinition + Clone + Send + Sync + 'static,
|
|
{
|
|
fn resolve_component(&self, context: &WidgetContext<'_>) -> Option<Component> {
|
|
Some(context.get(self).into_component())
|
|
}
|
|
}
|
|
|
|
impl<T> From<T> for DynamicComponent
|
|
where
|
|
T: DynamicComponentResolver,
|
|
{
|
|
fn from(resolve: T) -> Self {
|
|
Self(Arc::new(resolve))
|
|
}
|
|
}
|
|
|
|
impl DynamicComponent {
|
|
/// Returns a new dynamic component that invokes `resolve` each time it is
|
|
/// used by widgets.
|
|
#[must_use]
|
|
pub fn new<Func>(resolve: Func) -> Self
|
|
where
|
|
Func: for<'a, 'context> Fn(&'a WidgetContext<'context>) -> Option<Component>
|
|
+ Send
|
|
+ Sync
|
|
+ 'static,
|
|
{
|
|
Self::from(DynamicFunctionWrapper(resolve))
|
|
}
|
|
|
|
/// Invokes the resolver function, optionally returning a resolved
|
|
/// component.
|
|
#[must_use]
|
|
pub fn resolve(&self, context: &WidgetContext<'_>) -> Option<Component> {
|
|
self.0.resolve_component(context)
|
|
}
|
|
}
|