//! Pure numeric value types shared by the form controls. //! //! These types carry no gpui dependency: they are the unit-testable core of //! the slider / spinbox / color widgets. The key invariant is that a //! [`SliderValue`] round-trips through [`SliderValue::to_f64`] and //! [`SliderValue::from_f64`] losslessly for the kinds where exactness matters //! (integers and rationals), so repeated edits cannot accumulate drift. use gpui::SharedString; /// The family of values a control can edit. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ValueKind { /// A real number. Float, /// A whole number. Integer, /// An exact fraction `num / den`. Rational, /// An angle in degrees. Angle, } /// The value edited by a form control. #[derive(Debug, Clone, Copy, PartialEq)] pub enum SliderValue { /// A real number. Float(f64), /// A whole number. Integer(i64), /// An exact fraction. Rational(RationalValue), /// An angle in degrees. Angle(f64), } impl SliderValue { /// The [`ValueKind`] of this value. pub fn kind(self) -> ValueKind { match self { SliderValue::Float(_) => ValueKind::Float, SliderValue::Integer(_) => ValueKind::Integer, SliderValue::Rational(_) => ValueKind::Rational, SliderValue::Angle(_) => ValueKind::Angle, } } /// The numeric projection used for painting and interpolation. /// /// For [`SliderValue::Rational`] this is `num / den` as `f64`; the exact /// pair is preserved by [`SliderValue::Rational`] itself. pub fn to_f64(self) -> f64 { match self { SliderValue::Float(v) => v, SliderValue::Integer(v) => v as f64, SliderValue::Rational(v) => v.to_f64(), SliderValue::Angle(v) => v, } } /// Rebuild a value from its numeric projection. /// /// [`SliderValue::Integer`] truncates toward zero and /// [`SliderValue::Rational`] reconstructs the closest fraction with a /// bounded denominator, so this is only lossless when the source value /// was exact to begin with. pub fn from_f64(kind: ValueKind, value: f64) -> Self { match kind { ValueKind::Float => SliderValue::Float(value), ValueKind::Integer => SliderValue::Integer(value.trunc() as i64), ValueKind::Rational => SliderValue::Rational(RationalValue::from_f64(value, 1_000_000)), ValueKind::Angle => SliderValue::Angle(value), } } } /// An exact rational number, kept normalized (`den > 0`, `gcd(|num|, den) == 1`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct RationalValue { num: i64, den: i64, } /// Error returned when a rational cannot be constructed from raw parts. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum RationalError { /// The denominator must not be zero. #[error("rational denominator must not be zero")] ZeroDenominator, } impl RationalValue { /// The zero rational `0/1`. pub const ZERO: Self = Self { num: 0, den: 1 }; /// The one rational `1/1`. pub const ONE: Self = Self { num: 1, den: 1 }; /// Construct a normalized rational, rejecting a zero denominator. pub fn new(num: i64, den: i64) -> Result { if den == 0 { return Err(RationalError::ZeroDenominator); } Ok(Self::new_unchecked(num, den)) } /// Construct a normalized rational, clamping a zero denominator to one. pub fn new_or_one(num: i64, den: i64) -> Self { if den == 0 { return Self { num, den: 1 }; } Self::new_unchecked(num, den) } fn new_unchecked(num: i64, den: i64) -> Self { let (num, den) = if den < 0 { (-num, -den) } else { (num, den) }; let gcd = num.unsigned_abs().gcd(den as u64) as i64; Self { num: num / gcd, den: den / gcd, } } /// The numerator of the normalized fraction. pub fn num(self) -> i64 { self.num } /// The denominator of the normalized fraction (always positive). pub fn den(self) -> i64 { self.den } /// The value as `f64`. pub fn to_f64(self) -> f64 { self.num as f64 / self.den as f64 } /// The closest rational with `1 <= den <= max_den`, via continued /// fractions. `NaN` and infinities map to the zero rational; negative /// values keep their sign. pub fn from_f64(value: f64, max_den: i64) -> Self { if !value.is_finite() { return Self::ZERO; } let max_den = max_den.max(1); let negative = value < 0.0; let value = value.abs(); // Continued fraction convergents with bounded denominator. let mut n0 = 0i64; let mut d0 = 1i64; let mut n1 = 1i64; let mut d1 = 0i64; let mut remainder = value; for _ in 0..64 { let a = remainder.floor() as i64; let n2 = a.saturating_mul(n1).saturating_add(n0); let d2 = a.saturating_mul(d1).saturating_add(d0); if d2 > max_den { break; } n0 = n1; d0 = d1; n1 = n2; d1 = d2; let frac = remainder - remainder.floor(); if frac.abs() < 1e-12 { break; } remainder = 1.0 / frac; } let (num, den) = if negative { (-n1, d1) } else { (n1, d1) }; Self::new_unchecked(num, den) } } /// A failure to parse a value from text. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ParseValueError { /// The text did not match the expected format for this value kind. #[error("could not parse {0:?} as {1:?}")] InvalidFormat(SharedString, ValueKind), } /// Formats and parses a [`SliderValue`] for display and direct entry. /// /// Implementations are injected into controls so hosts can localize or /// specialize the representation (e.g. timecode instead of frames, or /// fractions with a fixed denominator). pub trait ValueFormatter: 'static { /// Render a value as text. fn format(&self, value: SliderValue) -> SharedString; /// Parse text into a value. Must reject malformed input with /// [`ParseValueError`]; controls refuse to apply rejected input. fn parse(&self, text: &str) -> Result; } /// The default formatter: /// /// * `Float` — up to three decimal digits, trailing zeros trimmed. /// * `Integer` — plain decimal. /// * `Rational` — `num/den`. /// * `Angle` — degrees with a trailing `°`. #[derive(Debug, Clone, Copy, Default)] pub struct DefaultFormatter; impl ValueFormatter for DefaultFormatter { fn format(&self, value: SliderValue) -> SharedString { match value { SliderValue::Float(v) => { if !v.is_finite() { return "0".into(); } let rounded = (v * 1000.0).round() / 1000.0; let mut text = format!("{rounded}"); if text.contains('.') { while text.ends_with('0') { text.pop(); } if text.ends_with('.') { text.pop(); } } text.into() } SliderValue::Integer(v) => format!("{v}").into(), SliderValue::Rational(v) => format!("{}/{}", v.num(), v.den()).into(), SliderValue::Angle(v) => { let rounded = (v * 1000.0).round() / 1000.0; format!("{rounded}°").into() } } } fn parse(&self, text: &str) -> Result { let text = text.trim(); let kind = |reason: SharedString| ParseValueError::InvalidFormat(reason, ValueKind::Float); if let Some((num, den)) = text.split_once('/') { let num: i64 = num .trim() .parse() .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational))?; let den: i64 = den .trim() .parse() .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational))?; return RationalValue::new(num, den) .map(SliderValue::Rational) .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Rational)); } let (body, is_angle) = text .strip_suffix('°') .map(|body| (body, true)) .or_else(|| { text.strip_suffix("deg") .or_else(|| text.strip_suffix("DEG")) .map(|body| (body, true)) }) .unwrap_or((text, false)); if is_angle { let value: f64 = body .trim() .parse() .map_err(|_| ParseValueError::InvalidFormat(text.into(), ValueKind::Angle))?; return Ok(SliderValue::Angle(value)); } // An exact decimal with no fractional part parses as an integer when // there are no digits after the point; otherwise as a float. let value: f64 = body.trim().parse().map_err(|_| kind(text.into()))?; if value.fract() == 0.0 && value.abs() < i64::MAX as f64 { Ok(SliderValue::Integer(value as i64)) } else { Ok(SliderValue::Float(value)) } } } /// Extension: `u64::gcd` used by [`RationalValue`]. trait Gcd { fn gcd(self, other: Self) -> Self; } impl Gcd for u64 { fn gcd(mut self, mut other: Self) -> Self { while other != 0 { (self, other) = (other, self % other); } self } } #[cfg(test)] mod tests { use super::*; #[test] fn rational_normalizes() { assert_eq!( RationalValue::new(4, 6).unwrap(), RationalValue::new(2, 3).unwrap() ); assert_eq!(RationalValue::new(2, 3).unwrap().num(), 2); assert_eq!(RationalValue::new(2, 3).unwrap().den(), 3); assert_eq!( RationalValue::new(-2, 4).unwrap(), RationalValue::new(-1, 2).unwrap() ); assert_eq!( RationalValue::new(1, -2).unwrap(), RationalValue::new(-1, 2).unwrap() ); } #[test] fn rational_rejects_zero_denominator() { assert_eq!( RationalValue::new(1, 0), Err(RationalError::ZeroDenominator) ); assert_eq!( RationalValue::new_or_one(1, 0), RationalValue::new(1, 1).unwrap() ); } #[test] fn rational_from_f64_exact() { assert_eq!( RationalValue::from_f64(0.5, 1000), RationalValue::new(1, 2).unwrap() ); assert_eq!( RationalValue::from_f64(0.25, 1000), RationalValue::new(1, 4).unwrap() ); assert_eq!( RationalValue::from_f64(2.0, 1000), RationalValue::new(2, 1).unwrap() ); assert_eq!( RationalValue::from_f64(-1.5, 1000), RationalValue::new(-3, 2).unwrap() ); assert_eq!( RationalValue::from_f64(0.3333333333, 1000), RationalValue::new(1, 3).unwrap() ); assert_eq!(RationalValue::from_f64(f64::NAN, 1000), RationalValue::ZERO); assert_eq!( RationalValue::from_f64(f64::INFINITY, 1000), RationalValue::ZERO ); } #[test] fn rational_from_f64_bounded_denominator() { // 1/999983 needs a huge denominator; the bounded approximation may be // 0 but must never exceed the bound or produce a negative denominator. let value = RationalValue::from_f64(1.0 / 999983.0, 1000); assert!(value.den() <= 1000); assert!(value.den() > 0); // A friendlier irrational keeps a nonzero approximation. let approx = RationalValue::from_f64(std::f64::consts::FRAC_1_PI, 1000); assert!(approx.num() > 0); assert!(approx.den() <= 1000); assert!((approx.to_f64() - std::f64::consts::FRAC_1_PI).abs() < 1e-3); } #[test] fn value_kind_projection() { assert_eq!(SliderValue::Integer(-3).to_f64(), -3.0); assert_eq!( SliderValue::Rational(RationalValue::new(1, 3).unwrap()).to_f64(), 1.0 / 3.0 ); assert_eq!( SliderValue::from_f64(ValueKind::Integer, 3.9), SliderValue::Integer(3) ); assert_eq!( SliderValue::from_f64(ValueKind::Rational, 0.5), SliderValue::Rational(RationalValue::new(1, 2).unwrap()) ); assert_eq!( SliderValue::from_f64(ValueKind::Angle, 90.0), SliderValue::Angle(90.0) ); } #[test] fn default_formatter_round_trips() { let formatter = DefaultFormatter; for value in [ SliderValue::Float(1.5), SliderValue::Integer(-42), SliderValue::Rational(RationalValue::new(1, 3).unwrap()), SliderValue::Angle(90.0), ] { let text = formatter.format(value); let parsed = formatter.parse(text.as_ref()).unwrap(); assert_eq!(parsed.kind(), value.kind()); } } #[test] fn default_formatter_trims_and_truncates() { let formatter = DefaultFormatter; assert_eq!(formatter.format(SliderValue::Float(1.5)), "1.5"); assert_eq!(formatter.format(SliderValue::Float(0.123456)), "0.123"); assert_eq!(formatter.format(SliderValue::Float(2.0)), "2"); assert_eq!(formatter.format(SliderValue::Angle(45.5)), "45.5°"); assert_eq!(formatter.format(SliderValue::Integer(7)), "7"); } #[test] fn default_formatter_rejects_invalid_input() { let formatter = DefaultFormatter; assert!(formatter.parse("abc").is_err()); assert!(formatter.parse("1/0").is_err()); assert!(formatter.parse("12/ab").is_err()); assert!(formatter.parse("").is_err()); assert!(formatter.parse("90°abc").is_err()); } #[test] fn default_formatter_parses_angle_suffix() { let formatter = DefaultFormatter; assert_eq!(formatter.parse("90°").unwrap(), SliderValue::Angle(90.0)); assert_eq!(formatter.parse("45 deg").unwrap(), SliderValue::Angle(45.0)); assert_eq!( formatter.parse("180 DEG").unwrap(), SliderValue::Angle(180.0) ); } }