1use azul_css::props::{
11 basic::{
12 pixel::PT_TO_PX,
13 PixelValue, SizeMetric,
14 },
15 layout::dimensions::{CalcAstItem, CalcAstItemVec},
16};
17
18pub(super) const PX_PER_INCH: f32 = 96.0;
20pub(super) const CM_PER_INCH: f32 = 2.54;
22pub(super) const MM_PER_INCH: f32 = 25.4;
24
25#[derive(Debug, Clone)]
31#[repr(C)]
32pub struct CalcResolveContext {
33 pub items: CalcAstItemVec,
35 pub em_size: f32,
37 pub rem_size: f32,
39}
40#[allow(variant_size_differences)]
44#[derive(Clone, Debug)]
46enum CalcFlatItem {
47 Num(f32),
48 Op(CalcOp),
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
53enum CalcOp {
54 Add,
55 Sub,
56 Mul,
57 Div,
58}
59
60#[inline(never)] #[must_use] pub fn evaluate_calc(ctx: &CalcResolveContext, basis: f32) -> f32 {
64 evaluate_calc_ast(ctx.items.as_slice(), basis, ctx.em_size, ctx.rem_size)
65}
66
67#[inline(never)] fn evaluate_calc_ast(
79 items: &[CalcAstItem],
80 basis: f32,
81 em_size: f32,
82 rem_size: f32,
83) -> f32 {
84 let mut flat: Vec<CalcFlatItem> = Vec::with_capacity(items.len());
86 let mut i = 0;
87 while i < items.len() {
88 match &items[i] {
89 CalcAstItem::Value(pv) => {
90 flat.push(CalcFlatItem::Num(resolve_pixel_value(
91 pv, basis, em_size, rem_size,
92 )));
93 }
94 CalcAstItem::Add => flat.push(CalcFlatItem::Op(CalcOp::Add)),
95 CalcAstItem::Sub => flat.push(CalcFlatItem::Op(CalcOp::Sub)),
96 CalcAstItem::Mul => flat.push(CalcFlatItem::Op(CalcOp::Mul)),
97 CalcAstItem::Div => flat.push(CalcFlatItem::Op(CalcOp::Div)),
98 CalcAstItem::BraceOpen => {
99 let start = i + 1;
101 let mut depth = 1u32;
102 let mut j = start;
103 while j < items.len() && depth > 0 {
104 match &items[j] {
105 CalcAstItem::BraceOpen => depth += 1,
106 CalcAstItem::BraceClose => depth -= 1,
107 _ => {}
108 }
109 if depth > 0 {
110 j += 1;
111 }
112 }
113 let sub_val = evaluate_calc_ast(&items[start..j], basis, em_size, rem_size);
115 flat.push(CalcFlatItem::Num(sub_val));
116 i = j; }
118 CalcAstItem::BraceClose => { }
119 }
120 i += 1;
121 }
122
123 let mut pass2: Vec<CalcFlatItem> = Vec::with_capacity(flat.len());
125 let mut k = 0;
126 while k < flat.len() {
127 if let CalcFlatItem::Op(op @ (CalcOp::Mul | CalcOp::Div)) = &flat[k] {
128 if let (Some(CalcFlatItem::Num(lhs)), Some(CalcFlatItem::Num(rhs))) =
130 (pass2.last(), flat.get(k + 1))
131 {
132 let result = match op {
133 CalcOp::Mul => lhs * rhs,
134 CalcOp::Div => {
135 if *rhs == 0.0 {
136 0.0
137 } else {
138 lhs / rhs
139 }
140 }
141 _ => unreachable!(),
142 };
143 *pass2.last_mut().unwrap() = CalcFlatItem::Num(result);
144 k += 2; continue;
146 }
147 }
148 pass2.push(flat[k].clone());
149 k += 1;
150 }
151
152 let mut result = match pass2.first() {
154 Some(CalcFlatItem::Num(v)) => *v,
155 _ => return 0.0,
156 };
157 let mut m = 1;
158 while m < pass2.len() {
159 if let (CalcFlatItem::Op(op), Some(CalcFlatItem::Num(rhs))) =
160 (&pass2[m], pass2.get(m + 1))
161 {
162 match op {
163 CalcOp::Add => result += rhs,
164 CalcOp::Sub => result -= rhs,
165 _ => {} }
167 m += 2;
168 } else {
169 m += 1;
170 }
171 }
172
173 result
174}
175
176#[must_use] pub fn resolve_pixel_value(
187 pv: &PixelValue,
188 basis: f32,
189 em_size: f32,
190 rem_size: f32,
191) -> f32 {
192 match pv.metric {
193 SizeMetric::Px => pv.number.get(),
194 SizeMetric::Pt => pv.number.get() * PT_TO_PX,
195 SizeMetric::In => pv.number.get() * PX_PER_INCH,
196 SizeMetric::Cm => pv.number.get() * PX_PER_INCH / CM_PER_INCH,
197 SizeMetric::Mm => pv.number.get() * PX_PER_INCH / MM_PER_INCH,
198 SizeMetric::Em => pv.number.get() * em_size,
199 SizeMetric::Rem => pv.number.get() * rem_size,
200 SizeMetric::Percent => basis * (pv.number.get() / 100.0),
201 SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
202 pv.number.get()
204 }
205 }
206}
207
208#[inline(never)] #[must_use] pub fn resolve_pixel_value_with_viewport(
211 pv: &PixelValue,
212 basis: f32,
213 em_size: f32,
214 rem_size: f32,
215 viewport_width: f32,
216 viewport_height: f32,
217) -> f32 {
218 match pv.metric {
219 SizeMetric::Vw => pv.number.get() / 100.0 * viewport_width,
220 SizeMetric::Vh => pv.number.get() / 100.0 * viewport_height,
221 SizeMetric::Vmin => pv.number.get() / 100.0 * viewport_width.min(viewport_height),
222 SizeMetric::Vmax => pv.number.get() / 100.0 * viewport_width.max(viewport_height),
223 _ => resolve_pixel_value(pv, basis, em_size, rem_size),
224 }
225}
226
227#[must_use] pub fn resolve_pixel_value_no_percent(
229 pv: &PixelValue,
230 em_size: f32,
231 rem_size: f32,
232) -> Option<f32> {
233 match pv.metric {
234 SizeMetric::Px => Some(pv.number.get()),
235 SizeMetric::Pt => Some(pv.number.get() * PT_TO_PX),
236 SizeMetric::In => Some(pv.number.get() * PX_PER_INCH),
237 SizeMetric::Cm => Some(pv.number.get() * PX_PER_INCH / CM_PER_INCH),
238 SizeMetric::Mm => Some(pv.number.get() * PX_PER_INCH / MM_PER_INCH),
239 SizeMetric::Em => Some(pv.number.get() * em_size),
240 SizeMetric::Rem => Some(pv.number.get() * rem_size),
241 SizeMetric::Percent
242 | SizeMetric::Vw
243 | SizeMetric::Vh
244 | SizeMetric::Vmin
245 | SizeMetric::Vmax => None,
246 }
247}
248
249#[must_use] pub fn resolve_pixel_value_no_percent_with_viewport(
252 pv: &PixelValue,
253 em_size: f32,
254 rem_size: f32,
255 viewport_width: f32,
256 viewport_height: f32,
257) -> Option<f32> {
258 match pv.metric {
259 SizeMetric::Vw => Some(pv.number.get() / 100.0 * viewport_width),
260 SizeMetric::Vh => Some(pv.number.get() / 100.0 * viewport_height),
261 SizeMetric::Vmin => Some(pv.number.get() / 100.0 * viewport_width.min(viewport_height)),
262 SizeMetric::Vmax => Some(pv.number.get() / 100.0 * viewport_width.max(viewport_height)),
263 _ => resolve_pixel_value_no_percent(pv, em_size, rem_size),
264 }
265}
266
267#[cfg(test)]
268#[allow(clippy::float_cmp, clippy::cast_precision_loss, clippy::unreadable_literal)]
269mod autotest_generated {
270 use azul_css::props::basic::FP_PRECISION_MULTIPLIER;
271
272 use super::*;
273
274 const ALL_METRICS: [SizeMetric; 12] = [
281 SizeMetric::Px,
282 SizeMetric::Pt,
283 SizeMetric::Em,
284 SizeMetric::Rem,
285 SizeMetric::In,
286 SizeMetric::Cm,
287 SizeMetric::Mm,
288 SizeMetric::Percent,
289 SizeMetric::Vw,
290 SizeMetric::Vh,
291 SizeMetric::Vmin,
292 SizeMetric::Vmax,
293 ];
294
295 const ABSOLUTE_METRICS: [SizeMetric; 7] = [
297 SizeMetric::Px,
298 SizeMetric::Pt,
299 SizeMetric::Em,
300 SizeMetric::Rem,
301 SizeMetric::In,
302 SizeMetric::Cm,
303 SizeMetric::Mm,
304 ];
305
306 const VIEWPORT_METRICS: [SizeMetric; 4] = [
307 SizeMetric::Vw,
308 SizeMetric::Vh,
309 SizeMetric::Vmin,
310 SizeMetric::Vmax,
311 ];
312
313 fn saturated_magnitude() -> f32 {
316 (isize::MAX as f32) / FP_PRECISION_MULTIPLIER
317 }
318
319 fn assert_close(actual: f32, expected: f32) {
320 let tolerance = 1e-3 * expected.abs().max(1.0);
321 assert!(
322 (actual - expected).abs() <= tolerance,
323 "expected ~{expected}, got {actual}",
324 );
325 }
326
327 fn val(metric: SizeMetric, n: f32) -> CalcAstItem {
328 CalcAstItem::Value(PixelValue::from_metric(metric, n))
329 }
330
331 fn px(n: f32) -> CalcAstItem {
332 val(SizeMetric::Px, n)
333 }
334
335 const EM: f32 = 16.0;
336 const REM: f32 = 10.0;
337
338 fn eval(items: &[CalcAstItem], basis: f32) -> f32 {
339 evaluate_calc_ast(items, basis, EM, REM)
340 }
341
342 fn ctx(items: Vec<CalcAstItem>, em_size: f32, rem_size: f32) -> CalcResolveContext {
343 CalcResolveContext {
344 items: CalcAstItemVec::from(items),
345 em_size,
346 rem_size,
347 }
348 }
349
350 #[test]
360 fn pixel_value_number_is_always_finite_even_for_nan_and_inf() {
361 for metric in ALL_METRICS {
362 for hostile in [
363 f32::NAN,
364 f32::INFINITY,
365 f32::NEG_INFINITY,
366 f32::MAX,
367 f32::MIN,
368 f32::MIN_POSITIVE,
369 -0.0,
370 ] {
371 let n = PixelValue::from_metric(metric, hostile).number.get();
372 assert!(
373 n.is_finite(),
374 "PixelValue::from_metric({metric:?}, {hostile}) produced non-finite {n}",
375 );
376 }
377 }
378 }
379
380 #[test]
381 fn pixel_value_nan_encodes_as_zero() {
382 assert_eq!(PixelValue::px(f32::NAN).number.get(), 0.0);
383 }
384
385 #[test]
386 fn pixel_value_infinities_saturate_to_fixed_point_bounds() {
387 let sat = saturated_magnitude();
388 assert_close(PixelValue::px(f32::INFINITY).number.get(), sat);
389 assert_close(PixelValue::px(f32::NEG_INFINITY).number.get(), -sat);
390 assert_close(PixelValue::px(f32::MAX).number.get(), sat);
393 assert_close(PixelValue::px(f32::MIN).number.get(), -sat);
394 }
395
396 #[test]
401 fn resolve_pixel_value_zero_is_zero_for_every_metric() {
402 for metric in ALL_METRICS {
403 let pv = PixelValue::from_metric(metric, 0.0);
404 let got = resolve_pixel_value(&pv, 500.0, EM, REM);
405 assert_eq!(got, 0.0, "0 {metric:?} should resolve to 0px, got {got}");
406 }
407 }
408
409 #[test]
410 fn resolve_pixel_value_absolute_unit_conversions() {
411 assert_eq!(resolve_pixel_value(&PixelValue::px(10.0), 0.0, EM, REM), 10.0);
412 assert_close(
413 resolve_pixel_value(&PixelValue::pt(12.0), 0.0, EM, REM),
414 12.0 * PT_TO_PX,
415 );
416 assert_close(resolve_pixel_value(&PixelValue::inch(1.0), 0.0, EM, REM), 96.0);
418 assert_close(resolve_pixel_value(&PixelValue::cm(2.54), 0.0, EM, REM), 96.0);
419 assert_close(resolve_pixel_value(&PixelValue::mm(25.4), 0.0, EM, REM), 96.0);
420 }
421
422 #[test]
423 fn resolve_pixel_value_em_and_rem_use_their_own_font_size() {
424 assert_eq!(resolve_pixel_value(&PixelValue::em(2.0), 0.0, 16.0, 10.0), 32.0);
425 assert_eq!(resolve_pixel_value(&PixelValue::rem(2.0), 0.0, 16.0, 10.0), 20.0);
426 }
427
428 #[test]
429 fn resolve_pixel_value_percent_uses_basis() {
430 assert_eq!(resolve_pixel_value(&PixelValue::percent(50.0), 200.0, EM, REM), 100.0);
431 assert_eq!(resolve_pixel_value(&PixelValue::percent(0.0), 200.0, EM, REM), 0.0);
432 assert_eq!(resolve_pixel_value(&PixelValue::percent(100.0), 0.0, EM, REM), 0.0);
433 }
434
435 #[test]
436 fn resolve_pixel_value_handles_negative_inputs_deterministically() {
437 assert_eq!(resolve_pixel_value(&PixelValue::px(-10.0), 0.0, EM, REM), -10.0);
438 assert_eq!(resolve_pixel_value(&PixelValue::em(-2.0), 0.0, 16.0, REM), -32.0);
439 assert_eq!(resolve_pixel_value(&PixelValue::percent(-50.0), 200.0, EM, REM), -100.0);
442 assert_eq!(resolve_pixel_value(&PixelValue::percent(50.0), -200.0, EM, REM), -100.0);
443 }
444
445 #[test]
446 fn resolve_pixel_value_viewport_units_fall_back_to_raw_number() {
447 for metric in VIEWPORT_METRICS {
449 let pv = PixelValue::from_metric(metric, 50.0);
450 assert_eq!(
451 resolve_pixel_value(&pv, 1000.0, EM, REM),
452 50.0,
453 "{metric:?} should fall back to its raw number",
454 );
455 }
456 }
457
458 #[test]
459 fn resolve_pixel_value_is_finite_for_any_pixel_value_in_a_sane_context() {
460 for metric in ALL_METRICS {
465 for n in [0.0, -0.0, 1.0, -1.0, f32::MAX, f32::MIN, f32::NAN, f32::INFINITY] {
466 let pv = PixelValue::from_metric(metric, n);
467 for basis in [0.0_f32, -1000.0, 1920.0, 1_000_000.0] {
468 let got = resolve_pixel_value(&pv, basis, EM, REM);
469 assert!(
470 got.is_finite(),
471 "{metric:?} {n} @ basis {basis} produced non-finite {got}",
472 );
473 }
474 }
475 }
476 }
477
478 #[test]
479 fn resolve_pixel_value_saturated_percent_of_huge_basis_overflows_to_inf_not_panic() {
480 let hundred = resolve_pixel_value(&PixelValue::percent(100.0), f32::MAX, EM, REM);
482 assert_eq!(hundred, f32::MAX);
483
484 let two_hundred = resolve_pixel_value(&PixelValue::percent(200.0), f32::MAX, EM, REM);
485 assert!(
486 two_hundred.is_infinite() && two_hundred.is_sign_positive(),
487 "expected +inf on overflow, got {two_hundred}",
488 );
489 }
490
491 #[test]
492 fn resolve_pixel_value_nan_basis_only_pollutes_percentages() {
493 assert_eq!(resolve_pixel_value(&PixelValue::px(10.0), f32::NAN, EM, REM), 10.0);
495 assert_eq!(resolve_pixel_value(&PixelValue::em(1.0), f32::NAN, 16.0, REM), 16.0);
496 assert!(resolve_pixel_value(&PixelValue::percent(50.0), f32::NAN, EM, REM).is_nan());
497 }
498
499 #[test]
500 fn resolve_pixel_value_zero_times_infinite_context_is_nan_not_zero() {
501 assert!(resolve_pixel_value(&PixelValue::em(0.0), 0.0, f32::INFINITY, REM).is_nan());
505 assert!(resolve_pixel_value(&PixelValue::percent(0.0), f32::INFINITY, EM, REM).is_nan());
506 let inf = resolve_pixel_value(&PixelValue::em(1.0), 0.0, f32::INFINITY, REM);
508 assert!(inf.is_infinite() && inf.is_sign_positive());
509 }
510
511 #[test]
516 fn resolve_with_viewport_resolves_viewport_units() {
517 let (vw, vh) = (1000.0_f32, 800.0_f32);
518 let r = |m: SizeMetric, n: f32| {
519 resolve_pixel_value_with_viewport(&PixelValue::from_metric(m, n), 0.0, EM, REM, vw, vh)
520 };
521 assert_eq!(r(SizeMetric::Vw, 50.0), 500.0);
522 assert_eq!(r(SizeMetric::Vh, 50.0), 400.0);
523 assert_eq!(r(SizeMetric::Vmin, 100.0), 800.0);
524 assert_eq!(r(SizeMetric::Vmax, 100.0), 1000.0);
525 }
526
527 #[test]
528 fn resolve_with_viewport_delegates_non_viewport_metrics_verbatim() {
529 for metric in ALL_METRICS {
530 if VIEWPORT_METRICS.contains(&metric) {
531 continue;
532 }
533 let pv = PixelValue::from_metric(metric, 33.75);
534 assert_eq!(
535 resolve_pixel_value_with_viewport(&pv, 500.0, EM, REM, 1920.0, 1080.0),
536 resolve_pixel_value(&pv, 500.0, EM, REM),
537 "{metric:?} must not be affected by viewport context",
538 );
539 }
540 }
541
542 #[test]
543 fn resolve_with_viewport_zero_and_negative_viewport() {
544 for metric in VIEWPORT_METRICS {
545 let pv = PixelValue::from_metric(metric, 50.0);
546 assert_eq!(
547 resolve_pixel_value_with_viewport(&pv, 0.0, EM, REM, 0.0, 0.0),
548 0.0,
549 "{metric:?} against a 0x0 viewport should be 0px",
550 );
551 }
552 let vw50 = PixelValue::from_metric(SizeMetric::Vw, 50.0);
554 assert_eq!(
555 resolve_pixel_value_with_viewport(&vw50, 0.0, EM, REM, -100.0, -100.0),
556 -50.0,
557 );
558 let vmin100 = PixelValue::from_metric(SizeMetric::Vmin, 100.0);
560 let vmax100 = PixelValue::from_metric(SizeMetric::Vmax, 100.0);
561 assert_eq!(
562 resolve_pixel_value_with_viewport(&vmin100, 0.0, EM, REM, -100.0, -50.0),
563 -100.0,
564 );
565 assert_eq!(
566 resolve_pixel_value_with_viewport(&vmax100, 0.0, EM, REM, -100.0, -50.0),
567 -50.0,
568 );
569 }
570
571 #[test]
572 fn resolve_with_viewport_nan_viewport_is_swallowed_by_vmin_vmax_but_not_vw_vh() {
573 let vmin = PixelValue::from_metric(SizeMetric::Vmin, 100.0);
576 let vmax = PixelValue::from_metric(SizeMetric::Vmax, 100.0);
577 assert_eq!(
578 resolve_pixel_value_with_viewport(&vmin, 0.0, EM, REM, f32::NAN, 800.0),
579 800.0,
580 );
581 assert_eq!(
582 resolve_pixel_value_with_viewport(&vmax, 0.0, EM, REM, f32::NAN, 800.0),
583 800.0,
584 );
585 let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
587 assert!(resolve_pixel_value_with_viewport(&vw, 0.0, EM, REM, f32::NAN, 800.0).is_nan());
588
589 assert!(
591 resolve_pixel_value_with_viewport(&vmin, 0.0, EM, REM, f32::NAN, f32::NAN).is_nan()
592 );
593 }
594
595 #[test]
596 fn resolve_with_viewport_infinite_viewport_does_not_panic() {
597 let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
598 let got = resolve_pixel_value_with_viewport(&vw, 0.0, EM, REM, f32::INFINITY, 0.0);
599 assert!(got.is_infinite() && got.is_sign_positive());
600
601 let vw0 = PixelValue::from_metric(SizeMetric::Vw, 0.0);
603 assert!(
604 resolve_pixel_value_with_viewport(&vw0, 0.0, EM, REM, f32::INFINITY, 0.0).is_nan()
605 );
606 }
607
608 #[test]
613 fn no_percent_returns_none_for_percent_and_viewport_units() {
614 for metric in [
615 SizeMetric::Percent,
616 SizeMetric::Vw,
617 SizeMetric::Vh,
618 SizeMetric::Vmin,
619 SizeMetric::Vmax,
620 ] {
621 let pv = PixelValue::from_metric(metric, 50.0);
622 assert_eq!(
623 resolve_pixel_value_no_percent(&pv, EM, REM),
624 None,
625 "{metric:?} needs layout context, so it must be None",
626 );
627 }
628 }
629
630 #[test]
631 fn no_percent_agrees_with_resolve_pixel_value_on_absolute_metrics() {
632 for metric in ABSOLUTE_METRICS {
636 for n in [0.0, 1.0, -7.5, 1234.5, f32::MAX, f32::MIN, f32::NAN] {
637 let pv = PixelValue::from_metric(metric, n);
638 let some = resolve_pixel_value_no_percent(&pv, EM, REM);
639 assert_eq!(
640 some,
641 Some(resolve_pixel_value(&pv, 999.0, EM, REM)),
642 "{metric:?} {n} disagrees between the two resolvers",
643 );
644 assert!(some.unwrap().is_finite());
645 }
646 }
647 }
648
649 #[test]
650 fn no_percent_with_viewport_returns_none_only_for_percent() {
651 for metric in ALL_METRICS {
652 let pv = PixelValue::from_metric(metric, 50.0);
653 let got = resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 1000.0, 800.0);
654 if metric == SizeMetric::Percent {
655 assert_eq!(got, None, "% must stay None without a basis");
656 } else {
657 assert!(got.is_some(), "{metric:?} should resolve with a viewport");
658 assert!(got.unwrap().is_finite());
659 }
660 }
661 }
662
663 #[test]
664 fn no_percent_with_viewport_agrees_with_resolve_with_viewport() {
665 for metric in ALL_METRICS {
666 if metric == SizeMetric::Percent {
667 continue;
668 }
669 let pv = PixelValue::from_metric(metric, 25.0);
670 assert_eq!(
671 resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 1920.0, 1080.0),
672 Some(resolve_pixel_value_with_viewport(&pv, 0.0, EM, REM, 1920.0, 1080.0)),
673 "{metric:?} disagrees between the viewport resolvers",
674 );
675 }
676 }
677
678 #[test]
679 fn no_percent_with_viewport_zero_viewport_yields_some_zero_not_none() {
680 for metric in VIEWPORT_METRICS {
681 let pv = PixelValue::from_metric(metric, 100.0);
682 assert_eq!(
683 resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 0.0, 0.0),
684 Some(0.0),
685 "{metric:?} against a 0x0 viewport is a defined 0px, not None",
686 );
687 }
688 }
689
690 #[test]
695 fn eval_empty_ast_is_zero() {
696 assert_eq!(eval(&[], 500.0), 0.0);
697 }
698
699 #[test]
700 fn eval_single_value() {
701 assert_eq!(eval(&[px(42.0)], 500.0), 42.0);
702 assert_eq!(eval(&[val(SizeMetric::Percent, 10.0)], 500.0), 50.0);
703 }
704
705 #[test]
706 fn eval_basic_addition_and_subtraction() {
707 let items = [val(SizeMetric::Percent, 100.0), CalcAstItem::Sub, px(20.0)];
709 assert_eq!(eval(&items, 500.0), 480.0);
710 }
711
712 #[test]
713 fn eval_add_and_sub_are_left_associative() {
714 let items = [px(10.0), CalcAstItem::Sub, px(3.0), CalcAstItem::Sub, px(2.0)];
715 assert_eq!(eval(&items, 0.0), 5.0, "must be (10-3)-2, not 10-(3-2)");
716 }
717
718 #[test]
719 fn eval_div_is_left_associative() {
720 let items = [px(10.0), CalcAstItem::Div, px(4.0), CalcAstItem::Div, px(2.0)];
721 assert_eq!(eval(&items, 0.0), 1.25, "must be (10/4)/2, not 10/(4/2)");
722 }
723
724 #[test]
725 fn eval_mul_binds_tighter_than_add() {
726 let items = [px(1.0), CalcAstItem::Add, px(2.0), CalcAstItem::Mul, px(3.0)];
728 assert_eq!(eval(&items, 0.0), 7.0);
729
730 let items = [px(2.0), CalcAstItem::Mul, px(3.0), CalcAstItem::Add, px(4.0)];
732 assert_eq!(eval(&items, 0.0), 10.0);
733 }
734
735 #[test]
736 fn eval_div_binds_tighter_than_sub() {
737 let items = [px(10.0), CalcAstItem::Sub, px(6.0), CalcAstItem::Div, px(2.0)];
739 assert_eq!(eval(&items, 0.0), 7.0);
740 }
741
742 #[test]
743 fn eval_braces_override_precedence() {
744 let items = [
746 CalcAstItem::BraceOpen,
747 px(1.0),
748 CalcAstItem::Add,
749 px(2.0),
750 CalcAstItem::BraceClose,
751 CalcAstItem::Mul,
752 px(3.0),
753 ];
754 assert_eq!(eval(&items, 0.0), 9.0);
755 }
756
757 #[test]
758 fn eval_nested_braces() {
759 let items = [
761 CalcAstItem::BraceOpen,
762 CalcAstItem::BraceOpen,
763 px(2.0),
764 CalcAstItem::Add,
765 px(3.0),
766 CalcAstItem::BraceClose,
767 CalcAstItem::Mul,
768 CalcAstItem::BraceOpen,
769 px(4.0),
770 CalcAstItem::Sub,
771 px(2.0),
772 CalcAstItem::BraceClose,
773 CalcAstItem::BraceClose,
774 ];
775 assert_eq!(eval(&items, 0.0), 10.0);
776 }
777
778 #[test]
779 fn eval_empty_braces_are_zero() {
780 let items = [CalcAstItem::BraceOpen, CalcAstItem::BraceClose];
781 assert_eq!(eval(&items, 500.0), 0.0);
782
783 let items = [
785 px(10.0),
786 CalcAstItem::Add,
787 CalcAstItem::BraceOpen,
788 CalcAstItem::BraceClose,
789 ];
790 assert_eq!(eval(&items, 500.0), 10.0);
791 }
792
793 #[test]
794 fn eval_percent_inside_braces_still_sees_the_basis() {
795 let items = [
797 CalcAstItem::BraceOpen,
798 val(SizeMetric::Percent, 100.0),
799 CalcAstItem::Sub,
800 px(20.0),
801 CalcAstItem::BraceClose,
802 CalcAstItem::Div,
803 px(4.0),
804 ];
805 assert_eq!(eval(&items, 500.0), 120.0);
806 }
807
808 #[test]
809 fn eval_em_and_rem_come_from_the_evaluator_args_not_the_basis() {
810 let items = [val(SizeMetric::Em, 2.0), CalcAstItem::Add, val(SizeMetric::Rem, 1.0)];
812 assert_eq!(evaluate_calc_ast(&items, 0.0, 10.0, 20.0), 40.0);
813 }
814
815 #[test]
824 fn eval_unmatched_brace_open_does_not_panic_or_hang() {
825 assert_eq!(eval(&[CalcAstItem::BraceOpen], 500.0), 0.0);
827 assert_eq!(eval(&[CalcAstItem::BraceOpen, px(5.0)], 500.0), 5.0);
829 let items = [
831 px(1.0),
832 CalcAstItem::Add,
833 CalcAstItem::BraceOpen,
834 px(2.0),
835 ];
836 assert_eq!(eval(&items, 500.0), 3.0);
837 }
838
839 #[test]
840 fn eval_stray_brace_close_is_ignored() {
841 let items = [
843 px(5.0),
844 CalcAstItem::BraceClose,
845 CalcAstItem::Add,
846 px(3.0),
847 ];
848 assert_eq!(eval(&items, 0.0), 8.0);
849
850 assert_eq!(eval(&[CalcAstItem::BraceClose, px(7.0)], 0.0), 7.0);
852 }
853
854 #[test]
855 fn eval_leading_operator_collapses_to_zero() {
856 assert_eq!(eval(&[CalcAstItem::Mul, px(5.0)], 0.0), 0.0);
858 assert_eq!(eval(&[CalcAstItem::Add, px(5.0)], 0.0), 0.0);
859 assert_eq!(eval(&[CalcAstItem::Div, px(5.0)], 0.0), 0.0);
860 }
861
862 #[test]
863 fn eval_trailing_operator_keeps_the_left_hand_side() {
864 for op in [
865 CalcAstItem::Add,
866 CalcAstItem::Sub,
867 CalcAstItem::Mul,
868 CalcAstItem::Div,
869 ] {
870 assert_eq!(eval(&[px(10.0), op], 0.0), 10.0, "trailing {op:?} dropped the lhs");
871 }
872 }
873
874 #[test]
875 fn eval_operator_only_ast_is_zero() {
876 let items = [
877 CalcAstItem::Add,
878 CalcAstItem::Sub,
879 CalcAstItem::Mul,
880 CalcAstItem::Div,
881 ];
882 assert_eq!(eval(&items, 500.0), 0.0);
883 }
884
885 #[test]
886 fn eval_adjacent_values_without_an_operator_keep_only_the_first() {
887 assert_eq!(eval(&[px(10.0), px(20.0)], 0.0), 10.0);
890 }
891
892 #[test]
893 fn eval_adjacent_operators_do_not_panic() {
894 let items = [
896 px(1.0),
897 CalcAstItem::Add,
898 CalcAstItem::Mul,
899 px(2.0),
900 ];
901 assert_eq!(eval(&items, 0.0), 1.0);
902 }
903
904 #[test]
905 fn eval_deeply_nested_braces_do_not_overflow_the_stack() {
906 const DEPTH: usize = 256;
909 let mut items: Vec<CalcAstItem> = vec![CalcAstItem::BraceOpen; DEPTH];
910 items.push(px(1.0));
911 items.resize(DEPTH * 2 + 1, CalcAstItem::BraceClose);
912 assert_eq!(eval(&items, 0.0), 1.0);
913 }
914
915 #[test]
916 fn eval_unbalanced_deep_braces_terminate() {
917 let items = vec![CalcAstItem::BraceOpen; 256];
920 assert_eq!(eval(&items, 0.0), 0.0);
921 }
922
923 #[test]
924 fn eval_long_flat_ast_terminates_with_the_right_sum() {
925 const TERMS: usize = 1000;
928 let mut items = Vec::with_capacity(TERMS * 2 - 1);
929 for i in 0..TERMS {
930 if i > 0 {
931 items.push(CalcAstItem::Add);
932 }
933 items.push(px(1.0));
934 }
935 assert_eq!(eval(&items, 0.0), TERMS as f32);
936 }
937
938 #[test]
943 fn eval_division_by_zero_is_guarded_to_zero_not_infinity() {
944 let items = [px(10.0), CalcAstItem::Div, px(0.0)];
945 let got = eval(&items, 0.0);
946 assert_eq!(got, 0.0);
947 assert!(got.is_finite(), "10px / 0px must not be an infinity");
948 }
949
950 #[test]
951 fn eval_division_by_a_zero_valued_percent_is_also_guarded() {
952 let items = [px(10.0), CalcAstItem::Div, val(SizeMetric::Percent, 100.0)];
954 assert_eq!(eval(&items, 0.0), 0.0);
955
956 let got = eval(&items, -0.0);
959 assert_eq!(got, 0.0);
960 assert!(got.is_finite(), "10px / (100% of -0) must not be -inf");
961 }
962
963 #[test]
964 fn eval_division_by_nan_is_not_caught_by_the_zero_guard() {
965 let items = [px(10.0), CalcAstItem::Div, val(SizeMetric::Percent, 100.0)];
968 assert!(eval(&items, f32::NAN).is_nan());
969 }
970
971 #[test]
972 fn eval_divisor_below_fixed_point_precision_truncates_into_the_zero_guard() {
973 assert_eq!(PixelValue::px(0.0001).number.get(), 0.0);
977 let items = [px(10.0), CalcAstItem::Div, px(0.0001)];
978 assert_eq!(eval(&items, 0.0), 0.0);
979
980 assert_close(PixelValue::px(0.001).number.get(), 0.001);
982 let items = [px(10.0), CalcAstItem::Div, px(0.001)];
983 let got = eval(&items, 0.0);
984 assert!(got.is_finite(), "10px / 0.001px should be finite, got {got}");
985 assert_close(got, 10_000.0);
986 }
987
988 #[test]
989 fn eval_multiplication_overflow_saturates_to_infinity_without_panicking() {
990 let mut items = vec![px(f32::MAX)];
993 for _ in 0..7 {
994 items.push(CalcAstItem::Mul);
995 items.push(px(f32::MAX));
996 }
997 let got = eval(&items, 0.0);
998 assert!(
999 got.is_infinite() && got.is_sign_positive(),
1000 "expected +inf on overflow, got {got}",
1001 );
1002 }
1003
1004 #[test]
1005 fn eval_inf_minus_inf_is_nan_not_a_panic() {
1006 let items = [
1008 val(SizeMetric::Percent, 100.0),
1009 CalcAstItem::Sub,
1010 val(SizeMetric::Percent, 100.0),
1011 ];
1012 assert!(eval(&items, f32::INFINITY).is_nan());
1013 }
1014
1015 #[test]
1016 fn eval_nan_basis_does_not_leak_into_an_absolute_expression() {
1017 let items = [px(10.0), CalcAstItem::Add, px(5.0)];
1018 assert_eq!(eval(&items, f32::NAN), 15.0);
1019 assert_eq!(eval(&items, f32::INFINITY), 15.0);
1020 }
1021
1022 #[test]
1023 fn eval_negative_results_are_not_clamped() {
1024 let items = [px(20.0), CalcAstItem::Sub, val(SizeMetric::Percent, 100.0)];
1027 assert_eq!(eval(&items, 500.0), -480.0);
1028 }
1029
1030 #[test]
1031 fn eval_viewport_units_inside_calc_use_the_no_viewport_fallback() {
1032 let items = [val(SizeMetric::Vw, 50.0), CalcAstItem::Add, px(0.0)];
1035 assert_eq!(eval(&items, 1000.0), 50.0);
1036 }
1037
1038 #[test]
1043 fn evaluate_calc_empty_context_is_zero() {
1044 let c = ctx(Vec::new(), 16.0, 10.0);
1045 assert_eq!(evaluate_calc(&c, 500.0), 0.0);
1046 assert_eq!(evaluate_calc(&c, f32::NAN), 0.0);
1047 }
1048
1049 #[test]
1050 fn evaluate_calc_matches_the_ast_evaluator_it_wraps() {
1051 let items = vec![
1052 val(SizeMetric::Percent, 100.0),
1053 CalcAstItem::Sub,
1054 CalcAstItem::BraceOpen,
1055 val(SizeMetric::Em, 2.0),
1056 CalcAstItem::Add,
1057 val(SizeMetric::Rem, 1.0),
1058 CalcAstItem::BraceClose,
1059 ];
1060 let c = ctx(items.clone(), 12.0, 20.0);
1061 assert_eq!(evaluate_calc(&c, 500.0), 456.0);
1063 assert_eq!(evaluate_calc(&c, 500.0), evaluate_calc_ast(&items, 500.0, 12.0, 20.0));
1064 }
1065
1066 #[test]
1067 fn evaluate_calc_forwards_its_own_em_and_rem_sizes_without_swapping_them() {
1068 let items = vec![val(SizeMetric::Em, 2.0), CalcAstItem::Add, val(SizeMetric::Rem, 1.0)];
1071 assert_eq!(evaluate_calc(&ctx(items.clone(), 10.0, 20.0), 0.0), 40.0);
1072 assert_eq!(evaluate_calc(&ctx(items, 20.0, 10.0), 0.0), 50.0);
1073 }
1074
1075 #[test]
1076 fn evaluate_calc_with_hostile_font_sizes_does_not_panic() {
1077 let items = vec![val(SizeMetric::Em, 2.0), CalcAstItem::Add, px(1.0)];
1078 assert!(evaluate_calc(&ctx(items.clone(), f32::NAN, 10.0), 0.0).is_nan());
1079
1080 let inf = evaluate_calc(&ctx(items.clone(), f32::INFINITY, 10.0), 0.0);
1081 assert!(inf.is_infinite() && inf.is_sign_positive());
1082
1083 assert_eq!(evaluate_calc(&ctx(items, 0.0, 10.0), 0.0), 1.0);
1085 }
1086
1087 #[test]
1088 fn evaluate_calc_is_pure_and_repeatable() {
1089 let c = ctx(
1092 vec![val(SizeMetric::Percent, 50.0), CalcAstItem::Add, px(10.0)],
1093 16.0,
1094 10.0,
1095 );
1096 assert_eq!(evaluate_calc(&c, 100.0), 60.0);
1097 assert_eq!(evaluate_calc(&c, 200.0), 110.0);
1098 assert_eq!(evaluate_calc(&c, 100.0), 60.0);
1099 }
1100}
1101