use core::fmt;
use std::num::ParseFloatError;
use crate::corety::AzString;
use crate::props::{
basic::{error::ParseFloatErrorWithInput, FloatValue, SizeMetric},
formatter::FormatAsCssValue,
};
pub const DEFAULT_FONT_SIZE: f32 = 16.0;
pub const PT_TO_PX: f32 = 96.0 / 72.0;
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
#[repr(transparent)]
pub struct NormalizedPercentage(f32);
impl NormalizedPercentage {
#[inline]
#[must_use] pub const fn new(value: f32) -> Self {
Self(value)
}
#[inline]
#[must_use] pub fn from_unnormalized(value: f32) -> Self {
Self(value / 100.0)
}
#[inline]
#[must_use] pub const fn get(self) -> f32 {
self.0
}
#[inline]
#[must_use] pub fn resolve(self, containing_block_size: f32) -> f32 {
self.0 * containing_block_size
}
}
impl fmt::Display for NormalizedPercentage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}%", self.0 * 100.0)
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[repr(C)]
pub struct CssLogicalSize {
pub inline_size: f32,
pub block_size: f32,
}
impl CssLogicalSize {
#[inline]
#[must_use] pub const fn new(inline_size: f32, block_size: f32) -> Self {
Self {
inline_size,
block_size,
}
}
#[inline]
#[must_use] pub const fn to_physical(self) -> PhysicalSize {
PhysicalSize {
width: self.inline_size,
height: self.block_size,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[repr(C)]
pub struct PhysicalSize {
pub width: f32,
pub height: f32,
}
impl PhysicalSize {
#[inline]
#[must_use] pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
#[inline]
#[must_use] pub const fn to_logical(self) -> CssLogicalSize {
CssLogicalSize {
inline_size: self.width,
block_size: self.height,
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct ResolutionContext {
pub element_font_size: f32,
pub parent_font_size: f32,
pub root_font_size: f32,
pub containing_block_size: PhysicalSize,
pub element_size: Option<PhysicalSize>,
pub viewport_size: PhysicalSize,
}
impl Default for ResolutionContext {
fn default() -> Self {
Self {
element_font_size: 16.0,
parent_font_size: 16.0,
root_font_size: 16.0,
containing_block_size: PhysicalSize::new(0.0, 0.0),
element_size: None,
viewport_size: PhysicalSize::new(0.0, 0.0),
}
}
}
impl ResolutionContext {
#[inline]
#[must_use] pub const fn default_const() -> Self {
Self {
element_font_size: 16.0,
parent_font_size: 16.0,
root_font_size: 16.0,
containing_block_size: PhysicalSize {
width: 0.0,
height: 0.0,
},
element_size: None,
viewport_size: PhysicalSize {
width: 0.0,
height: 0.0,
},
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum PropertyContext {
FontSize,
Margin,
Padding,
Width,
Height,
BorderWidth,
BorderRadius,
Transform,
Other,
}
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct PixelValue {
pub metric: SizeMetric,
pub number: FloatValue,
}
impl PixelValue {
pub fn scale_for_dpi(&mut self, scale_factor: f32) {
self.number = FloatValue::new(self.number.get() * scale_factor);
}
}
impl FormatAsCssValue for PixelValue {
fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.number, self.metric)
}
}
impl crate::css::PrintAsCssValue for PixelValue {
fn print_as_css_value(&self) -> String {
format!("{}{}", self.number, self.metric)
}
}
impl crate::codegen::format::FormatAsRustCode for PixelValue {
fn format_as_rust_code(&self, _tabs: usize) -> String {
format!(
"PixelValue {{ metric: {:?}, number: FloatValue::new({}) }}",
self.metric,
self.number.get()
)
}
}
impl fmt::Debug for PixelValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.number, self.metric)
}
}
impl fmt::Display for PixelValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.number, self.metric)
}
}
impl PixelValue {
#[inline]
#[must_use] pub const fn zero() -> Self {
const ZERO_PX: PixelValue = PixelValue::const_px(0);
ZERO_PX
}
#[inline]
#[must_use] pub const fn const_px(value: isize) -> Self {
Self::const_from_metric(SizeMetric::Px, value)
}
#[inline]
#[must_use] pub const fn const_em(value: isize) -> Self {
Self::const_from_metric(SizeMetric::Em, value)
}
#[inline]
#[must_use] pub const fn const_em_fractional(pre_comma: isize, post_comma: isize) -> Self {
Self::const_from_metric_fractional(SizeMetric::Em, pre_comma, post_comma)
}
#[inline]
#[must_use] pub const fn const_pt(value: isize) -> Self {
Self::const_from_metric(SizeMetric::Pt, value)
}
#[inline]
#[must_use] pub const fn const_pt_fractional(pre_comma: isize, post_comma: isize) -> Self {
Self::const_from_metric_fractional(SizeMetric::Pt, pre_comma, post_comma)
}
#[inline]
#[must_use] pub const fn const_percent(value: isize) -> Self {
Self::const_from_metric(SizeMetric::Percent, value)
}
#[inline]
#[must_use] pub const fn const_in(value: isize) -> Self {
Self::const_from_metric(SizeMetric::In, value)
}
#[inline]
#[must_use] pub const fn const_cm(value: isize) -> Self {
Self::const_from_metric(SizeMetric::Cm, value)
}
#[inline]
#[must_use] pub const fn const_mm(value: isize) -> Self {
Self::const_from_metric(SizeMetric::Mm, value)
}
#[inline]
#[must_use] pub const fn const_from_metric(metric: SizeMetric, value: isize) -> Self {
Self {
metric,
number: FloatValue::const_new(value),
}
}
#[inline]
#[must_use] pub const fn const_from_metric_fractional(
metric: SizeMetric,
pre_comma: isize,
post_comma: isize,
) -> Self {
Self {
metric,
number: FloatValue::const_new_fractional(pre_comma, post_comma),
}
}
#[inline]
#[must_use] pub fn px(value: f32) -> Self {
Self::from_metric(SizeMetric::Px, value)
}
#[inline]
#[must_use] pub fn em(value: f32) -> Self {
Self::from_metric(SizeMetric::Em, value)
}
#[inline]
#[must_use] pub fn inch(value: f32) -> Self {
Self::from_metric(SizeMetric::In, value)
}
#[inline]
#[must_use] pub fn cm(value: f32) -> Self {
Self::from_metric(SizeMetric::Cm, value)
}
#[inline]
#[must_use] pub fn mm(value: f32) -> Self {
Self::from_metric(SizeMetric::Mm, value)
}
#[inline]
#[must_use] pub fn pt(value: f32) -> Self {
Self::from_metric(SizeMetric::Pt, value)
}
#[inline]
#[must_use] pub fn percent(value: f32) -> Self {
Self::from_metric(SizeMetric::Percent, value)
}
#[inline]
#[must_use] pub fn rem(value: f32) -> Self {
Self::from_metric(SizeMetric::Rem, value)
}
#[inline]
#[must_use] pub fn from_metric(metric: SizeMetric, value: f32) -> Self {
Self {
metric,
number: FloatValue::new(value),
}
}
#[inline]
#[allow(clippy::suboptimal_flops)] #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
if self.metric == other.metric {
Self {
metric: self.metric,
number: self.number.interpolate(&other.number, t),
}
} else {
let self_px_interp = self.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
let other_px_interp = other.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
Self::from_metric(
SizeMetric::Px,
self_px_interp + (other_px_interp - self_px_interp) * t,
)
}
}
#[inline]
#[must_use] pub fn to_percent(&self) -> Option<NormalizedPercentage> {
match self.metric {
SizeMetric::Percent => Some(NormalizedPercentage::from_unnormalized(self.number.get())),
_ => None,
}
}
#[doc(hidden)]
#[inline]
#[must_use] pub fn to_pixels_internal(&self, percent_resolve: f32, em_resolve: f32, rem_resolve: f32) -> f32 {
match self.metric {
SizeMetric::Px => self.number.get(),
SizeMetric::Pt => self.number.get() * PT_TO_PX,
SizeMetric::In => self.number.get() * 96.0,
SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
SizeMetric::Em => self.number.get() * em_resolve,
SizeMetric::Rem => self.number.get() * rem_resolve,
SizeMetric::Percent => {
NormalizedPercentage::from_unnormalized(self.number.get()).resolve(percent_resolve)
}
SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => 0.0,
}
}
#[inline]
#[must_use] pub fn resolve_with_context(
&self,
context: &ResolutionContext,
property_context: PropertyContext,
) -> f32 {
match self.metric {
SizeMetric::Px => self.number.get(),
SizeMetric::Pt => self.number.get() * PT_TO_PX,
SizeMetric::In => self.number.get() * 96.0,
SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
SizeMetric::Em => {
let reference_font_size = if property_context == PropertyContext::FontSize {
context.parent_font_size
} else {
context.element_font_size
};
self.number.get() * reference_font_size
}
SizeMetric::Rem => self.number.get() * context.root_font_size,
SizeMetric::Vw => self.number.get() * context.viewport_size.width / 100.0,
SizeMetric::Vh => self.number.get() * context.viewport_size.height / 100.0,
SizeMetric::Vmin => {
let min_dimension = context
.viewport_size
.width
.min(context.viewport_size.height);
self.number.get() * min_dimension / 100.0
}
SizeMetric::Vmax => {
let max_dimension = context
.viewport_size
.width
.max(context.viewport_size.height);
self.number.get() * max_dimension / 100.0
}
SizeMetric::Percent => {
#[allow(clippy::match_same_arms)]
let reference = match property_context {
PropertyContext::FontSize => context.parent_font_size,
PropertyContext::Width => context.containing_block_size.width,
PropertyContext::Height => context.containing_block_size.height,
PropertyContext::Margin | PropertyContext::Padding => {
context.containing_block_size.width
}
PropertyContext::BorderWidth => 0.0,
PropertyContext::BorderRadius => {
context.element_size.map_or(0.0, |s| s.width)
}
PropertyContext::Transform => {
context.element_size.map_or(0.0, |s| s.width)
}
PropertyContext::Other => context.containing_block_size.width,
};
NormalizedPercentage::from_unnormalized(self.number.get()).resolve(reference)
}
}
}
}
pub const THIN_BORDER_THICKNESS: PixelValue = PixelValue {
metric: SizeMetric::Px,
number: FloatValue { number: 1000 },
};
pub const MEDIUM_BORDER_THICKNESS: PixelValue = PixelValue {
metric: SizeMetric::Px,
number: FloatValue { number: 3000 },
};
pub const THICK_BORDER_THICKNESS: PixelValue = PixelValue {
metric: SizeMetric::Px,
number: FloatValue { number: 5000 },
};
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct PixelValueNoPercent {
pub inner: PixelValue,
}
impl PixelValueNoPercent {
pub fn scale_for_dpi(&mut self, scale_factor: f32) {
self.inner.scale_for_dpi(scale_factor);
}
}
impl_option!(
PixelValueNoPercent,
OptionPixelValueNoPercent,
[Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_option!(
PixelValue,
OptionPixelValue,
[Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl fmt::Display for PixelValueNoPercent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner)
}
}
impl ::core::fmt::Debug for PixelValueNoPercent {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
write!(f, "{self}")
}
}
impl PixelValueNoPercent {
#[doc(hidden)]
#[inline]
#[must_use] pub fn to_pixels_internal(&self, em_resolve: f32, rem_resolve: f32) -> f32 {
self.inner.to_pixels_internal(0.0, em_resolve, rem_resolve)
}
#[inline]
#[must_use] pub const fn zero() -> Self {
const ZERO_PXNP: PixelValueNoPercent = PixelValueNoPercent {
inner: PixelValue::zero(),
};
ZERO_PXNP
}
}
impl From<PixelValue> for PixelValueNoPercent {
fn from(e: PixelValue) -> Self {
Self { inner: e }
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum CssPixelValueParseError<'a> {
EmptyString,
NoValueGiven(&'a str, SizeMetric),
ValueParseErr(ParseFloatError, &'a str),
InvalidPixelValue(&'a str),
}
impl_debug_as_display!(CssPixelValueParseError<'a>);
impl_display! { CssPixelValueParseError<'a>, {
EmptyString => format!("Missing [px / pt / em / %] value"),
NoValueGiven(input, metric) => format!("Expected floating-point pixel value, got: \"{}{}\"", input, metric),
ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
InvalidPixelValue(s) => format!("Invalid pixel value: \"{}\"", s),
}}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct PixelNoValueGivenError {
pub value: AzString,
pub metric: SizeMetric,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum CssPixelValueParseErrorOwned {
EmptyString,
NoValueGiven(PixelNoValueGivenError),
ValueParseErr(ParseFloatErrorWithInput),
InvalidPixelValue(AzString),
}
impl CssPixelValueParseError<'_> {
#[must_use] pub fn to_contained(&self) -> CssPixelValueParseErrorOwned {
match self {
CssPixelValueParseError::EmptyString => CssPixelValueParseErrorOwned::EmptyString,
CssPixelValueParseError::NoValueGiven(s, metric) => {
CssPixelValueParseErrorOwned::NoValueGiven(PixelNoValueGivenError { value: (*s).to_string().into(), metric: *metric })
}
CssPixelValueParseError::ValueParseErr(err, s) => {
CssPixelValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput { error: err.clone().into(), input: (*s).to_string().into() })
}
CssPixelValueParseError::InvalidPixelValue(s) => {
CssPixelValueParseErrorOwned::InvalidPixelValue((*s).to_string().into())
}
}
}
}
impl CssPixelValueParseErrorOwned {
#[must_use] pub fn to_shared(&self) -> CssPixelValueParseError<'_> {
match self {
Self::EmptyString => CssPixelValueParseError::EmptyString,
Self::NoValueGiven(e) => {
CssPixelValueParseError::NoValueGiven(e.value.as_str(), e.metric)
}
Self::ValueParseErr(e) => {
CssPixelValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
}
Self::InvalidPixelValue(s) => {
CssPixelValueParseError::InvalidPixelValue(s.as_str())
}
}
}
}
fn parse_pixel_value_inner<'a>(
input: &'a str,
match_values: &[(&'static str, SizeMetric)],
) -> Result<PixelValue, CssPixelValueParseError<'a>> {
let input = input.trim();
if input.is_empty() {
return Err(CssPixelValueParseError::EmptyString);
}
for (match_val, metric) in match_values {
if let Some(value) = input.strip_suffix(match_val) {
let value = value.trim();
if value.is_empty() {
return Err(CssPixelValueParseError::NoValueGiven(input, *metric));
}
match value.parse::<f32>() {
Ok(o) => {
return Ok(PixelValue::from_metric(*metric, o));
}
Err(e) => {
return Err(CssPixelValueParseError::ValueParseErr(e, value));
}
}
}
}
input.trim().parse::<f32>().map_or_else(
|_| Err(CssPixelValueParseError::InvalidPixelValue(input)),
|o| Ok(PixelValue::px(o)),
)
}
pub fn parse_pixel_value(input: &str) -> Result<PixelValue, CssPixelValueParseError<'_>> {
parse_pixel_value_inner(
input,
&[
("px", SizeMetric::Px),
("rem", SizeMetric::Rem), ("em", SizeMetric::Em),
("pt", SizeMetric::Pt),
("vmax", SizeMetric::Vmax),
("vmin", SizeMetric::Vmin), ("vw", SizeMetric::Vw),
("vh", SizeMetric::Vh),
("in", SizeMetric::In),
("mm", SizeMetric::Mm),
("cm", SizeMetric::Cm),
("%", SizeMetric::Percent),
],
)
}
pub fn parse_pixel_value_no_percent(
input: &str,
) -> Result<PixelValueNoPercent, CssPixelValueParseError<'_>> {
Ok(PixelValueNoPercent {
inner: parse_pixel_value_inner(
input,
&[
("px", SizeMetric::Px),
("rem", SizeMetric::Rem), ("em", SizeMetric::Em),
("pt", SizeMetric::Pt),
("vmax", SizeMetric::Vmax),
("vmin", SizeMetric::Vmin), ("vw", SizeMetric::Vw),
("vh", SizeMetric::Vh),
("in", SizeMetric::In),
("mm", SizeMetric::Mm),
("cm", SizeMetric::Cm),
],
)?,
})
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PixelValueWithAuto {
None,
Initial,
Inherit,
Auto,
Exact(PixelValue),
}
pub fn parse_pixel_value_with_auto(
input: &str,
) -> Result<PixelValueWithAuto, CssPixelValueParseError<'_>> {
let input = input.trim();
match input {
"none" => Ok(PixelValueWithAuto::None),
"initial" => Ok(PixelValueWithAuto::Initial),
"inherit" => Ok(PixelValueWithAuto::Inherit),
"auto" => Ok(PixelValueWithAuto::Auto),
e => Ok(PixelValueWithAuto::Exact(parse_pixel_value(e)?)),
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum SystemMetricRef {
#[default]
ButtonRadius,
ButtonPaddingHorizontal,
ButtonPaddingVertical,
ButtonBorderWidth,
TitlebarHeight,
TitlebarButtonWidth,
TitlebarPadding,
SafeAreaTop,
SafeAreaBottom,
SafeAreaLeft,
SafeAreaRight,
}
impl SystemMetricRef {
#[must_use] pub const fn resolve(&self, metrics: &crate::system::SystemMetrics) -> Option<PixelValue> {
match self {
Self::ButtonRadius => metrics.corner_radius.as_option().copied(),
Self::ButtonPaddingHorizontal => metrics.button_padding_horizontal.as_option().copied(),
Self::ButtonPaddingVertical => metrics.button_padding_vertical.as_option().copied(),
Self::ButtonBorderWidth => metrics.border_width.as_option().copied(),
Self::TitlebarHeight => metrics.titlebar.height.as_option().copied(),
Self::TitlebarButtonWidth => metrics.titlebar.button_area_width.as_option().copied(),
Self::TitlebarPadding => metrics.titlebar.padding_horizontal.as_option().copied(),
Self::SafeAreaTop => metrics.titlebar.safe_area.top.as_option().copied(),
Self::SafeAreaBottom => metrics.titlebar.safe_area.bottom.as_option().copied(),
Self::SafeAreaLeft => metrics.titlebar.safe_area.left.as_option().copied(),
Self::SafeAreaRight => metrics.titlebar.safe_area.right.as_option().copied(),
}
}
#[must_use] pub const fn as_css_str(&self) -> &'static str {
match self {
Self::ButtonRadius => "system:button-radius",
Self::ButtonPaddingHorizontal => "system:button-padding-horizontal",
Self::ButtonPaddingVertical => "system:button-padding-vertical",
Self::ButtonBorderWidth => "system:button-border-width",
Self::TitlebarHeight => "system:titlebar-height",
Self::TitlebarButtonWidth => "system:titlebar-button-width",
Self::TitlebarPadding => "system:titlebar-padding",
Self::SafeAreaTop => "system:safe-area-top",
Self::SafeAreaBottom => "system:safe-area-bottom",
Self::SafeAreaLeft => "system:safe-area-left",
Self::SafeAreaRight => "system:safe-area-right",
}
}
#[must_use] pub fn from_css_str(s: &str) -> Option<Self> {
match s {
"button-radius" => Some(Self::ButtonRadius),
"button-padding-horizontal" => Some(Self::ButtonPaddingHorizontal),
"button-padding-vertical" => Some(Self::ButtonPaddingVertical),
"button-border-width" => Some(Self::ButtonBorderWidth),
"titlebar-height" => Some(Self::TitlebarHeight),
"titlebar-button-width" => Some(Self::TitlebarButtonWidth),
"titlebar-padding" => Some(Self::TitlebarPadding),
"safe-area-top" => Some(Self::SafeAreaTop),
"safe-area-bottom" => Some(Self::SafeAreaBottom),
"safe-area-left" => Some(Self::SafeAreaLeft),
"safe-area-right" => Some(Self::SafeAreaRight),
_ => None,
}
}
}
impl fmt::Display for SystemMetricRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_css_str())
}
}
impl FormatAsCssValue for SystemMetricRef {
fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_css_str())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
#[repr(C, u8)]
pub enum PixelValueOrSystem {
Value(PixelValue),
System(SystemMetricRef),
}
impl Default for PixelValueOrSystem {
fn default() -> Self {
Self::Value(PixelValue::zero())
}
}
impl From<PixelValue> for PixelValueOrSystem {
fn from(value: PixelValue) -> Self {
Self::Value(value)
}
}
impl PixelValueOrSystem {
#[must_use] pub const fn value(v: PixelValue) -> Self {
Self::Value(v)
}
#[must_use] pub const fn system(s: SystemMetricRef) -> Self {
Self::System(s)
}
#[must_use] pub fn resolve(&self, system_metrics: &crate::system::SystemMetrics, fallback: PixelValue) -> PixelValue {
match self {
Self::Value(v) => *v,
Self::System(ref_type) => ref_type.resolve(system_metrics).unwrap_or(fallback),
}
}
}
impl fmt::Display for PixelValueOrSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Value(v) => write!(f, "{v}"),
Self::System(s) => write!(f, "{s}"),
}
}
}
impl FormatAsCssValue for PixelValueOrSystem {
fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Value(v) => v.format_as_css_value(f),
Self::System(s) => s.format_as_css_value(f),
}
}
}
#[cfg(feature = "parser")]
pub fn parse_pixel_value_or_system(
input: &str,
) -> Result<PixelValueOrSystem, CssPixelValueParseError<'_>> {
let input = input.trim();
if let Some(metric_name) = input.strip_prefix("system:") {
if let Some(metric_ref) = SystemMetricRef::from_css_str(metric_name) {
return Ok(PixelValueOrSystem::System(metric_ref));
}
return Err(CssPixelValueParseError::InvalidPixelValue(input));
}
Ok(PixelValueOrSystem::Value(parse_pixel_value(input)?))
}
#[cfg(all(test, feature = "parser"))]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
#[test]
fn test_parse_pixel_value() {
assert_eq!(parse_pixel_value("10px").unwrap(), PixelValue::px(10.0));
assert_eq!(parse_pixel_value("1.5em").unwrap(), PixelValue::em(1.5));
assert_eq!(parse_pixel_value("2rem").unwrap(), PixelValue::rem(2.0));
assert_eq!(parse_pixel_value("-20pt").unwrap(), PixelValue::pt(-20.0));
assert_eq!(parse_pixel_value("50%").unwrap(), PixelValue::percent(50.0));
assert_eq!(parse_pixel_value("1in").unwrap(), PixelValue::inch(1.0));
assert_eq!(parse_pixel_value("2.54cm").unwrap(), PixelValue::cm(2.54));
assert_eq!(parse_pixel_value("10mm").unwrap(), PixelValue::mm(10.0));
assert_eq!(parse_pixel_value(" 0 ").unwrap(), PixelValue::px(0.0));
}
#[test]
fn test_resolve_with_context_em() {
let context = ResolutionContext {
element_font_size: 32.0,
parent_font_size: 16.0,
..Default::default()
};
let margin = PixelValue::em(0.67);
assert!(
(margin.resolve_with_context(&context, PropertyContext::Margin) - 21.44).abs() < 0.01
);
let font_size = PixelValue::em(2.0);
assert_eq!(
font_size.resolve_with_context(&context, PropertyContext::FontSize),
32.0
);
}
#[test]
fn test_resolve_with_context_rem() {
let context = ResolutionContext {
element_font_size: 32.0,
parent_font_size: 16.0,
root_font_size: 18.0,
..Default::default()
};
let margin = PixelValue::rem(2.0);
assert_eq!(
margin.resolve_with_context(&context, PropertyContext::Margin),
36.0
);
let font_size = PixelValue::rem(1.5);
assert_eq!(
font_size.resolve_with_context(&context, PropertyContext::FontSize),
27.0
);
}
#[test]
fn test_resolve_with_context_percent_margin() {
let context = ResolutionContext {
element_font_size: 16.0,
parent_font_size: 16.0,
root_font_size: 16.0,
containing_block_size: PhysicalSize::new(800.0, 600.0),
element_size: None,
viewport_size: PhysicalSize::new(1920.0, 1080.0),
};
let margin = PixelValue::percent(10.0); assert_eq!(
margin.resolve_with_context(&context, PropertyContext::Margin),
80.0
); }
#[test]
fn test_parse_pixel_value_no_percent() {
assert_eq!(
parse_pixel_value_no_percent("10px").unwrap().inner,
PixelValue::px(10.0)
);
assert!(parse_pixel_value_no_percent("50%").is_err());
}
#[test]
fn test_parse_pixel_value_with_auto() {
assert_eq!(
parse_pixel_value_with_auto("10px").unwrap(),
PixelValueWithAuto::Exact(PixelValue::px(10.0))
);
assert_eq!(
parse_pixel_value_with_auto("auto").unwrap(),
PixelValueWithAuto::Auto
);
assert_eq!(
parse_pixel_value_with_auto("initial").unwrap(),
PixelValueWithAuto::Initial
);
assert_eq!(
parse_pixel_value_with_auto("inherit").unwrap(),
PixelValueWithAuto::Inherit
);
assert_eq!(
parse_pixel_value_with_auto("none").unwrap(),
PixelValueWithAuto::None
);
}
#[test]
fn test_parse_pixel_value_errors() {
assert!(parse_pixel_value("").is_err());
assert!(parse_pixel_value("10").is_ok()); assert!(parse_pixel_value("10 px").is_ok()); assert!(parse_pixel_value("px").is_err());
assert!(parse_pixel_value("ten-px").is_err());
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::unreadable_literal,
clippy::cast_precision_loss,
clippy::too_many_lines,
clippy::excessive_precision
)]
mod autotest_generated {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use super::*;
use crate::{
codegen::format::FormatAsRustCode,
css::PrintAsCssValue,
props::{
basic::length::{FloatValue, SizeMetric},
formatter::FormatAsCssValue,
},
system::{SafeAreaInsets, SystemMetrics, TitlebarMetrics},
};
const MULT: f32 = 1000.0;
const MAX_SAFE_CONST: isize = isize::MAX / 1000;
const MIN_SAFE_CONST: isize = isize::MIN / 1000;
const ALL_METRICS: [SizeMetric; 12] = [
SizeMetric::Px,
SizeMetric::Pt,
SizeMetric::Em,
SizeMetric::Rem,
SizeMetric::In,
SizeMetric::Cm,
SizeMetric::Mm,
SizeMetric::Percent,
SizeMetric::Vw,
SizeMetric::Vh,
SizeMetric::Vmin,
SizeMetric::Vmax,
];
const ALL_PROPERTY_CONTEXTS: [PropertyContext; 9] = [
PropertyContext::FontSize,
PropertyContext::Margin,
PropertyContext::Padding,
PropertyContext::Width,
PropertyContext::Height,
PropertyContext::BorderWidth,
PropertyContext::BorderRadius,
PropertyContext::Transform,
PropertyContext::Other,
];
const ALL_SYSTEM_REFS: [SystemMetricRef; 11] = [
SystemMetricRef::ButtonRadius,
SystemMetricRef::ButtonPaddingHorizontal,
SystemMetricRef::ButtonPaddingVertical,
SystemMetricRef::ButtonBorderWidth,
SystemMetricRef::TitlebarHeight,
SystemMetricRef::TitlebarButtonWidth,
SystemMetricRef::TitlebarPadding,
SystemMetricRef::SafeAreaTop,
SystemMetricRef::SafeAreaBottom,
SystemMetricRef::SafeAreaLeft,
SystemMetricRef::SafeAreaRight,
];
const EXTREME_F32: [f32; 13] = [
0.0,
-0.0,
1.0,
-1.0,
f32::MIN_POSITIVE,
-f32::MIN_POSITIVE,
1e30,
-1e30,
f32::MAX,
f32::MIN,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NAN,
];
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 0.001
}
fn hash_of<T: Hash>(v: &T) -> u64 {
let mut h = DefaultHasher::new();
v.hash(&mut h);
h.finish()
}
struct CssVal<T>(T);
impl<T: FormatAsCssValue> fmt::Display for CssVal<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.format_as_css_value(f)
}
}
fn as_css_value<T: FormatAsCssValue>(v: T) -> String {
CssVal(v).to_string()
}
fn distinct_context() -> ResolutionContext {
ResolutionContext {
element_font_size: 32.0,
parent_font_size: 8.0,
root_font_size: 4.0,
containing_block_size: PhysicalSize::new(800.0, 600.0),
element_size: Some(PhysicalSize::new(200.0, 100.0)),
viewport_size: PhysicalSize::new(1000.0, 500.0),
}
}
fn populated_metrics() -> SystemMetrics {
SystemMetrics {
corner_radius: OptionPixelValue::Some(PixelValue::px(1.0)),
border_width: OptionPixelValue::Some(PixelValue::px(2.0)),
button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(3.0)),
button_padding_vertical: OptionPixelValue::Some(PixelValue::px(4.0)),
titlebar: TitlebarMetrics {
height: OptionPixelValue::Some(PixelValue::px(5.0)),
button_area_width: OptionPixelValue::Some(PixelValue::px(6.0)),
padding_horizontal: OptionPixelValue::Some(PixelValue::px(7.0)),
safe_area: SafeAreaInsets {
top: OptionPixelValue::Some(PixelValue::px(8.0)),
bottom: OptionPixelValue::Some(PixelValue::px(9.0)),
left: OptionPixelValue::Some(PixelValue::px(10.0)),
right: OptionPixelValue::Some(PixelValue::px(11.0)),
},
..TitlebarMetrics::default()
},
}
}
#[test]
fn parse_pixel_value_rejects_empty_and_whitespace_only() {
assert_eq!(
parse_pixel_value("").unwrap_err(),
CssPixelValueParseError::EmptyString
);
for ws in [" ", "\t\n", "\r\n\t ", "\n"] {
assert_eq!(
parse_pixel_value(ws).unwrap_err(),
CssPixelValueParseError::EmptyString,
"whitespace-only input {ws:?} must trim down to EmptyString"
);
}
}
#[test]
fn parse_pixel_value_rejects_a_bare_unit_with_no_number() {
for unit in [
"px", "rem", "em", "pt", "in", "mm", "cm", "vmax", "vw", "vh", "%",
] {
let err = parse_pixel_value(unit).unwrap_err();
assert!(
matches!(err, CssPixelValueParseError::NoValueGiven(input, _) if input == unit),
"bare unit {unit:?} should be NoValueGiven, got {err:?}"
);
}
assert!(matches!(
parse_pixel_value(" px").unwrap_err(),
CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
));
}
#[test]
fn parse_pixel_value_vmin_is_shadowed_by_the_in_suffix() {
assert_eq!(
parse_pixel_value("5vmin").unwrap(),
PixelValue::from_metric(SizeMetric::Vmin, 5.0)
);
assert!(matches!(
parse_pixel_value("vmin").unwrap_err(),
CssPixelValueParseError::NoValueGiven(..)
));
assert_eq!(
parse_pixel_value("5vmax").unwrap(),
PixelValue::from_metric(SizeMetric::Vmax, 5.0)
);
assert_eq!(
parse_pixel_value("5vw").unwrap(),
PixelValue::from_metric(SizeMetric::Vw, 5.0)
);
assert_eq!(
parse_pixel_value("5vh").unwrap(),
PixelValue::from_metric(SizeMetric::Vh, 5.0)
);
}
#[test]
fn parse_pixel_value_inner_proves_the_vmin_bug_is_pure_suffix_ordering() {
let in_first: [(&'static str, SizeMetric); 2] =
[("in", SizeMetric::In), ("vmin", SizeMetric::Vmin)];
let vmin_first: [(&'static str, SizeMetric); 2] =
[("vmin", SizeMetric::Vmin), ("in", SizeMetric::In)];
assert!(parse_pixel_value_inner("5vmin", &in_first).is_err());
assert_eq!(
parse_pixel_value_inner("5vmin", &vmin_first).unwrap(),
PixelValue::from_metric(SizeMetric::Vmin, 5.0)
);
assert_eq!(
parse_pixel_value_inner("5in", &vmin_first).unwrap(),
PixelValue::inch(5.0)
);
}
#[test]
fn parse_pixel_value_inner_with_an_empty_table_falls_back_to_unitless_px() {
let empty: [(&'static str, SizeMetric); 0] = [];
assert_eq!(
parse_pixel_value_inner("10", &empty).unwrap(),
PixelValue::px(10.0)
);
assert!(matches!(
parse_pixel_value_inner("10px", &empty).unwrap_err(),
CssPixelValueParseError::InvalidPixelValue("10px")
));
assert_eq!(
parse_pixel_value_inner("", &empty).unwrap_err(),
CssPixelValueParseError::EmptyString
);
}
#[test]
fn parse_pixel_value_accepts_every_unit_it_advertises() {
let cases: [(&str, PixelValue); 12] = [
("10px", PixelValue::px(10.0)),
("1.5em", PixelValue::em(1.5)),
("2rem", PixelValue::rem(2.0)),
("-20pt", PixelValue::pt(-20.0)),
("50%", PixelValue::percent(50.0)),
("1in", PixelValue::inch(1.0)),
("2.54cm", PixelValue::cm(2.54)),
("10mm", PixelValue::mm(10.0)),
("+7px", PixelValue::px(7.0)),
(".5px", PixelValue::px(0.5)),
("5.px", PixelValue::px(5.0)),
("1e2px", PixelValue::px(100.0)),
];
for (input, expected) in cases {
assert_eq!(
parse_pixel_value(input).unwrap(),
expected,
"parsing {input:?}"
);
}
assert_eq!(parse_pixel_value(" 0 ").unwrap(), PixelValue::px(0.0));
assert_eq!(parse_pixel_value("10 px").unwrap(), PixelValue::px(10.0));
assert_eq!(parse_pixel_value("\t10px\n").unwrap(), PixelValue::px(10.0));
}
#[test]
fn parse_pixel_value_boundary_numbers_saturate_instead_of_overflowing() {
assert_eq!(parse_pixel_value("-0").unwrap(), PixelValue::px(0.0));
assert_eq!(parse_pixel_value("-0").unwrap(), PixelValue::zero());
assert_eq!(parse_pixel_value("0.0004px").unwrap(), PixelValue::px(0.0));
assert_eq!(parse_pixel_value("-0.0009px").unwrap(), PixelValue::px(0.0));
assert_eq!(parse_pixel_value("1e-40px").unwrap(), PixelValue::px(0.0));
for huge in ["9223372036854775807", "1e40px", "3.5e38"] {
let v = parse_pixel_value(huge).unwrap();
assert!(
v.number.get().is_finite(),
"{huge:?} leaked a non-finite value: {}",
v.number.get()
);
assert!(v.number.get() > 0.0, "{huge:?} lost its sign");
}
let neg = parse_pixel_value("-1e40px").unwrap();
assert!(neg.number.get().is_finite() && neg.number.get() < 0.0);
}
#[test]
fn parse_pixel_value_inherits_rusts_float_keywords() {
assert_eq!(parse_pixel_value("NaN").unwrap(), PixelValue::zero());
let inf = parse_pixel_value("infinity").unwrap();
assert_eq!(inf, PixelValue::px(f32::INFINITY));
assert!(inf.number.get().is_finite() && inf.number.get() > 0.0);
let neg_inf = parse_pixel_value("-infinity").unwrap();
assert_eq!(neg_inf, PixelValue::px(f32::NEG_INFINITY));
assert!(neg_inf.number.get().is_finite() && neg_inf.number.get() < 0.0);
let inf_short = parse_pixel_value("inf").unwrap();
assert_eq!(inf_short, PixelValue::px(f32::INFINITY));
assert!(inf_short.number.get().is_finite() && inf_short.number.get() > 0.0);
}
#[test]
fn parse_pixel_value_is_case_sensitive_about_units() {
for input in ["10PX", "10Px", "10EM", "10REM", "10VMAX"] {
let err = parse_pixel_value(input).unwrap_err();
assert!(
matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input),
"uppercase unit {input:?} should be InvalidPixelValue, got {err:?}"
);
}
}
#[test]
fn parse_pixel_value_rejects_garbage_and_trailing_junk() {
for input in [
"ten-px",
"px10",
"10px;garbage",
"10;",
"--",
"1%%",
"10 20px",
"#",
"10px 10px",
"e",
"0x10px",
] {
assert!(
parse_pixel_value(input).is_err(),
"{input:?} must not parse, got {:?}",
parse_pixel_value(input)
);
}
}
#[test]
fn parse_pixel_value_survives_unicode() {
for input in [
"\u{1F600}", "10px\u{1F600}", "10px\u{0301}", "\u{200B}10px", "\u{0661}\u{0660}px", "10\u{0440}\u{0445}", "\u{202E}10px", ] {
let got = parse_pixel_value(input);
assert!(got.is_err(), "{input:?} must be rejected, got {got:?}");
}
assert!(matches!(
parse_pixel_value("\u{200B}10px").unwrap_err(),
CssPixelValueParseError::ValueParseErr(_, "\u{200B}10")
));
}
#[test]
fn parse_pixel_value_handles_extremely_long_and_deeply_nested_input() {
let long_number = format!("{}px", "9".repeat(100_000));
let parsed = parse_pixel_value(&long_number).unwrap();
assert!(parsed.number.get().is_finite());
assert_eq!(parsed.metric, SizeMetric::Px);
let long_junk = "x".repeat(100_000);
assert!(parse_pixel_value(&long_junk).is_err());
let nested = "(".repeat(10_000);
assert!(matches!(
parse_pixel_value(&nested).unwrap_err(),
CssPixelValueParseError::InvalidPixelValue(_)
));
}
#[test]
fn parse_pixel_value_no_percent_rejects_percentages_but_keeps_the_rest() {
assert_eq!(
parse_pixel_value_no_percent("10px").unwrap().inner,
PixelValue::px(10.0)
);
assert_eq!(
parse_pixel_value_no_percent("5vmax").unwrap().inner,
PixelValue::from_metric(SizeMetric::Vmax, 5.0)
);
assert!(matches!(
parse_pixel_value_no_percent("50%").unwrap_err(),
CssPixelValueParseError::InvalidPixelValue("50%")
));
assert!(matches!(
parse_pixel_value_no_percent("%").unwrap_err(),
CssPixelValueParseError::InvalidPixelValue("%")
));
assert_eq!(
parse_pixel_value_no_percent("").unwrap_err(),
CssPixelValueParseError::EmptyString
);
assert_eq!(
parse_pixel_value_no_percent(" ").unwrap_err(),
CssPixelValueParseError::EmptyString
);
assert!(parse_pixel_value_no_percent("\u{1F600}").is_err());
assert_eq!(
parse_pixel_value_no_percent("5vmin").unwrap().inner,
PixelValue::from_metric(SizeMetric::Vmin, 5.0)
);
}
#[test]
fn parse_pixel_value_with_auto_keywords_and_fallthrough() {
assert_eq!(
parse_pixel_value_with_auto("auto").unwrap(),
PixelValueWithAuto::Auto
);
assert_eq!(
parse_pixel_value_with_auto(" initial ").unwrap(),
PixelValueWithAuto::Initial
);
assert_eq!(
parse_pixel_value_with_auto("\tinherit\n").unwrap(),
PixelValueWithAuto::Inherit
);
assert_eq!(
parse_pixel_value_with_auto("none").unwrap(),
PixelValueWithAuto::None
);
assert_eq!(
parse_pixel_value_with_auto("10px").unwrap(),
PixelValueWithAuto::Exact(PixelValue::px(10.0))
);
for input in ["AUTO", "Auto", "INHERIT", "None"] {
assert!(
parse_pixel_value_with_auto(input).is_err(),
"{input:?} unexpectedly matched a keyword"
);
}
assert_eq!(
parse_pixel_value_with_auto("").unwrap_err(),
CssPixelValueParseError::EmptyString
);
assert_eq!(
parse_pixel_value_with_auto(" \t ").unwrap_err(),
CssPixelValueParseError::EmptyString
);
assert!(parse_pixel_value_with_auto("auto;garbage").is_err());
assert!(parse_pixel_value_with_auto("\u{1F600}").is_err());
assert!(parse_pixel_value_with_auto(&"(".repeat(10_000)).is_err());
}
#[cfg(feature = "parser")]
#[test]
fn parse_pixel_value_or_system_accepts_every_system_ref() {
for r in ALL_SYSTEM_REFS {
let css = r.as_css_str(); assert_eq!(
parse_pixel_value_or_system(css).unwrap(),
PixelValueOrSystem::System(r),
"round-tripping {css:?}"
);
assert_eq!(
parse_pixel_value_or_system(&format!(" {css} ")).unwrap(),
PixelValueOrSystem::System(r)
);
}
assert_eq!(
parse_pixel_value_or_system("10px").unwrap(),
PixelValueOrSystem::Value(PixelValue::px(10.0))
);
assert_eq!(
parse_pixel_value_or_system("1.5em").unwrap(),
PixelValueOrSystem::Value(PixelValue::em(1.5))
);
}
#[cfg(feature = "parser")]
#[test]
fn parse_pixel_value_or_system_rejects_malformed_system_refs() {
assert!(matches!(
parse_pixel_value_or_system("system:button-padding").unwrap_err(),
CssPixelValueParseError::InvalidPixelValue("system:button-padding")
));
for input in [
"system:", "system:unknown", "system: button-radius", "system:BUTTON-RADIUS", "system:button-radius;x", "system:\u{1F600}", ] {
let err = parse_pixel_value_or_system(input).unwrap_err();
assert!(
matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input),
"{input:?} should be InvalidPixelValue, got {err:?}"
);
}
assert!(matches!(
parse_pixel_value_or_system("SYSTEM:button-radius").unwrap_err(),
CssPixelValueParseError::InvalidPixelValue("SYSTEM:button-radius")
));
assert_eq!(
parse_pixel_value_or_system("").unwrap_err(),
CssPixelValueParseError::EmptyString
);
assert_eq!(
parse_pixel_value_or_system(" ").unwrap_err(),
CssPixelValueParseError::EmptyString
);
let long = format!("system:{}", "a".repeat(100_000));
assert!(parse_pixel_value_or_system(&long).is_err());
}
#[test]
fn parse_errors_survive_the_owned_round_trip() {
let float_err = "x".parse::<f32>().unwrap_err();
let errors = [
CssPixelValueParseError::EmptyString,
CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px),
CssPixelValueParseError::ValueParseErr(float_err, "abc"),
CssPixelValueParseError::InvalidPixelValue("ten-px"),
];
for err in errors {
let owned = err.to_contained();
let shared = owned.to_shared();
assert_eq!(shared, err, "to_contained -> to_shared must be lossless");
assert!(!err.to_string().is_empty());
assert_eq!(shared.to_string(), err.to_string());
}
}
#[test]
fn parse_errors_round_trip_from_real_parse_failures() {
for input in ["", "px", "\u{200B}10px", "ten-px", "%"] {
let err = parse_pixel_value(input).unwrap_err();
let owned = err.to_contained();
assert_eq!(owned.to_shared(), err, "round-trip failed for {input:?}");
}
}
#[test]
fn float_constructors_never_leak_a_non_finite_value() {
type Ctor = (fn(f32) -> PixelValue, SizeMetric);
let ctors: [Ctor; 8] = [
(PixelValue::px, SizeMetric::Px),
(PixelValue::em, SizeMetric::Em),
(PixelValue::pt, SizeMetric::Pt),
(PixelValue::inch, SizeMetric::In),
(PixelValue::cm, SizeMetric::Cm),
(PixelValue::mm, SizeMetric::Mm),
(PixelValue::percent, SizeMetric::Percent),
(PixelValue::rem, SizeMetric::Rem),
];
for (ctor, metric) in ctors {
for v in EXTREME_F32 {
let px = ctor(v);
assert_eq!(px.metric, metric, "constructor lost its metric for {v}");
assert!(
px.number.get().is_finite(),
"{metric:?} constructor leaked a non-finite value for input {v}"
);
}
assert_eq!(ctor(f32::NAN).number.get(), 0.0, "NaN must sanitize to 0");
assert!(ctor(f32::INFINITY).number.get() > 0.0);
assert!(ctor(f32::NEG_INFINITY).number.get() < 0.0);
}
for metric in ALL_METRICS {
for v in EXTREME_F32 {
let px = PixelValue::from_metric(metric, v);
assert_eq!(px.metric, metric);
assert!(px.number.get().is_finite());
}
assert_eq!(
PixelValue::from_metric(metric, 12.0),
PixelValue {
metric,
number: FloatValue::new(12.0)
}
);
}
}
#[test]
fn float_constructors_quantize_to_one_thousandth() {
assert_eq!(PixelValue::px(0.0004).number.get(), 0.0);
assert_eq!(PixelValue::px(-0.0009).number.get(), 0.0);
assert_eq!(PixelValue::px(1.0005).number.get(), 1.0);
assert_eq!(PixelValue::px(-0.0), PixelValue::px(0.0));
assert_eq!(
hash_of(&PixelValue::px(-0.0)),
hash_of(&PixelValue::px(0.0))
);
assert_eq!(PixelValue::px(f32::NAN), PixelValue::px(f32::NAN));
assert_eq!(PixelValue::px(f32::NAN), PixelValue::zero());
}
#[test]
fn const_constructors_agree_with_their_float_twins() {
assert_eq!(PixelValue::const_px(5), PixelValue::px(5.0));
assert_eq!(PixelValue::const_em(5), PixelValue::em(5.0));
assert_eq!(PixelValue::const_pt(5), PixelValue::pt(5.0));
assert_eq!(PixelValue::const_percent(5), PixelValue::percent(5.0));
assert_eq!(PixelValue::const_in(5), PixelValue::inch(5.0));
assert_eq!(PixelValue::const_cm(5), PixelValue::cm(5.0));
assert_eq!(PixelValue::const_mm(5), PixelValue::mm(5.0));
assert_eq!(PixelValue::const_px(0), PixelValue::zero());
assert_eq!(PixelValue::const_px(-7), PixelValue::px(-7.0));
for metric in ALL_METRICS {
assert_eq!(
PixelValue::const_from_metric(metric, 7),
PixelValue::from_metric(metric, 7.0),
"const_from_metric disagrees with from_metric for {metric:?}"
);
assert_eq!(
PixelValue::const_from_metric(metric, -7),
PixelValue::from_metric(metric, -7.0)
);
}
}
#[test]
fn const_constructors_are_usable_up_to_the_documented_isize_bound() {
for v in [0, 1, -1, MAX_SAFE_CONST, MIN_SAFE_CONST] {
let px = PixelValue::const_px(v);
assert!(
px.number.get().is_finite(),
"const_px({v}) leaked a non-finite value"
);
}
assert!(PixelValue::const_px(MAX_SAFE_CONST).number.get() > 0.0);
assert!(PixelValue::const_px(MIN_SAFE_CONST).number.get() < 0.0);
assert_eq!(
PixelValue::const_px(MAX_SAFE_CONST).number.number(),
MAX_SAFE_CONST * 1000
);
}
#[test]
fn const_fractional_constructors_match_their_documented_examples() {
assert!(approx(PixelValue::const_em_fractional(1, 5).number.get(), 1.5));
assert!(approx(
PixelValue::const_em_fractional(0, 83).number.get(),
0.83
));
assert!(approx(
PixelValue::const_em_fractional(1, 17).number.get(),
1.17
));
assert_eq!(PixelValue::const_em_fractional(1, 5).metric, SizeMetric::Em);
assert_eq!(PixelValue::const_pt_fractional(1, 5).metric, SizeMetric::Pt);
assert!(approx(PixelValue::const_pt_fractional(2, 25).number.get(), 2.25));
assert_eq!(
PixelValue::const_from_metric_fractional(SizeMetric::Px, 0, 0),
PixelValue::zero()
);
assert!(approx(
PixelValue::const_from_metric_fractional(SizeMetric::Px, -1, 5)
.number
.get(),
-1.5
));
assert!(approx(
PixelValue::const_from_metric_fractional(SizeMetric::Px, 1, 5234)
.number
.get(),
1.523
));
let extreme =
PixelValue::const_from_metric_fractional(SizeMetric::Px, 0, isize::MAX);
assert!(extreme.number.get().is_finite());
}
#[test]
fn scale_for_dpi_is_defined_for_every_scale_factor() {
let mut doubled = PixelValue::px(10.0);
doubled.scale_for_dpi(2.0);
assert_eq!(doubled, PixelValue::px(20.0));
doubled.scale_for_dpi(2.0);
assert_eq!(doubled, PixelValue::px(40.0));
let mut zeroed = PixelValue::em(3.0);
zeroed.scale_for_dpi(0.0);
assert_eq!(zeroed, PixelValue::em(0.0));
assert_eq!(zeroed.metric, SizeMetric::Em, "metric must be preserved");
let mut flipped = PixelValue::px(10.0);
flipped.scale_for_dpi(-1.5);
assert_eq!(flipped, PixelValue::px(-15.0));
let mut nan_scaled = PixelValue::px(10.0);
nan_scaled.scale_for_dpi(f32::NAN);
assert_eq!(nan_scaled.number.get(), 0.0);
let mut inf_scaled = PixelValue::px(10.0);
inf_scaled.scale_for_dpi(f32::INFINITY);
assert!(inf_scaled.number.get().is_finite() && inf_scaled.number.get() > 0.0);
let mut max_scaled = PixelValue::px(f32::MAX);
max_scaled.scale_for_dpi(f32::MAX);
assert!(max_scaled.number.get().is_finite());
let mut wrapped = PixelValueNoPercent::from(PixelValue::px(10.0));
wrapped.scale_for_dpi(2.5);
assert_eq!(wrapped.inner, PixelValue::px(25.0));
let mut wrapped_nan = PixelValueNoPercent::from(PixelValue::px(10.0));
wrapped_nan.scale_for_dpi(f32::NAN);
assert_eq!(wrapped_nan.inner.number.get(), 0.0);
}
#[test]
fn interpolate_within_one_metric_keeps_that_metric() {
let a = PixelValue::em(1.0);
let b = PixelValue::em(3.0);
assert_eq!(a.interpolate(&b, 0.0), a);
assert_eq!(a.interpolate(&b, 1.0), b);
assert_eq!(a.interpolate(&b, 0.5), PixelValue::em(2.0));
assert_eq!(a.interpolate(&b, 2.0), PixelValue::em(5.0));
assert_eq!(a.interpolate(&b, -1.0), PixelValue::em(-1.0));
let p = PixelValue::percent(0.0).interpolate(&PixelValue::percent(100.0), 0.5);
assert_eq!(p, PixelValue::percent(50.0));
assert_eq!(p.metric, SizeMetric::Percent);
let nan_t = a.interpolate(&b, f32::NAN);
assert_eq!(nan_t.number.get(), 0.0);
assert_eq!(nan_t.metric, SizeMetric::Em);
assert!(a.interpolate(&b, f32::INFINITY).number.get().is_finite());
}
#[test]
fn interpolate_across_metrics_falls_back_to_px() {
let from_px = PixelValue::px(0.0);
let to_em = PixelValue::em(1.0); let mid = from_px.interpolate(&to_em, 0.5);
assert_eq!(mid.metric, SizeMetric::Px);
assert!(approx(mid.number.get(), DEFAULT_FONT_SIZE / 2.0));
assert!(approx(
PixelValue::px(0.0)
.interpolate(&PixelValue::pt(72.0), 1.0)
.number
.get(),
96.0
));
for metric in [
SizeMetric::Percent,
SizeMetric::Vw,
SizeMetric::Vh,
SizeMetric::Vmin,
SizeMetric::Vmax,
] {
let other = PixelValue::from_metric(metric, 50.0);
let done = PixelValue::px(100.0).interpolate(&other, 1.0);
assert_eq!(
done,
PixelValue::px(0.0),
"{metric:?} should collapse to 0px on the cross-metric path"
);
}
let nan_t = PixelValue::px(0.0).interpolate(&PixelValue::em(1.0), f32::NAN);
assert_eq!(nan_t.number.get(), 0.0);
}
#[test]
fn to_pixels_internal_converts_every_absolute_and_relative_unit() {
assert_eq!(PixelValue::px(10.0).to_pixels_internal(0.0, 16.0, 16.0), 10.0);
assert!(approx(
PixelValue::pt(72.0).to_pixels_internal(0.0, 16.0, 16.0),
96.0
));
assert!(approx(
PixelValue::inch(1.0).to_pixels_internal(0.0, 16.0, 16.0),
96.0
));
assert!(approx(
PixelValue::cm(2.54).to_pixels_internal(0.0, 16.0, 16.0),
96.0
));
assert!(approx(
PixelValue::mm(25.4).to_pixels_internal(0.0, 16.0, 16.0),
96.0
));
assert_eq!(PT_TO_PX, 96.0 / 72.0);
assert_eq!(PixelValue::em(2.0).to_pixels_internal(0.0, 10.0, 100.0), 20.0);
assert_eq!(
PixelValue::rem(2.0).to_pixels_internal(0.0, 10.0, 100.0),
200.0
);
assert_eq!(
PixelValue::percent(50.0).to_pixels_internal(800.0, 16.0, 16.0),
400.0
);
assert_eq!(
PixelValue::percent(0.0).to_pixels_internal(800.0, 16.0, 16.0),
0.0
);
assert_eq!(
PixelValue::percent(-50.0).to_pixels_internal(800.0, 16.0, 16.0),
-400.0
);
for metric in [
SizeMetric::Vw,
SizeMetric::Vh,
SizeMetric::Vmin,
SizeMetric::Vmax,
] {
assert_eq!(
PixelValue::from_metric(metric, 50.0).to_pixels_internal(800.0, 16.0, 16.0),
0.0,
"{metric:?} must resolve to 0 on the legacy path"
);
}
}
#[test]
fn to_pixels_internal_non_finite_resolves_are_defined_not_panics() {
assert!(PixelValue::em(1.0)
.to_pixels_internal(0.0, f32::NAN, 0.0)
.is_nan());
assert!(PixelValue::rem(1.0)
.to_pixels_internal(0.0, 0.0, f32::INFINITY)
.is_infinite());
assert!(PixelValue::percent(50.0)
.to_pixels_internal(f32::INFINITY, 16.0, 16.0)
.is_infinite());
assert!(PixelValue::percent(50.0)
.to_pixels_internal(f32::NAN, 16.0, 16.0)
.is_nan());
assert!(PixelValue::percent(0.0)
.to_pixels_internal(f32::INFINITY, 16.0, 16.0)
.is_nan());
assert!(PixelValue::em(f32::MAX)
.to_pixels_internal(0.0, f32::MAX, 0.0)
.is_infinite());
for v in EXTREME_F32 {
assert!(PixelValue::px(v)
.to_pixels_internal(f32::NAN, f32::NAN, f32::NAN)
.is_finite());
}
}
#[test]
fn pixel_value_no_percent_to_pixels_internal_zeroes_out_percentages() {
assert_eq!(
PixelValueNoPercent::from(PixelValue::px(10.0)).to_pixels_internal(16.0, 16.0),
10.0
);
assert_eq!(
PixelValueNoPercent::from(PixelValue::em(2.0)).to_pixels_internal(10.0, 100.0),
20.0
);
assert_eq!(
PixelValueNoPercent::from(PixelValue::rem(2.0)).to_pixels_internal(10.0, 100.0),
200.0
);
assert_eq!(
PixelValueNoPercent::from(PixelValue::percent(50.0)).to_pixels_internal(16.0, 16.0),
0.0
);
assert_eq!(PixelValueNoPercent::zero().to_pixels_internal(16.0, 16.0), 0.0);
assert_eq!(PixelValueNoPercent::zero().inner, PixelValue::zero());
assert_eq!(PixelValueNoPercent::default().inner, PixelValue::zero());
}
#[test]
fn to_percent_is_some_only_for_the_percent_metric() {
for metric in ALL_METRICS {
let v = PixelValue::from_metric(metric, 50.0);
if metric == SizeMetric::Percent {
assert_eq!(v.to_percent().unwrap().get(), 0.5, "50% must normalize to 0.5");
} else {
assert!(
v.to_percent().is_none(),
"{metric:?} must not masquerade as a percentage"
);
}
}
assert_eq!(
PixelValue::percent(50.0)
.to_percent()
.unwrap()
.resolve(640.0),
320.0
);
assert_eq!(
PixelValue::percent(-50.0).to_percent().unwrap().get(),
-0.5
);
assert_eq!(PixelValue::percent(0.0).to_percent().unwrap().get(), 0.0);
assert!(PixelValue::percent(f32::MAX)
.to_percent()
.unwrap()
.get()
.is_finite());
assert_eq!(
PixelValue::percent(f32::NAN).to_percent().unwrap().get(),
0.0
);
}
#[test]
fn normalized_percentage_new_and_from_unnormalized_disagree_by_100x() {
assert_eq!(NormalizedPercentage::new(0.5).get(), 0.5);
assert_eq!(NormalizedPercentage::from_unnormalized(50.0).get(), 0.5);
assert_eq!(NormalizedPercentage::from_unnormalized(0.0).get(), 0.0);
assert_eq!(NormalizedPercentage::from_unnormalized(100.0).get(), 1.0);
assert_eq!(NormalizedPercentage::from_unnormalized(-25.0).get(), -0.25);
assert_eq!(NormalizedPercentage::new(0.5).resolve(640.0), 320.0);
assert_eq!(NormalizedPercentage::new(0.0).resolve(640.0), 0.0);
assert_eq!(NormalizedPercentage::new(1.0).resolve(f32::MAX), f32::MAX);
assert_eq!(NormalizedPercentage::new(-1.0).resolve(100.0), -100.0);
assert!(NormalizedPercentage::new(f32::NAN).get().is_nan());
assert!(NormalizedPercentage::new(f32::NAN).resolve(100.0).is_nan());
assert!(NormalizedPercentage::from_unnormalized(f32::INFINITY)
.get()
.is_infinite());
assert!(NormalizedPercentage::new(1.0)
.resolve(f32::INFINITY)
.is_infinite());
assert!(NormalizedPercentage::new(0.0)
.resolve(f32::INFINITY)
.is_nan());
assert_eq!(NormalizedPercentage::new(0.5).to_string(), "50%");
assert_eq!(NormalizedPercentage::new(0.0).to_string(), "0%");
assert!(!NormalizedPercentage::new(f32::NAN).to_string().is_empty());
assert!(!NormalizedPercentage::new(f32::INFINITY)
.to_string()
.is_empty());
}
#[test]
fn resolve_with_context_reads_the_right_reference_for_each_property() {
let ctx = distinct_context();
assert_eq!(
PixelValue::em(2.0).resolve_with_context(&ctx, PropertyContext::Margin),
64.0
);
assert_eq!(
PixelValue::em(2.0).resolve_with_context(&ctx, PropertyContext::FontSize),
16.0
);
for pc in ALL_PROPERTY_CONTEXTS {
assert_eq!(
PixelValue::rem(2.0).resolve_with_context(&ctx, pc),
8.0,
"rem must ignore the property context ({pc:?})"
);
}
let pct = PixelValue::percent(50.0);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::Width),
400.0,
"width % -> containing block WIDTH"
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::Height),
300.0,
"height % -> containing block HEIGHT"
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::Margin),
400.0,
"margin % -> containing block WIDTH, even vertically (CSS 2.1 §8.3)"
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::Padding),
400.0,
"padding % -> containing block WIDTH, even vertically (CSS 2.1 §8.4)"
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::Other),
400.0
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::FontSize),
4.0,
"font-size % -> PARENT font size"
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::BorderRadius),
100.0,
"border-radius % -> the element's own box"
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::Transform),
100.0
);
assert_eq!(
pct.resolve_with_context(&ctx, PropertyContext::BorderWidth),
0.0,
"% is invalid on border-width (CSS Backgrounds 3 §4.1) -> 0"
);
}
#[test]
fn resolve_with_context_percent_without_an_element_size_is_zero() {
let ctx = ResolutionContext {
element_size: None,
..distinct_context()
};
assert_eq!(
PixelValue::percent(50.0)
.resolve_with_context(&ctx, PropertyContext::BorderRadius),
0.0
);
assert_eq!(
PixelValue::percent(50.0).resolve_with_context(&ctx, PropertyContext::Transform),
0.0
);
}
#[test]
fn resolve_with_context_absolute_units_ignore_the_context_entirely() {
let sane = distinct_context();
let poisoned = ResolutionContext {
element_font_size: f32::NAN,
parent_font_size: f32::INFINITY,
root_font_size: f32::NEG_INFINITY,
containing_block_size: PhysicalSize::new(f32::NAN, f32::NAN),
element_size: Some(PhysicalSize::new(f32::INFINITY, f32::NAN)),
viewport_size: PhysicalSize::new(f32::NAN, f32::INFINITY),
};
let absolutes = [
PixelValue::px(10.0),
PixelValue::pt(10.0),
PixelValue::inch(10.0),
PixelValue::cm(10.0),
PixelValue::mm(10.0),
];
for v in absolutes {
for pc in ALL_PROPERTY_CONTEXTS {
let a = v.resolve_with_context(&sane, pc);
let b = v.resolve_with_context(&poisoned, pc);
assert_eq!(a, b, "{:?} must not read the context ({pc:?})", v.metric);
assert!(a.is_finite());
}
}
assert_eq!(
PixelValue::px(10.0).resolve_with_context(&sane, PropertyContext::Width),
10.0
);
assert!(approx(
PixelValue::inch(1.0).resolve_with_context(&sane, PropertyContext::Width),
96.0
));
assert!(approx(
PixelValue::pt(72.0).resolve_with_context(&sane, PropertyContext::Width),
96.0
));
assert!(approx(
PixelValue::cm(2.54).resolve_with_context(&sane, PropertyContext::Width),
96.0
));
assert!(approx(
PixelValue::mm(25.4).resolve_with_context(&sane, PropertyContext::Width),
96.0
));
}
#[test]
fn resolve_with_context_viewport_units_use_the_viewport() {
let ctx = distinct_context();
assert_eq!(
PixelValue::from_metric(SizeMetric::Vw, 10.0)
.resolve_with_context(&ctx, PropertyContext::Width),
100.0
);
assert_eq!(
PixelValue::from_metric(SizeMetric::Vh, 10.0)
.resolve_with_context(&ctx, PropertyContext::Width),
50.0
);
assert_eq!(
PixelValue::from_metric(SizeMetric::Vmin, 10.0)
.resolve_with_context(&ctx, PropertyContext::Width),
50.0,
"vmin must take the SMALLER viewport dimension"
);
assert_eq!(
PixelValue::from_metric(SizeMetric::Vmax, 10.0)
.resolve_with_context(&ctx, PropertyContext::Width),
100.0,
"vmax must take the LARGER viewport dimension"
);
let zero_vp = ResolutionContext::default_const();
for metric in [
SizeMetric::Vw,
SizeMetric::Vh,
SizeMetric::Vmin,
SizeMetric::Vmax,
] {
assert_eq!(
PixelValue::from_metric(metric, 100.0)
.resolve_with_context(&zero_vp, PropertyContext::Width),
0.0,
"{metric:?} against a 0x0 viewport must be 0"
);
}
let nan_vp = ResolutionContext {
viewport_size: PhysicalSize::new(f32::NAN, f32::NAN),
..distinct_context()
};
assert!(PixelValue::from_metric(SizeMetric::Vw, 10.0)
.resolve_with_context(&nan_vp, PropertyContext::Width)
.is_nan());
let half_nan_vp = ResolutionContext {
viewport_size: PhysicalSize::new(f32::NAN, 500.0),
..distinct_context()
};
assert_eq!(
PixelValue::from_metric(SizeMetric::Vmin, 10.0)
.resolve_with_context(&half_nan_vp, PropertyContext::Width),
50.0
);
}
#[test]
fn resolve_with_context_never_panics_on_extreme_values() {
let ctx = distinct_context();
for metric in ALL_METRICS {
for v in EXTREME_F32 {
for pc in ALL_PROPERTY_CONTEXTS {
let _ = PixelValue::from_metric(metric, v).resolve_with_context(&ctx, pc);
}
}
}
}
#[test]
fn resolution_context_default_matches_default_const() {
let a = ResolutionContext::default();
let b = ResolutionContext::default_const();
assert_eq!(a.element_font_size, b.element_font_size);
assert_eq!(a.parent_font_size, b.parent_font_size);
assert_eq!(a.root_font_size, b.root_font_size);
assert_eq!(a.containing_block_size, b.containing_block_size);
assert_eq!(a.element_size, b.element_size);
assert_eq!(a.viewport_size, b.viewport_size);
assert_eq!(a.element_font_size, DEFAULT_FONT_SIZE);
assert!(a.element_size.is_none());
}
#[test]
fn logical_and_physical_sizes_round_trip() {
let logical = CssLogicalSize::new(800.0, 600.0);
assert_eq!(logical.to_physical(), PhysicalSize::new(800.0, 600.0));
assert_eq!(logical.to_physical().to_logical(), logical);
let physical = PhysicalSize::new(1920.0, 1080.0);
assert_eq!(physical.to_logical(), CssLogicalSize::new(1920.0, 1080.0));
assert_eq!(physical.to_logical().to_physical(), physical);
assert_eq!(CssLogicalSize::new(800.0, 600.0).to_physical().width, 800.0);
assert_eq!(PhysicalSize::new(800.0, 600.0).to_logical().block_size, 600.0);
let nan = PhysicalSize::new(f32::NAN, f32::INFINITY);
assert!(nan.to_logical().inline_size.is_nan());
assert!(nan.to_logical().block_size.is_infinite());
}
#[test]
fn every_rendering_of_a_pixel_value_agrees() {
for metric in ALL_METRICS {
let v = PixelValue::from_metric(metric, 1.5);
let display = v.to_string();
assert_eq!(format!("{v:?}"), display, "Debug != Display for {metric:?}");
assert_eq!(v.print_as_css_value(), display);
assert_eq!(as_css_value(v), display);
assert!(display.starts_with("1.5"), "{display} lost its number");
assert!(display.len() > 3, "{display} lost its unit");
}
assert_eq!(PixelValue::px(10.0).to_string(), "10px");
assert_eq!(PixelValue::percent(50.0).to_string(), "50%");
assert_eq!(PixelValue::zero().to_string(), "0px");
assert_eq!(
PixelValue::from_metric(SizeMetric::Vmin, 12.0).to_string(),
"12vmin"
);
let np = PixelValueNoPercent::from(PixelValue::px(10.0));
assert_eq!(np.to_string(), "10px");
assert_eq!(format!("{np:?}"), "10px");
assert_eq!(PixelValueNoPercent::zero().to_string(), "0px");
}
#[test]
fn display_never_leaks_nan_or_infinity_into_css() {
for metric in ALL_METRICS {
for v in EXTREME_F32 {
let s = PixelValue::from_metric(metric, v).to_string();
assert!(
!s.contains("NaN") && !s.contains("inf"),
"{metric:?} with input {v} serialized to {s:?}"
);
assert!(!s.is_empty());
}
}
assert_eq!(PixelValue::px(f32::NAN).to_string(), "0px");
}
#[test]
fn pixel_values_round_trip_through_css_for_every_metric_but_vmin() {
for metric in ALL_METRICS {
if metric == SizeMetric::Vmin {
continue; }
for number in [0.0_f32, 1.0, 1.5, -20.0, 0.001, 12345.0] {
let original = PixelValue::from_metric(metric, number);
let css = original.print_as_css_value();
let reparsed = parse_pixel_value(&css).unwrap_or_else(|e| {
panic!("{css:?} (from {metric:?} {number}) failed to re-parse: {e:?}")
});
assert_eq!(reparsed, original, "round-trip broke for {css:?}");
assert_eq!(reparsed.print_as_css_value(), css);
}
}
for metric in ALL_METRICS {
if metric == SizeMetric::Vmin || metric == SizeMetric::Percent {
continue;
}
let original = PixelValueNoPercent::from(PixelValue::from_metric(metric, 7.0));
let css = original.to_string();
assert_eq!(
parse_pixel_value_no_percent(&css).unwrap(),
original,
"no-percent round-trip broke for {css:?}"
);
}
for (css, expected) in [
("auto", PixelValueWithAuto::Auto),
("none", PixelValueWithAuto::None),
("initial", PixelValueWithAuto::Initial),
("inherit", PixelValueWithAuto::Inherit),
] {
assert_eq!(parse_pixel_value_with_auto(css).unwrap(), expected);
}
let exact = PixelValue::em(1.5);
assert_eq!(
parse_pixel_value_with_auto(&exact.print_as_css_value()).unwrap(),
PixelValueWithAuto::Exact(exact)
);
}
#[test]
fn format_as_rust_code_emits_a_reconstructible_literal() {
assert_eq!(
PixelValue::px(10.0).format_as_rust_code(0),
"PixelValue { metric: Px, number: FloatValue::new(10) }"
);
assert_eq!(
PixelValue::percent(-1.5).format_as_rust_code(4),
"PixelValue { metric: Percent, number: FloatValue::new(-1.5) }"
);
let nan = PixelValue::from_metric(SizeMetric::Vmax, f32::NAN).format_as_rust_code(0);
assert_eq!(nan, "PixelValue { metric: Vmax, number: FloatValue::new(0) }");
assert!(!PixelValue::px(f32::INFINITY)
.format_as_rust_code(0)
.contains("inf"));
}
#[test]
fn border_thickness_constants_match_the_css_keywords() {
assert_eq!(THIN_BORDER_THICKNESS, PixelValue::px(1.0));
assert_eq!(MEDIUM_BORDER_THICKNESS, PixelValue::px(3.0));
assert_eq!(THICK_BORDER_THICKNESS, PixelValue::px(5.0));
assert_eq!(THIN_BORDER_THICKNESS.number.get(), 1.0);
assert_eq!(MEDIUM_BORDER_THICKNESS.number.get(), 3.0);
assert_eq!(THICK_BORDER_THICKNESS.number.get(), 5.0);
assert_eq!(THIN_BORDER_THICKNESS.number.number() as f32, MULT);
assert!(THIN_BORDER_THICKNESS < MEDIUM_BORDER_THICKNESS);
assert!(MEDIUM_BORDER_THICKNESS < THICK_BORDER_THICKNESS);
assert_eq!(THIN_BORDER_THICKNESS.to_string(), "1px");
}
#[test]
fn ord_is_lexicographic_by_metric_then_number_not_by_resolved_size() {
assert!(PixelValue::px(100.0) < PixelValue::em(1.0));
assert!(PixelValue::px(1.0) < PixelValue::px(2.0));
assert!(PixelValue::percent(1.0) > PixelValue::mm(9999.0));
let a = PixelValue::px(1.5);
let b = PixelValue::px(1.5);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
assert_ne!(hash_of(&PixelValue::px(1.0)), hash_of(&PixelValue::em(1.0)));
assert_eq!(PixelValue::px(1.0001), PixelValue::px(1.0002));
assert_eq!(
hash_of(&PixelValue::px(1.0001)),
hash_of(&PixelValue::px(1.0002))
);
}
#[test]
fn system_metric_ref_css_strings_round_trip() {
for r in ALL_SYSTEM_REFS {
let css = r.as_css_str();
assert!(
css.starts_with("system:"),
"{css:?} is missing the system: prefix"
);
assert_eq!(r.to_string(), css, "Display must match as_css_str");
assert_eq!(as_css_value(r), css);
let name = css.strip_prefix("system:").unwrap();
assert_eq!(
SystemMetricRef::from_css_str(name),
Some(r),
"{name:?} must parse back to {r:?}"
);
assert_eq!(SystemMetricRef::from_css_str(name).unwrap().as_css_str(), css);
assert_eq!(SystemMetricRef::from_css_str(css), None);
}
assert_eq!(SystemMetricRef::default(), SystemMetricRef::ButtonRadius);
}
#[test]
fn system_metric_ref_from_css_str_rejects_everything_else() {
for input in [
"",
" ",
"\t\n",
" button-radius ", "Button-Radius", "button_radius", "button-padding", "button-radius;x",
"\u{1F600}",
"b\u{0301}utton-radius",
] {
assert_eq!(
SystemMetricRef::from_css_str(input),
None,
"{input:?} must not resolve to a system metric"
);
}
assert_eq!(
SystemMetricRef::from_css_str(&"a".repeat(100_000)),
None
);
assert_eq!(SystemMetricRef::from_css_str(&"(".repeat(10_000)), None);
}
#[test]
fn system_metric_ref_resolve_maps_each_variant_to_its_own_field() {
let metrics = populated_metrics();
let expected = [
(SystemMetricRef::ButtonRadius, 1.0),
(SystemMetricRef::ButtonBorderWidth, 2.0),
(SystemMetricRef::ButtonPaddingHorizontal, 3.0),
(SystemMetricRef::ButtonPaddingVertical, 4.0),
(SystemMetricRef::TitlebarHeight, 5.0),
(SystemMetricRef::TitlebarButtonWidth, 6.0),
(SystemMetricRef::TitlebarPadding, 7.0),
(SystemMetricRef::SafeAreaTop, 8.0),
(SystemMetricRef::SafeAreaBottom, 9.0),
(SystemMetricRef::SafeAreaLeft, 10.0),
(SystemMetricRef::SafeAreaRight, 11.0),
];
for (r, px) in expected {
assert_eq!(
r.resolve(&metrics),
Some(PixelValue::px(px)),
"{r:?} resolved to the wrong field"
);
}
let empty = SystemMetrics::default();
for r in ALL_SYSTEM_REFS {
assert_eq!(r.resolve(&empty), None, "{r:?} must be None when unset");
}
}
#[test]
fn pixel_value_or_system_resolves_and_falls_back() {
let metrics = populated_metrics();
let empty = SystemMetrics::default();
let fallback = PixelValue::px(99.0);
let concrete = PixelValueOrSystem::value(PixelValue::px(10.0));
assert_eq!(concrete.resolve(&metrics, fallback), PixelValue::px(10.0));
assert_eq!(concrete.resolve(&empty, fallback), PixelValue::px(10.0));
let sys = PixelValueOrSystem::system(SystemMetricRef::ButtonRadius);
assert_eq!(sys.resolve(&metrics, fallback), PixelValue::px(1.0));
for r in ALL_SYSTEM_REFS {
assert_eq!(
PixelValueOrSystem::system(r).resolve(&empty, fallback),
fallback,
"{r:?} must fall back when the metric is unset"
);
}
let nan_fallback = PixelValue::px(f32::NAN);
assert_eq!(
sys.resolve(&empty, nan_fallback).number.get(),
0.0
);
assert_eq!(
PixelValueOrSystem::default(),
PixelValueOrSystem::Value(PixelValue::zero())
);
assert_eq!(
PixelValueOrSystem::from(PixelValue::em(2.0)),
PixelValueOrSystem::Value(PixelValue::em(2.0))
);
assert_eq!(
PixelValueOrSystem::default().resolve(&metrics, fallback),
PixelValue::zero()
);
}
#[test]
fn pixel_value_or_system_renders_both_arms() {
let concrete = PixelValueOrSystem::value(PixelValue::px(10.0));
assert_eq!(concrete.to_string(), "10px");
assert_eq!(as_css_value(concrete), "10px");
let sys = PixelValueOrSystem::system(SystemMetricRef::TitlebarHeight);
assert_eq!(sys.to_string(), "system:titlebar-height");
assert_eq!(as_css_value(sys), "system:titlebar-height");
assert_eq!(PixelValueOrSystem::default().to_string(), "0px");
for v in EXTREME_F32 {
let s = PixelValueOrSystem::value(PixelValue::px(v)).to_string();
assert!(!s.contains("NaN") && !s.contains("inf"), "leaked {s:?}");
}
}
}