Skip to content

Commit

Permalink
Implement FromStr for Val (#16926)
Browse files Browse the repository at this point in the history
# Objective

This PR implements `FromStr` for `Val`, so developers can parse values
like `10px` and `50%`

## Testing

Added tests for this. I think they cover pretty much everything, and
it's a fairly simple unit test.

## Limitations

Currently the following float values are not parsed:
- `inf`, `-inf`, `+infinity`, `NaN`
- `2.5E10`, `2.5e10`, `2.5E-10`

For my use case this is perfectly fine but other developers might want
to support these values
  • Loading branch information
VictorKoenders authored Dec 24, 2024
1 parent 5b899dc commit bacc693
Showing 1 changed file with 128 additions and 0 deletions.
128 changes: 128 additions & 0 deletions crates/bevy_ui/src/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<Self, Self::Err> {
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!(
Expand Down Expand Up @@ -689,6 +763,60 @@ mod tests {
);
}

#[test]
fn val_str_parse() {
assert_eq!("auto".parse::<Val>(), Ok(Val::Auto));
assert_eq!("Auto".parse::<Val>(), Ok(Val::Auto));
assert_eq!("AUTO".parse::<Val>(), Ok(Val::Auto));

assert_eq!("3px".parse::<Val>(), Ok(Val::Px(3.)));
assert_eq!("3 px".parse::<Val>(), Ok(Val::Px(3.)));
assert_eq!("3.5px".parse::<Val>(), Ok(Val::Px(3.5)));
assert_eq!("-3px".parse::<Val>(), Ok(Val::Px(-3.)));
assert_eq!("3.5 PX".parse::<Val>(), Ok(Val::Px(3.5)));

assert_eq!("3%".parse::<Val>(), Ok(Val::Percent(3.)));
assert_eq!("3 %".parse::<Val>(), Ok(Val::Percent(3.)));
assert_eq!("3.5%".parse::<Val>(), Ok(Val::Percent(3.5)));
assert_eq!("-3%".parse::<Val>(), Ok(Val::Percent(-3.)));

assert_eq!("3vw".parse::<Val>(), Ok(Val::Vw(3.)));
assert_eq!("3 vw".parse::<Val>(), Ok(Val::Vw(3.)));
assert_eq!("3.5vw".parse::<Val>(), Ok(Val::Vw(3.5)));
assert_eq!("-3vw".parse::<Val>(), Ok(Val::Vw(-3.)));
assert_eq!("3.5 VW".parse::<Val>(), Ok(Val::Vw(3.5)));

assert_eq!("3vh".parse::<Val>(), Ok(Val::Vh(3.)));
assert_eq!("3 vh".parse::<Val>(), Ok(Val::Vh(3.)));
assert_eq!("3.5vh".parse::<Val>(), Ok(Val::Vh(3.5)));
assert_eq!("-3vh".parse::<Val>(), Ok(Val::Vh(-3.)));
assert_eq!("3.5 VH".parse::<Val>(), Ok(Val::Vh(3.5)));

assert_eq!("3vmin".parse::<Val>(), Ok(Val::VMin(3.)));
assert_eq!("3 vmin".parse::<Val>(), Ok(Val::VMin(3.)));
assert_eq!("3.5vmin".parse::<Val>(), Ok(Val::VMin(3.5)));
assert_eq!("-3vmin".parse::<Val>(), Ok(Val::VMin(-3.)));
assert_eq!("3.5 VMIN".parse::<Val>(), Ok(Val::VMin(3.5)));

assert_eq!("3vmax".parse::<Val>(), Ok(Val::VMax(3.)));
assert_eq!("3 vmax".parse::<Val>(), Ok(Val::VMax(3.)));
assert_eq!("3.5vmax".parse::<Val>(), Ok(Val::VMax(3.5)));
assert_eq!("-3vmax".parse::<Val>(), Ok(Val::VMax(-3.)));
assert_eq!("3.5 VMAX".parse::<Val>(), Ok(Val::VMax(3.5)));

assert_eq!("".parse::<Val>(), Err(ValParseError::UnitMissing));
assert_eq!(
"hello world".parse::<Val>(),
Err(ValParseError::ValueMissing)
);
assert_eq!("3".parse::<Val>(), Err(ValParseError::UnitMissing));
assert_eq!("3.5".parse::<Val>(), Err(ValParseError::UnitMissing));
assert_eq!("3pxx".parse::<Val>(), Err(ValParseError::InvalidUnit));
assert_eq!("3.5pxx".parse::<Val>(), Err(ValParseError::InvalidUnit));
assert_eq!("3-3px".parse::<Val>(), Err(ValParseError::InvalidValue));
assert_eq!("3.5-3px".parse::<Val>(), Err(ValParseError::InvalidValue));
}

#[test]
fn default_val_equals_const_default_val() {
assert_eq!(Val::default(), Val::DEFAULT);
Expand Down

0 comments on commit bacc693

Please sign in to comment.