use azul_css::props::{
basic::{
pixel::PT_TO_PX,
PixelValue, SizeMetric,
},
layout::dimensions::{CalcAstItem, CalcAstItemVec},
};
pub(super) const PX_PER_INCH: f32 = 96.0;
pub(super) const CM_PER_INCH: f32 = 2.54;
pub(super) const MM_PER_INCH: f32 = 25.4;
#[derive(Debug, Clone)]
#[repr(C)]
pub struct CalcResolveContext {
pub items: CalcAstItemVec,
pub em_size: f32,
pub rem_size: f32,
}
#[allow(variant_size_differences)]
#[derive(Clone, Debug)]
enum CalcFlatItem {
Num(f32),
Op(CalcOp),
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum CalcOp {
Add,
Sub,
Mul,
Div,
}
#[inline(never)] #[must_use] pub fn evaluate_calc(ctx: &CalcResolveContext, basis: f32) -> f32 {
evaluate_calc_ast(ctx.items.as_slice(), basis, ctx.em_size, ctx.rem_size)
}
#[inline(never)] fn evaluate_calc_ast(
items: &[CalcAstItem],
basis: f32,
em_size: f32,
rem_size: f32,
) -> f32 {
let mut flat: Vec<CalcFlatItem> = Vec::with_capacity(items.len());
let mut i = 0;
while i < items.len() {
match &items[i] {
CalcAstItem::Value(pv) => {
flat.push(CalcFlatItem::Num(resolve_pixel_value(
pv, basis, em_size, rem_size,
)));
}
CalcAstItem::Add => flat.push(CalcFlatItem::Op(CalcOp::Add)),
CalcAstItem::Sub => flat.push(CalcFlatItem::Op(CalcOp::Sub)),
CalcAstItem::Mul => flat.push(CalcFlatItem::Op(CalcOp::Mul)),
CalcAstItem::Div => flat.push(CalcFlatItem::Op(CalcOp::Div)),
CalcAstItem::BraceOpen => {
let start = i + 1;
let mut depth = 1u32;
let mut j = start;
while j < items.len() && depth > 0 {
match &items[j] {
CalcAstItem::BraceOpen => depth += 1,
CalcAstItem::BraceClose => depth -= 1,
_ => {}
}
if depth > 0 {
j += 1;
}
}
let sub_val = evaluate_calc_ast(&items[start..j], basis, em_size, rem_size);
flat.push(CalcFlatItem::Num(sub_val));
i = j; }
CalcAstItem::BraceClose => { }
}
i += 1;
}
let mut pass2: Vec<CalcFlatItem> = Vec::with_capacity(flat.len());
let mut k = 0;
while k < flat.len() {
if let CalcFlatItem::Op(op @ (CalcOp::Mul | CalcOp::Div)) = &flat[k] {
if let (Some(CalcFlatItem::Num(lhs)), Some(CalcFlatItem::Num(rhs))) =
(pass2.last(), flat.get(k + 1))
{
let result = match op {
CalcOp::Mul => lhs * rhs,
CalcOp::Div => {
if *rhs == 0.0 {
0.0
} else {
lhs / rhs
}
}
_ => unreachable!(),
};
*pass2.last_mut().unwrap() = CalcFlatItem::Num(result);
k += 2; continue;
}
}
pass2.push(flat[k].clone());
k += 1;
}
let mut result = match pass2.first() {
Some(CalcFlatItem::Num(v)) => *v,
_ => return 0.0,
};
let mut m = 1;
while m < pass2.len() {
if let (CalcFlatItem::Op(op), Some(CalcFlatItem::Num(rhs))) =
(&pass2[m], pass2.get(m + 1))
{
match op {
CalcOp::Add => result += rhs,
CalcOp::Sub => result -= rhs,
_ => {} }
m += 2;
} else {
m += 1;
}
}
result
}
#[must_use] pub fn resolve_pixel_value(
pv: &PixelValue,
basis: f32,
em_size: f32,
rem_size: f32,
) -> f32 {
match pv.metric {
SizeMetric::Px => pv.number.get(),
SizeMetric::Pt => pv.number.get() * PT_TO_PX,
SizeMetric::In => pv.number.get() * PX_PER_INCH,
SizeMetric::Cm => pv.number.get() * PX_PER_INCH / CM_PER_INCH,
SizeMetric::Mm => pv.number.get() * PX_PER_INCH / MM_PER_INCH,
SizeMetric::Em => pv.number.get() * em_size,
SizeMetric::Rem => pv.number.get() * rem_size,
SizeMetric::Percent => basis * (pv.number.get() / 100.0),
SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
pv.number.get()
}
}
}
#[inline(never)] #[must_use] pub fn resolve_pixel_value_with_viewport(
pv: &PixelValue,
basis: f32,
em_size: f32,
rem_size: f32,
viewport_width: f32,
viewport_height: f32,
) -> f32 {
match pv.metric {
SizeMetric::Vw => pv.number.get() / 100.0 * viewport_width,
SizeMetric::Vh => pv.number.get() / 100.0 * viewport_height,
SizeMetric::Vmin => pv.number.get() / 100.0 * viewport_width.min(viewport_height),
SizeMetric::Vmax => pv.number.get() / 100.0 * viewport_width.max(viewport_height),
_ => resolve_pixel_value(pv, basis, em_size, rem_size),
}
}
#[must_use] pub fn resolve_pixel_value_no_percent(
pv: &PixelValue,
em_size: f32,
rem_size: f32,
) -> Option<f32> {
match pv.metric {
SizeMetric::Px => Some(pv.number.get()),
SizeMetric::Pt => Some(pv.number.get() * PT_TO_PX),
SizeMetric::In => Some(pv.number.get() * PX_PER_INCH),
SizeMetric::Cm => Some(pv.number.get() * PX_PER_INCH / CM_PER_INCH),
SizeMetric::Mm => Some(pv.number.get() * PX_PER_INCH / MM_PER_INCH),
SizeMetric::Em => Some(pv.number.get() * em_size),
SizeMetric::Rem => Some(pv.number.get() * rem_size),
SizeMetric::Percent
| SizeMetric::Vw
| SizeMetric::Vh
| SizeMetric::Vmin
| SizeMetric::Vmax => None,
}
}
#[must_use] pub fn resolve_pixel_value_no_percent_with_viewport(
pv: &PixelValue,
em_size: f32,
rem_size: f32,
viewport_width: f32,
viewport_height: f32,
) -> Option<f32> {
match pv.metric {
SizeMetric::Vw => Some(pv.number.get() / 100.0 * viewport_width),
SizeMetric::Vh => Some(pv.number.get() / 100.0 * viewport_height),
SizeMetric::Vmin => Some(pv.number.get() / 100.0 * viewport_width.min(viewport_height)),
SizeMetric::Vmax => Some(pv.number.get() / 100.0 * viewport_width.max(viewport_height)),
_ => resolve_pixel_value_no_percent(pv, em_size, rem_size),
}
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::cast_precision_loss, clippy::unreadable_literal)]
mod autotest_generated {
use azul_css::props::basic::FP_PRECISION_MULTIPLIER;
use super::*;
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 ABSOLUTE_METRICS: [SizeMetric; 7] = [
SizeMetric::Px,
SizeMetric::Pt,
SizeMetric::Em,
SizeMetric::Rem,
SizeMetric::In,
SizeMetric::Cm,
SizeMetric::Mm,
];
const VIEWPORT_METRICS: [SizeMetric; 4] = [
SizeMetric::Vw,
SizeMetric::Vh,
SizeMetric::Vmin,
SizeMetric::Vmax,
];
fn saturated_magnitude() -> f32 {
(isize::MAX as f32) / FP_PRECISION_MULTIPLIER
}
fn assert_close(actual: f32, expected: f32) {
let tolerance = 1e-3 * expected.abs().max(1.0);
assert!(
(actual - expected).abs() <= tolerance,
"expected ~{expected}, got {actual}",
);
}
fn val(metric: SizeMetric, n: f32) -> CalcAstItem {
CalcAstItem::Value(PixelValue::from_metric(metric, n))
}
fn px(n: f32) -> CalcAstItem {
val(SizeMetric::Px, n)
}
const EM: f32 = 16.0;
const REM: f32 = 10.0;
fn eval(items: &[CalcAstItem], basis: f32) -> f32 {
evaluate_calc_ast(items, basis, EM, REM)
}
fn ctx(items: Vec<CalcAstItem>, em_size: f32, rem_size: f32) -> CalcResolveContext {
CalcResolveContext {
items: CalcAstItemVec::from(items),
em_size,
rem_size,
}
}
#[test]
fn pixel_value_number_is_always_finite_even_for_nan_and_inf() {
for metric in ALL_METRICS {
for hostile in [
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
f32::MAX,
f32::MIN,
f32::MIN_POSITIVE,
-0.0,
] {
let n = PixelValue::from_metric(metric, hostile).number.get();
assert!(
n.is_finite(),
"PixelValue::from_metric({metric:?}, {hostile}) produced non-finite {n}",
);
}
}
}
#[test]
fn pixel_value_nan_encodes_as_zero() {
assert_eq!(PixelValue::px(f32::NAN).number.get(), 0.0);
}
#[test]
fn pixel_value_infinities_saturate_to_fixed_point_bounds() {
let sat = saturated_magnitude();
assert_close(PixelValue::px(f32::INFINITY).number.get(), sat);
assert_close(PixelValue::px(f32::NEG_INFINITY).number.get(), -sat);
assert_close(PixelValue::px(f32::MAX).number.get(), sat);
assert_close(PixelValue::px(f32::MIN).number.get(), -sat);
}
#[test]
fn resolve_pixel_value_zero_is_zero_for_every_metric() {
for metric in ALL_METRICS {
let pv = PixelValue::from_metric(metric, 0.0);
let got = resolve_pixel_value(&pv, 500.0, EM, REM);
assert_eq!(got, 0.0, "0 {metric:?} should resolve to 0px, got {got}");
}
}
#[test]
fn resolve_pixel_value_absolute_unit_conversions() {
assert_eq!(resolve_pixel_value(&PixelValue::px(10.0), 0.0, EM, REM), 10.0);
assert_close(
resolve_pixel_value(&PixelValue::pt(12.0), 0.0, EM, REM),
12.0 * PT_TO_PX,
);
assert_close(resolve_pixel_value(&PixelValue::inch(1.0), 0.0, EM, REM), 96.0);
assert_close(resolve_pixel_value(&PixelValue::cm(2.54), 0.0, EM, REM), 96.0);
assert_close(resolve_pixel_value(&PixelValue::mm(25.4), 0.0, EM, REM), 96.0);
}
#[test]
fn resolve_pixel_value_em_and_rem_use_their_own_font_size() {
assert_eq!(resolve_pixel_value(&PixelValue::em(2.0), 0.0, 16.0, 10.0), 32.0);
assert_eq!(resolve_pixel_value(&PixelValue::rem(2.0), 0.0, 16.0, 10.0), 20.0);
}
#[test]
fn resolve_pixel_value_percent_uses_basis() {
assert_eq!(resolve_pixel_value(&PixelValue::percent(50.0), 200.0, EM, REM), 100.0);
assert_eq!(resolve_pixel_value(&PixelValue::percent(0.0), 200.0, EM, REM), 0.0);
assert_eq!(resolve_pixel_value(&PixelValue::percent(100.0), 0.0, EM, REM), 0.0);
}
#[test]
fn resolve_pixel_value_handles_negative_inputs_deterministically() {
assert_eq!(resolve_pixel_value(&PixelValue::px(-10.0), 0.0, EM, REM), -10.0);
assert_eq!(resolve_pixel_value(&PixelValue::em(-2.0), 0.0, 16.0, REM), -32.0);
assert_eq!(resolve_pixel_value(&PixelValue::percent(-50.0), 200.0, EM, REM), -100.0);
assert_eq!(resolve_pixel_value(&PixelValue::percent(50.0), -200.0, EM, REM), -100.0);
}
#[test]
fn resolve_pixel_value_viewport_units_fall_back_to_raw_number() {
for metric in VIEWPORT_METRICS {
let pv = PixelValue::from_metric(metric, 50.0);
assert_eq!(
resolve_pixel_value(&pv, 1000.0, EM, REM),
50.0,
"{metric:?} should fall back to its raw number",
);
}
}
#[test]
fn resolve_pixel_value_is_finite_for_any_pixel_value_in_a_sane_context() {
for metric in ALL_METRICS {
for n in [0.0, -0.0, 1.0, -1.0, f32::MAX, f32::MIN, f32::NAN, f32::INFINITY] {
let pv = PixelValue::from_metric(metric, n);
for basis in [0.0_f32, -1000.0, 1920.0, 1_000_000.0] {
let got = resolve_pixel_value(&pv, basis, EM, REM);
assert!(
got.is_finite(),
"{metric:?} {n} @ basis {basis} produced non-finite {got}",
);
}
}
}
}
#[test]
fn resolve_pixel_value_saturated_percent_of_huge_basis_overflows_to_inf_not_panic() {
let hundred = resolve_pixel_value(&PixelValue::percent(100.0), f32::MAX, EM, REM);
assert_eq!(hundred, f32::MAX);
let two_hundred = resolve_pixel_value(&PixelValue::percent(200.0), f32::MAX, EM, REM);
assert!(
two_hundred.is_infinite() && two_hundred.is_sign_positive(),
"expected +inf on overflow, got {two_hundred}",
);
}
#[test]
fn resolve_pixel_value_nan_basis_only_pollutes_percentages() {
assert_eq!(resolve_pixel_value(&PixelValue::px(10.0), f32::NAN, EM, REM), 10.0);
assert_eq!(resolve_pixel_value(&PixelValue::em(1.0), f32::NAN, 16.0, REM), 16.0);
assert!(resolve_pixel_value(&PixelValue::percent(50.0), f32::NAN, EM, REM).is_nan());
}
#[test]
fn resolve_pixel_value_zero_times_infinite_context_is_nan_not_zero() {
assert!(resolve_pixel_value(&PixelValue::em(0.0), 0.0, f32::INFINITY, REM).is_nan());
assert!(resolve_pixel_value(&PixelValue::percent(0.0), f32::INFINITY, EM, REM).is_nan());
let inf = resolve_pixel_value(&PixelValue::em(1.0), 0.0, f32::INFINITY, REM);
assert!(inf.is_infinite() && inf.is_sign_positive());
}
#[test]
fn resolve_with_viewport_resolves_viewport_units() {
let (vw, vh) = (1000.0_f32, 800.0_f32);
let r = |m: SizeMetric, n: f32| {
resolve_pixel_value_with_viewport(&PixelValue::from_metric(m, n), 0.0, EM, REM, vw, vh)
};
assert_eq!(r(SizeMetric::Vw, 50.0), 500.0);
assert_eq!(r(SizeMetric::Vh, 50.0), 400.0);
assert_eq!(r(SizeMetric::Vmin, 100.0), 800.0);
assert_eq!(r(SizeMetric::Vmax, 100.0), 1000.0);
}
#[test]
fn resolve_with_viewport_delegates_non_viewport_metrics_verbatim() {
for metric in ALL_METRICS {
if VIEWPORT_METRICS.contains(&metric) {
continue;
}
let pv = PixelValue::from_metric(metric, 33.75);
assert_eq!(
resolve_pixel_value_with_viewport(&pv, 500.0, EM, REM, 1920.0, 1080.0),
resolve_pixel_value(&pv, 500.0, EM, REM),
"{metric:?} must not be affected by viewport context",
);
}
}
#[test]
fn resolve_with_viewport_zero_and_negative_viewport() {
for metric in VIEWPORT_METRICS {
let pv = PixelValue::from_metric(metric, 50.0);
assert_eq!(
resolve_pixel_value_with_viewport(&pv, 0.0, EM, REM, 0.0, 0.0),
0.0,
"{metric:?} against a 0x0 viewport should be 0px",
);
}
let vw50 = PixelValue::from_metric(SizeMetric::Vw, 50.0);
assert_eq!(
resolve_pixel_value_with_viewport(&vw50, 0.0, EM, REM, -100.0, -100.0),
-50.0,
);
let vmin100 = PixelValue::from_metric(SizeMetric::Vmin, 100.0);
let vmax100 = PixelValue::from_metric(SizeMetric::Vmax, 100.0);
assert_eq!(
resolve_pixel_value_with_viewport(&vmin100, 0.0, EM, REM, -100.0, -50.0),
-100.0,
);
assert_eq!(
resolve_pixel_value_with_viewport(&vmax100, 0.0, EM, REM, -100.0, -50.0),
-50.0,
);
}
#[test]
fn resolve_with_viewport_nan_viewport_is_swallowed_by_vmin_vmax_but_not_vw_vh() {
let vmin = PixelValue::from_metric(SizeMetric::Vmin, 100.0);
let vmax = PixelValue::from_metric(SizeMetric::Vmax, 100.0);
assert_eq!(
resolve_pixel_value_with_viewport(&vmin, 0.0, EM, REM, f32::NAN, 800.0),
800.0,
);
assert_eq!(
resolve_pixel_value_with_viewport(&vmax, 0.0, EM, REM, f32::NAN, 800.0),
800.0,
);
let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
assert!(resolve_pixel_value_with_viewport(&vw, 0.0, EM, REM, f32::NAN, 800.0).is_nan());
assert!(
resolve_pixel_value_with_viewport(&vmin, 0.0, EM, REM, f32::NAN, f32::NAN).is_nan()
);
}
#[test]
fn resolve_with_viewport_infinite_viewport_does_not_panic() {
let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
let got = resolve_pixel_value_with_viewport(&vw, 0.0, EM, REM, f32::INFINITY, 0.0);
assert!(got.is_infinite() && got.is_sign_positive());
let vw0 = PixelValue::from_metric(SizeMetric::Vw, 0.0);
assert!(
resolve_pixel_value_with_viewport(&vw0, 0.0, EM, REM, f32::INFINITY, 0.0).is_nan()
);
}
#[test]
fn no_percent_returns_none_for_percent_and_viewport_units() {
for metric in [
SizeMetric::Percent,
SizeMetric::Vw,
SizeMetric::Vh,
SizeMetric::Vmin,
SizeMetric::Vmax,
] {
let pv = PixelValue::from_metric(metric, 50.0);
assert_eq!(
resolve_pixel_value_no_percent(&pv, EM, REM),
None,
"{metric:?} needs layout context, so it must be None",
);
}
}
#[test]
fn no_percent_agrees_with_resolve_pixel_value_on_absolute_metrics() {
for metric in ABSOLUTE_METRICS {
for n in [0.0, 1.0, -7.5, 1234.5, f32::MAX, f32::MIN, f32::NAN] {
let pv = PixelValue::from_metric(metric, n);
let some = resolve_pixel_value_no_percent(&pv, EM, REM);
assert_eq!(
some,
Some(resolve_pixel_value(&pv, 999.0, EM, REM)),
"{metric:?} {n} disagrees between the two resolvers",
);
assert!(some.unwrap().is_finite());
}
}
}
#[test]
fn no_percent_with_viewport_returns_none_only_for_percent() {
for metric in ALL_METRICS {
let pv = PixelValue::from_metric(metric, 50.0);
let got = resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 1000.0, 800.0);
if metric == SizeMetric::Percent {
assert_eq!(got, None, "% must stay None without a basis");
} else {
assert!(got.is_some(), "{metric:?} should resolve with a viewport");
assert!(got.unwrap().is_finite());
}
}
}
#[test]
fn no_percent_with_viewport_agrees_with_resolve_with_viewport() {
for metric in ALL_METRICS {
if metric == SizeMetric::Percent {
continue;
}
let pv = PixelValue::from_metric(metric, 25.0);
assert_eq!(
resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 1920.0, 1080.0),
Some(resolve_pixel_value_with_viewport(&pv, 0.0, EM, REM, 1920.0, 1080.0)),
"{metric:?} disagrees between the viewport resolvers",
);
}
}
#[test]
fn no_percent_with_viewport_zero_viewport_yields_some_zero_not_none() {
for metric in VIEWPORT_METRICS {
let pv = PixelValue::from_metric(metric, 100.0);
assert_eq!(
resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 0.0, 0.0),
Some(0.0),
"{metric:?} against a 0x0 viewport is a defined 0px, not None",
);
}
}
#[test]
fn eval_empty_ast_is_zero() {
assert_eq!(eval(&[], 500.0), 0.0);
}
#[test]
fn eval_single_value() {
assert_eq!(eval(&[px(42.0)], 500.0), 42.0);
assert_eq!(eval(&[val(SizeMetric::Percent, 10.0)], 500.0), 50.0);
}
#[test]
fn eval_basic_addition_and_subtraction() {
let items = [val(SizeMetric::Percent, 100.0), CalcAstItem::Sub, px(20.0)];
assert_eq!(eval(&items, 500.0), 480.0);
}
#[test]
fn eval_add_and_sub_are_left_associative() {
let items = [px(10.0), CalcAstItem::Sub, px(3.0), CalcAstItem::Sub, px(2.0)];
assert_eq!(eval(&items, 0.0), 5.0, "must be (10-3)-2, not 10-(3-2)");
}
#[test]
fn eval_div_is_left_associative() {
let items = [px(10.0), CalcAstItem::Div, px(4.0), CalcAstItem::Div, px(2.0)];
assert_eq!(eval(&items, 0.0), 1.25, "must be (10/4)/2, not 10/(4/2)");
}
#[test]
fn eval_mul_binds_tighter_than_add() {
let items = [px(1.0), CalcAstItem::Add, px(2.0), CalcAstItem::Mul, px(3.0)];
assert_eq!(eval(&items, 0.0), 7.0);
let items = [px(2.0), CalcAstItem::Mul, px(3.0), CalcAstItem::Add, px(4.0)];
assert_eq!(eval(&items, 0.0), 10.0);
}
#[test]
fn eval_div_binds_tighter_than_sub() {
let items = [px(10.0), CalcAstItem::Sub, px(6.0), CalcAstItem::Div, px(2.0)];
assert_eq!(eval(&items, 0.0), 7.0);
}
#[test]
fn eval_braces_override_precedence() {
let items = [
CalcAstItem::BraceOpen,
px(1.0),
CalcAstItem::Add,
px(2.0),
CalcAstItem::BraceClose,
CalcAstItem::Mul,
px(3.0),
];
assert_eq!(eval(&items, 0.0), 9.0);
}
#[test]
fn eval_nested_braces() {
let items = [
CalcAstItem::BraceOpen,
CalcAstItem::BraceOpen,
px(2.0),
CalcAstItem::Add,
px(3.0),
CalcAstItem::BraceClose,
CalcAstItem::Mul,
CalcAstItem::BraceOpen,
px(4.0),
CalcAstItem::Sub,
px(2.0),
CalcAstItem::BraceClose,
CalcAstItem::BraceClose,
];
assert_eq!(eval(&items, 0.0), 10.0);
}
#[test]
fn eval_empty_braces_are_zero() {
let items = [CalcAstItem::BraceOpen, CalcAstItem::BraceClose];
assert_eq!(eval(&items, 500.0), 0.0);
let items = [
px(10.0),
CalcAstItem::Add,
CalcAstItem::BraceOpen,
CalcAstItem::BraceClose,
];
assert_eq!(eval(&items, 500.0), 10.0);
}
#[test]
fn eval_percent_inside_braces_still_sees_the_basis() {
let items = [
CalcAstItem::BraceOpen,
val(SizeMetric::Percent, 100.0),
CalcAstItem::Sub,
px(20.0),
CalcAstItem::BraceClose,
CalcAstItem::Div,
px(4.0),
];
assert_eq!(eval(&items, 500.0), 120.0);
}
#[test]
fn eval_em_and_rem_come_from_the_evaluator_args_not_the_basis() {
let items = [val(SizeMetric::Em, 2.0), CalcAstItem::Add, val(SizeMetric::Rem, 1.0)];
assert_eq!(evaluate_calc_ast(&items, 0.0, 10.0, 20.0), 40.0);
}
#[test]
fn eval_unmatched_brace_open_does_not_panic_or_hang() {
assert_eq!(eval(&[CalcAstItem::BraceOpen], 500.0), 0.0);
assert_eq!(eval(&[CalcAstItem::BraceOpen, px(5.0)], 500.0), 5.0);
let items = [
px(1.0),
CalcAstItem::Add,
CalcAstItem::BraceOpen,
px(2.0),
];
assert_eq!(eval(&items, 500.0), 3.0);
}
#[test]
fn eval_stray_brace_close_is_ignored() {
let items = [
px(5.0),
CalcAstItem::BraceClose,
CalcAstItem::Add,
px(3.0),
];
assert_eq!(eval(&items, 0.0), 8.0);
assert_eq!(eval(&[CalcAstItem::BraceClose, px(7.0)], 0.0), 7.0);
}
#[test]
fn eval_leading_operator_collapses_to_zero() {
assert_eq!(eval(&[CalcAstItem::Mul, px(5.0)], 0.0), 0.0);
assert_eq!(eval(&[CalcAstItem::Add, px(5.0)], 0.0), 0.0);
assert_eq!(eval(&[CalcAstItem::Div, px(5.0)], 0.0), 0.0);
}
#[test]
fn eval_trailing_operator_keeps_the_left_hand_side() {
for op in [
CalcAstItem::Add,
CalcAstItem::Sub,
CalcAstItem::Mul,
CalcAstItem::Div,
] {
assert_eq!(eval(&[px(10.0), op], 0.0), 10.0, "trailing {op:?} dropped the lhs");
}
}
#[test]
fn eval_operator_only_ast_is_zero() {
let items = [
CalcAstItem::Add,
CalcAstItem::Sub,
CalcAstItem::Mul,
CalcAstItem::Div,
];
assert_eq!(eval(&items, 500.0), 0.0);
}
#[test]
fn eval_adjacent_values_without_an_operator_keep_only_the_first() {
assert_eq!(eval(&[px(10.0), px(20.0)], 0.0), 10.0);
}
#[test]
fn eval_adjacent_operators_do_not_panic() {
let items = [
px(1.0),
CalcAstItem::Add,
CalcAstItem::Mul,
px(2.0),
];
assert_eq!(eval(&items, 0.0), 1.0);
}
#[test]
fn eval_deeply_nested_braces_do_not_overflow_the_stack() {
const DEPTH: usize = 256;
let mut items: Vec<CalcAstItem> = vec![CalcAstItem::BraceOpen; DEPTH];
items.push(px(1.0));
items.resize(DEPTH * 2 + 1, CalcAstItem::BraceClose);
assert_eq!(eval(&items, 0.0), 1.0);
}
#[test]
fn eval_unbalanced_deep_braces_terminate() {
let items = vec![CalcAstItem::BraceOpen; 256];
assert_eq!(eval(&items, 0.0), 0.0);
}
#[test]
fn eval_long_flat_ast_terminates_with_the_right_sum() {
const TERMS: usize = 1000;
let mut items = Vec::with_capacity(TERMS * 2 - 1);
for i in 0..TERMS {
if i > 0 {
items.push(CalcAstItem::Add);
}
items.push(px(1.0));
}
assert_eq!(eval(&items, 0.0), TERMS as f32);
}
#[test]
fn eval_division_by_zero_is_guarded_to_zero_not_infinity() {
let items = [px(10.0), CalcAstItem::Div, px(0.0)];
let got = eval(&items, 0.0);
assert_eq!(got, 0.0);
assert!(got.is_finite(), "10px / 0px must not be an infinity");
}
#[test]
fn eval_division_by_a_zero_valued_percent_is_also_guarded() {
let items = [px(10.0), CalcAstItem::Div, val(SizeMetric::Percent, 100.0)];
assert_eq!(eval(&items, 0.0), 0.0);
let got = eval(&items, -0.0);
assert_eq!(got, 0.0);
assert!(got.is_finite(), "10px / (100% of -0) must not be -inf");
}
#[test]
fn eval_division_by_nan_is_not_caught_by_the_zero_guard() {
let items = [px(10.0), CalcAstItem::Div, val(SizeMetric::Percent, 100.0)];
assert!(eval(&items, f32::NAN).is_nan());
}
#[test]
fn eval_divisor_below_fixed_point_precision_truncates_into_the_zero_guard() {
assert_eq!(PixelValue::px(0.0001).number.get(), 0.0);
let items = [px(10.0), CalcAstItem::Div, px(0.0001)];
assert_eq!(eval(&items, 0.0), 0.0);
assert_close(PixelValue::px(0.001).number.get(), 0.001);
let items = [px(10.0), CalcAstItem::Div, px(0.001)];
let got = eval(&items, 0.0);
assert!(got.is_finite(), "10px / 0.001px should be finite, got {got}");
assert_close(got, 10_000.0);
}
#[test]
fn eval_multiplication_overflow_saturates_to_infinity_without_panicking() {
let mut items = vec![px(f32::MAX)];
for _ in 0..7 {
items.push(CalcAstItem::Mul);
items.push(px(f32::MAX));
}
let got = eval(&items, 0.0);
assert!(
got.is_infinite() && got.is_sign_positive(),
"expected +inf on overflow, got {got}",
);
}
#[test]
fn eval_inf_minus_inf_is_nan_not_a_panic() {
let items = [
val(SizeMetric::Percent, 100.0),
CalcAstItem::Sub,
val(SizeMetric::Percent, 100.0),
];
assert!(eval(&items, f32::INFINITY).is_nan());
}
#[test]
fn eval_nan_basis_does_not_leak_into_an_absolute_expression() {
let items = [px(10.0), CalcAstItem::Add, px(5.0)];
assert_eq!(eval(&items, f32::NAN), 15.0);
assert_eq!(eval(&items, f32::INFINITY), 15.0);
}
#[test]
fn eval_negative_results_are_not_clamped() {
let items = [px(20.0), CalcAstItem::Sub, val(SizeMetric::Percent, 100.0)];
assert_eq!(eval(&items, 500.0), -480.0);
}
#[test]
fn eval_viewport_units_inside_calc_use_the_no_viewport_fallback() {
let items = [val(SizeMetric::Vw, 50.0), CalcAstItem::Add, px(0.0)];
assert_eq!(eval(&items, 1000.0), 50.0);
}
#[test]
fn evaluate_calc_empty_context_is_zero() {
let c = ctx(Vec::new(), 16.0, 10.0);
assert_eq!(evaluate_calc(&c, 500.0), 0.0);
assert_eq!(evaluate_calc(&c, f32::NAN), 0.0);
}
#[test]
fn evaluate_calc_matches_the_ast_evaluator_it_wraps() {
let items = vec![
val(SizeMetric::Percent, 100.0),
CalcAstItem::Sub,
CalcAstItem::BraceOpen,
val(SizeMetric::Em, 2.0),
CalcAstItem::Add,
val(SizeMetric::Rem, 1.0),
CalcAstItem::BraceClose,
];
let c = ctx(items.clone(), 12.0, 20.0);
assert_eq!(evaluate_calc(&c, 500.0), 456.0);
assert_eq!(evaluate_calc(&c, 500.0), evaluate_calc_ast(&items, 500.0, 12.0, 20.0));
}
#[test]
fn evaluate_calc_forwards_its_own_em_and_rem_sizes_without_swapping_them() {
let items = vec![val(SizeMetric::Em, 2.0), CalcAstItem::Add, val(SizeMetric::Rem, 1.0)];
assert_eq!(evaluate_calc(&ctx(items.clone(), 10.0, 20.0), 0.0), 40.0);
assert_eq!(evaluate_calc(&ctx(items, 20.0, 10.0), 0.0), 50.0);
}
#[test]
fn evaluate_calc_with_hostile_font_sizes_does_not_panic() {
let items = vec![val(SizeMetric::Em, 2.0), CalcAstItem::Add, px(1.0)];
assert!(evaluate_calc(&ctx(items.clone(), f32::NAN, 10.0), 0.0).is_nan());
let inf = evaluate_calc(&ctx(items.clone(), f32::INFINITY, 10.0), 0.0);
assert!(inf.is_infinite() && inf.is_sign_positive());
assert_eq!(evaluate_calc(&ctx(items, 0.0, 10.0), 0.0), 1.0);
}
#[test]
fn evaluate_calc_is_pure_and_repeatable() {
let c = ctx(
vec![val(SizeMetric::Percent, 50.0), CalcAstItem::Add, px(10.0)],
16.0,
10.0,
);
assert_eq!(evaluate_calc(&c, 100.0), 60.0);
assert_eq!(evaluate_calc(&c, 200.0), 110.0);
assert_eq!(evaluate_calc(&c, 100.0), 60.0);
}
}