New slider example showing min/max

This commit is contained in:
Jonathan Johnson 2023-11-14 11:27:04 -08:00
parent 42ed86cdfd
commit 4668db3983
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
3 changed files with 208 additions and 33 deletions

29
examples/slider.rs Normal file
View file

@ -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()
}

View file

@ -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<ZeroToOne> {
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<f32> 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<std::cmp::Ordering> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialOrd<f32> for ZeroToOne {
fn partial_cmp(&self, other: &f32) -> Option<std::cmp::Ordering> {
fn partial_cmp(&self, other: &f32) -> Option<Ordering> {
Some(self.0.total_cmp(other))
}
}

View file

@ -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<ColorSource>,
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<OklabHue>) -> 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<OklabHue>) -> 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<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 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 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<OklabHue>) -> Self {
ColorSchemeBuilder::from_hue(hue).build()
}
}
impl Default for ColorScheme {
fn default() -> Self {
Self::from_primary_hue(138.5)
Self::from_primary(138.5)
}
}