From 4668db398358c97db9633f127438df3b8ac96766 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 14 Nov 2023 11:27:04 -0800 Subject: [PATCH] New slider example showing min/max --- examples/slider.rs | 29 ++++++++++ src/animation.rs | 71 ++++++++++++++++++----- src/styles.rs | 141 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 208 insertions(+), 33 deletions(-) create mode 100644 examples/slider.rs diff --git a/examples/slider.rs b/examples/slider.rs new file mode 100644 index 0000000..5672c9f --- /dev/null +++ b/examples/slider.rs @@ -0,0 +1,29 @@ +use gooey::value::{Dynamic, StringValue}; +use gooey::widget::MakeWidget; +use gooey::widgets::slider::Slidable; +use gooey::Run; +use kludgine::figures::units::Lp; + +fn main() -> gooey::Result { + let min_text = Dynamic::new(u8::MIN.to_string()); + let min = min_text.map_each(|min| min.parse().unwrap_or(u8::MIN)); + let max_text = Dynamic::new(u8::MAX.to_string()); + let max = max_text.map_each(|max| max.parse().unwrap_or(u8::MAX)); + let value = Dynamic::new(128_u8); + let value_text = value.map_each(ToString::to_string); + + "Min" + .and(min_text.into_input()) + .and("Max") + .and(max_text.into_input()) + .into_columns() + .centered() + .and(value.slider_between(min, max)) + .and(value_text.centered()) + .into_rows() + .expand_horizontally() + .width(..Lp::points(800)) + .centered() + .expand() + .run() +} diff --git a/src/animation.rs b/src/animation.rs index 30cc1c0..c3e5479 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -39,6 +39,7 @@ pub mod easings; +use std::cmp::Ordering; use std::fmt::{Debug, Display}; use std::ops::{ControlFlow, Deref, Div, Mul}; use std::panic::{RefUnwindSafe, UnwindSafe}; @@ -780,9 +781,13 @@ macro_rules! impl_percent_between { impl PercentBetween for $type { fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne { assert!(min <= max, "percent_between requires min <= max"); + assert!( + self >= min && self <= max, + "self must satisfy min <= self <= max" + ); let range = *max - *min; - ZeroToOne::from(*self as $float / range as $float) + ZeroToOne::from((*self - *min) as $float / range as $float) } } }; @@ -810,20 +815,60 @@ impl PercentBetween for Color { min: Color, max: Color, func: impl Fn(Color) -> u8, - ) -> ZeroToOne { - func(value).percent_between(&func(min), &func(max)) + ) -> Option { + let value = func(value); + let min = func(min); + let max = func(max); + match min.cmp(&max) { + Ordering::Less => Some(value.percent_between(&min, &max)), + Ordering::Equal => None, + Ordering::Greater => Some(value.percent_between(&max, &min).one_minus()), + } } - ZeroToOne::new( - (*channel_percent(*self, *min, *max, Color::red) - + *channel_percent(*self, *min, *max, Color::green) - + *channel_percent(*self, *min, *max, Color::blue) - + *channel_percent(*self, *min, *max, Color::alpha)) - / 4., - ) + let mut total_percent_change = 0.; + let mut different_channels = 0_u8; + + for func in [Color::red, Color::green, Color::blue, Color::alpha] { + if let Some(red) = channel_percent(*self, *min, *max, func) { + total_percent_change += *red; + different_channels += 1; + } + } + + if different_channels > 0 { + ZeroToOne::new(total_percent_change / f32::from(different_channels)) + } else { + ZeroToOne::ZERO + } } } +#[test] +fn int_percent_between() { + assert_eq!(1_u8.percent_between(&1_u8, &2_u8), ZeroToOne::ZERO); +} + +#[test] +fn color_lerp() { + let gray = Color::new(51, 51, 51, 51); + let percent_gray = gray.percent_between(&Color::CLEAR_BLACK, &Color::WHITE); + + assert_eq!(gray, Color::CLEAR_BLACK.lerp(&Color::WHITE, *percent_gray)); + + let gray = Color::new(51, 51, 51, 255); + let percent_gray = gray.percent_between(&Color::BLACK, &Color::WHITE); + + assert_eq!(gray, Color::BLACK.lerp(&Color::WHITE, *percent_gray)); + + let red_green = Color::RED.lerp(&Color::GREEN, 0.5); + let percent_between = red_green.percent_between(&Color::RED, &Color::GREEN); + // Why 1 / 255 / 4? This operation is working on u8s, and there are 4 + // channels that can be averaged. The percent is guaranteed to be within + // this range, which works out to be 0.0098 percent. + assert!((*percent_between - 0.5).abs() < 1. / 255. / 4.); +} + /// An `f32` that is clamped between 0.0 and 1.0 and cannot be NaN or Infinity. /// /// Because of these restrictions, this type implements `Ord` and `Eq`. @@ -922,19 +967,19 @@ impl PartialEq for ZeroToOne { } impl Ord for ZeroToOne { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { + fn cmp(&self, other: &Self) -> Ordering { self.0.total_cmp(&other.0) } } impl PartialOrd for ZeroToOne { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialOrd for ZeroToOne { - fn partial_cmp(&self, other: &f32) -> Option { + fn partial_cmp(&self, other: &f32) -> Option { Some(self.0.total_cmp(other)) } } diff --git a/src/styles.rs b/src/styles.rs index 8d8c95f..7cadfea 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -1665,27 +1665,21 @@ pub struct ColorSchemeBuilder { /// 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, - hue_shift: f32, + hue_shift: OklabHue, } impl ColorSchemeBuilder { - /// Returns a builder for the provided hue, in degrees. - #[must_use] - pub fn from_hue(hue: impl Into) -> Self { - Self::new(ColorSource::new(hue, 0.8)) - } - /// Returns a builder for the provided primary color. #[must_use] - pub fn new(primary: ColorSource) -> Self { + pub fn new(primary: impl ProtoColor) -> Self { Self { - primary, + primary: primary.into_source(ZeroToOne::new(0.8)), secondary: None, tertiary: None, error: None, neutral: None, neutral_variant: None, - hue_shift: 30., + hue_shift: OklabHue::new(30.), } } @@ -1697,7 +1691,8 @@ impl ColorSchemeBuilder { } fn generate_tertiary(&self, secondary: ColorSource) -> ColorSource { - let hue_shift = (secondary.hue - self.primary.hue).into_degrees().signum() * self.hue_shift; + 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., @@ -1726,10 +1721,59 @@ impl ColorSchemeBuilder { fn generate_neutral_variant(&self) -> ColorSource { ColorSource { hue: self.primary.hue, - saturation: ZeroToOne::new(0.1), + 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.secondary = Some(tertiary.into_source(self.primary.saturation / 3.)); + 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) -> Self { + self.hue_shift = hue_shift.into(); + self + } + /// Builds a color scheme from the provided colors, generating any /// unspecified colors. #[must_use] @@ -1753,6 +1797,69 @@ impl ColorSchemeBuilder { } } +/// 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; + + /// Returns a color source built from this prototype color + #[must_use] + fn into_source(self, saturation_if_not_provided: impl Into) -> ColorSource { + let saturation = self + .saturation() + .unwrap_or_else(|| saturation_if_not_provided.into()); + ColorSource::new(self.hue(), saturation) + } +} + +impl ProtoColor for f32 { + fn hue(&self) -> OklabHue { + (*self).into() + } + + fn saturation(&self) -> Option { + None + } +} + +impl ProtoColor for OklabHue { + fn hue(&self) -> OklabHue { + *self + } + + fn saturation(&self) -> Option { + None + } +} + +impl ProtoColor for ColorSource { + fn hue(&self) -> OklabHue { + self.hue + } + + fn saturation(&self) -> Option { + Some(self.saturation) + } +} + +impl ProtoColor for (Hue, Saturation) +where + Hue: Into + Copy, + Saturation: Into + Copy, +{ + fn hue(&self) -> OklabHue { + self.0.into() + } + + fn saturation(&self) -> Option { + Some(self.1.into()) + } +} + /// A color scheme for a Gooey application. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ColorScheme { @@ -1773,20 +1880,14 @@ pub struct ColorScheme { impl ColorScheme { /// Returns a generated color scheme based on a `primary` color. #[must_use] - pub fn from_primary(primary: ColorSource) -> Self { + pub fn from_primary(primary: impl ProtoColor) -> Self { ColorSchemeBuilder::new(primary).build() } - - /// Returns a generated color scheme based on a `primary` hue, in degrees. - #[must_use] - pub fn from_primary_hue(hue: impl Into) -> Self { - ColorSchemeBuilder::from_hue(hue).build() - } } impl Default for ColorScheme { fn default() -> Self { - Self::from_primary_hue(138.5) + Self::from_primary(138.5) } }