diff --git a/crates/bevy_ui/src/geometry.rs b/crates/bevy_ui/src/geometry.rs index 63adc5015f04d..3acf78d2f4516 100644 --- a/crates/bevy_ui/src/geometry.rs +++ b/crates/bevy_ui/src/geometry.rs @@ -10,6 +10,16 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// This enum allows specifying values for various [`Node`](crate::Node) properties in different units, /// such as logical pixels, percentages, or automatically determined values. +/// +/// `Val` also implements [`core::str::FromStr`] to allow parsing values from strings in the format `#.#px`. Whitespaces between the value and unit is allowed. The following units are supported: +/// * `px`: logical pixels +/// * `%`: percentage +/// * `vw`: percentage of the viewport width +/// * `vh`: percentage of the viewport height +/// * `vmin`: percentage of the viewport's smaller dimension +/// * `vmax`: percentage of the viewport's larger dimension +/// +/// Additionally, `auto` will be parsed as [`Val::Auto`]. #[derive(Copy, Clone, Debug, Reflect)] #[reflect(Default, PartialEq, Debug)] #[cfg_attr( @@ -45,6 +55,70 @@ pub enum Val { VMax(f32), } +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ValParseError { + UnitMissing, + ValueMissing, + InvalidValue, + InvalidUnit, +} + +impl core::fmt::Display for ValParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ValParseError::UnitMissing => write!(f, "unit missing"), + ValParseError::ValueMissing => write!(f, "value missing"), + ValParseError::InvalidValue => write!(f, "invalid value"), + ValParseError::InvalidUnit => write!(f, "invalid unit"), + } + } +} + +impl core::str::FromStr for Val { + type Err = ValParseError; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + if s.eq_ignore_ascii_case("auto") { + return Ok(Val::Auto); + } + + let Some(end_of_number) = s + .bytes() + .position(|c| !(c.is_ascii_digit() || c == b'.' || c == b'-' || c == b'+')) + else { + return Err(ValParseError::UnitMissing); + }; + + if end_of_number == 0 { + return Err(ValParseError::ValueMissing); + } + + let (value, unit) = s.split_at(end_of_number); + + let value: f32 = value.parse().map_err(|_| ValParseError::InvalidValue)?; + + let unit = unit.trim(); + + if unit.eq_ignore_ascii_case("px") { + Ok(Val::Px(value)) + } else if unit.eq_ignore_ascii_case("%") { + Ok(Val::Percent(value)) + } else if unit.eq_ignore_ascii_case("vw") { + Ok(Val::Vw(value)) + } else if unit.eq_ignore_ascii_case("vh") { + Ok(Val::Vh(value)) + } else if unit.eq_ignore_ascii_case("vmin") { + Ok(Val::VMin(value)) + } else if unit.eq_ignore_ascii_case("vmax") { + Ok(Val::VMax(value)) + } else { + Err(ValParseError::InvalidUnit) + } + } +} + impl PartialEq for Val { fn eq(&self, other: &Self) -> bool { let same_unit = matches!( @@ -689,6 +763,60 @@ mod tests { ); } + #[test] + fn val_str_parse() { + assert_eq!("auto".parse::(), Ok(Val::Auto)); + assert_eq!("Auto".parse::(), Ok(Val::Auto)); + assert_eq!("AUTO".parse::(), Ok(Val::Auto)); + + assert_eq!("3px".parse::(), Ok(Val::Px(3.))); + assert_eq!("3 px".parse::(), Ok(Val::Px(3.))); + assert_eq!("3.5px".parse::(), Ok(Val::Px(3.5))); + assert_eq!("-3px".parse::(), Ok(Val::Px(-3.))); + assert_eq!("3.5 PX".parse::(), Ok(Val::Px(3.5))); + + assert_eq!("3%".parse::(), Ok(Val::Percent(3.))); + assert_eq!("3 %".parse::(), Ok(Val::Percent(3.))); + assert_eq!("3.5%".parse::(), Ok(Val::Percent(3.5))); + assert_eq!("-3%".parse::(), Ok(Val::Percent(-3.))); + + assert_eq!("3vw".parse::(), Ok(Val::Vw(3.))); + assert_eq!("3 vw".parse::(), Ok(Val::Vw(3.))); + assert_eq!("3.5vw".parse::(), Ok(Val::Vw(3.5))); + assert_eq!("-3vw".parse::(), Ok(Val::Vw(-3.))); + assert_eq!("3.5 VW".parse::(), Ok(Val::Vw(3.5))); + + assert_eq!("3vh".parse::(), Ok(Val::Vh(3.))); + assert_eq!("3 vh".parse::(), Ok(Val::Vh(3.))); + assert_eq!("3.5vh".parse::(), Ok(Val::Vh(3.5))); + assert_eq!("-3vh".parse::(), Ok(Val::Vh(-3.))); + assert_eq!("3.5 VH".parse::(), Ok(Val::Vh(3.5))); + + assert_eq!("3vmin".parse::(), Ok(Val::VMin(3.))); + assert_eq!("3 vmin".parse::(), Ok(Val::VMin(3.))); + assert_eq!("3.5vmin".parse::(), Ok(Val::VMin(3.5))); + assert_eq!("-3vmin".parse::(), Ok(Val::VMin(-3.))); + assert_eq!("3.5 VMIN".parse::(), Ok(Val::VMin(3.5))); + + assert_eq!("3vmax".parse::(), Ok(Val::VMax(3.))); + assert_eq!("3 vmax".parse::(), Ok(Val::VMax(3.))); + assert_eq!("3.5vmax".parse::(), Ok(Val::VMax(3.5))); + assert_eq!("-3vmax".parse::(), Ok(Val::VMax(-3.))); + assert_eq!("3.5 VMAX".parse::(), Ok(Val::VMax(3.5))); + + assert_eq!("".parse::(), Err(ValParseError::UnitMissing)); + assert_eq!( + "hello world".parse::(), + Err(ValParseError::ValueMissing) + ); + assert_eq!("3".parse::(), Err(ValParseError::UnitMissing)); + assert_eq!("3.5".parse::(), Err(ValParseError::UnitMissing)); + assert_eq!("3pxx".parse::(), Err(ValParseError::InvalidUnit)); + assert_eq!("3.5pxx".parse::(), Err(ValParseError::InvalidUnit)); + assert_eq!("3-3px".parse::(), Err(ValParseError::InvalidValue)); + assert_eq!("3.5-3px".parse::(), Err(ValParseError::InvalidValue)); + } + #[test] fn default_val_equals_const_default_val() { assert_eq!(Val::default(), Val::DEFAULT);