1use crate::style::calc::{CalcFormula, CalcValue, parse_calc_sum};
2use crate::style::{ToCss, unexpected_token};
3use std::{fmt, ops::Neg};
4
5use cssparser::{Parser, Token, match_ignore_ascii_case};
6use taffy::{CompactLength, Dimension, LengthPercentage, LengthPercentageAuto};
7
8use crate::style::{
9 AspectRatio, CssSyntaxKind, CssToken, FromCss, MakeComputed, ParseResult, SizingContext,
10 tw::{TW_VAR_SPACING, TailwindPropertyParser},
11};
12
13pub(crate) const ONE_CM_IN_PX: f32 = 96.0 / 2.54;
14pub(crate) const ONE_MM_IN_PX: f32 = ONE_CM_IN_PX / 10.0;
15pub(crate) const ONE_Q_IN_PX: f32 = ONE_CM_IN_PX / 40.0;
16pub(crate) const ONE_IN_PX: f32 = 2.54 * ONE_CM_IN_PX;
17pub(crate) const ONE_PT_IN_PX: f32 = ONE_IN_PX / 72.0;
18pub(crate) const ONE_PC_IN_PX: f32 = ONE_IN_PX / 6.0;
19const CALC_ZERO_EPSILON: f32 = 1e-6;
20const SAFE_INT_MIN_PX: f32 = i32::MIN as f32;
21const SAFE_INT_MAX_PX: f32 = i32::MAX as f32;
22
23pub(crate) fn length_from_dimension_unit<const DEFAULT_AUTO: bool>(
25 unit: &str,
26 value: f32,
27) -> Option<Length<DEFAULT_AUTO>> {
28 Some(match_ignore_ascii_case! {unit,
29 "px" => Length::Px(value),
30 "em" => Length::Em(value),
31 "rem" => Length::Rem(value),
32 "lh" => Length::Lh(value),
33 "rlh" => Length::Rlh(value),
34 "vw" => Length::Vw(value),
35 "dvw" => Length::Vw(value),
36 "svw" => Length::Vw(value),
37 "lvw" => Length::Vw(value),
38 "cqw" => Length::CqW(value),
39 "cqi" => Length::CqW(value),
40 "vi" => Length::Vw(value),
41 "vh" => Length::Vh(value),
42 "dvh" => Length::Vh(value),
43 "svh" => Length::Vh(value),
44 "lvh" => Length::Vh(value),
45 "cqh" => Length::CqH(value),
46 "cqb" => Length::CqH(value),
47 "vb" => Length::Vh(value),
48 "vmin" => Length::VMin(value),
49 "cqmin" => Length::CqMin(value),
50 "vmax" => Length::VMax(value),
51 "cqmax" => Length::CqMax(value),
52 "cm" => Length::Cm(value),
53 "mm" => Length::Mm(value),
54 "in" => Length::In(value),
55 "q" => Length::Q(value),
56 "pt" => Length::Pt(value),
57 "pc" => Length::Pc(value),
58 _ => return None,
59 })
60}
61
62fn is_near_zero(value: f32) -> bool {
63 value.abs() <= CALC_ZERO_EPSILON
64}
65
66fn clamp_px_for_integer_cast(value: f32) -> f32 {
67 if value.is_nan() {
68 return 0.0;
69 }
70
71 if value.is_infinite() {
72 return if value.is_sign_positive() {
73 SAFE_INT_MAX_PX
74 } else {
75 SAFE_INT_MIN_PX
76 };
77 }
78
79 value.clamp(SAFE_INT_MIN_PX, SAFE_INT_MAX_PX)
80}
81
82pub type LengthDefaultsToZero = Length<false>;
84
85#[derive(Debug, Clone, PartialEq, Copy)]
87#[non_exhaustive]
88pub enum Length<const DEFAULT_AUTO: bool = true> {
89 Auto,
91 Percentage(f32),
93 Rem(f32),
95 Em(f32),
97 Lh(f32),
99 Rlh(f32),
101 Vh(f32),
103 Vw(f32),
105 CqH(f32),
107 CqW(f32),
109 CqMin(f32),
111 CqMax(f32),
113 VMin(f32),
115 VMax(f32),
117 Cm(f32),
119 Mm(f32),
121 In(f32),
123 Q(f32),
125 Pt(f32),
127 Pc(f32),
129 Px(f32),
131 Calc(CalcFormula),
133}
134
135impl<const DEFAULT_AUTO: bool> Default for Length<DEFAULT_AUTO> {
136 fn default() -> Self {
137 if DEFAULT_AUTO {
138 Self::Auto
139 } else {
140 Self::Px(0.0)
141 }
142 }
143}
144
145impl<const DEFAULT_AUTO: bool> Length<DEFAULT_AUTO> {
146 #[inline]
148 pub fn from_spacing(units: f32) -> Self {
149 Length::Rem(units * TW_VAR_SPACING)
150 }
151}
152
153impl<const DEFAULT_AUTO: bool> TailwindPropertyParser for Length<DEFAULT_AUTO> {
154 fn parse_tw(token: &str) -> Option<Self> {
155 if let Ok(value) = token.parse::<f32>() {
156 return Some(Length::from_spacing(value));
157 }
158
159 match AspectRatio::from_str(token) {
160 Ok(AspectRatio::Ratio(ratio)) => return Some(Length::Percentage(ratio * 100.0)),
161 Ok(AspectRatio::Auto) => return Some(Length::Auto),
162 _ => {}
163 }
164
165 match_ignore_ascii_case! {token,
166 "auto" => Some(Length::Auto),
167 "dvw" => Some(Length::Vw(100.0)),
168 "svw" => Some(Length::Vw(100.0)),
169 "lvw" => Some(Length::Vw(100.0)),
170 "cqw" => Some(Length::CqW(100.0)),
171 "cqi" => Some(Length::CqW(100.0)),
172 "vi" => Some(Length::Vw(100.0)),
173 "dvh" => Some(Length::Vh(100.0)),
174 "svh" => Some(Length::Vh(100.0)),
175 "lvh" => Some(Length::Vh(100.0)),
176 "cqh" => Some(Length::CqH(100.0)),
177 "cqb" => Some(Length::CqH(100.0)),
178 "vb" => Some(Length::Vh(100.0)),
179 "vmin" => Some(Length::VMin(100.0)),
180 "cqmin" => Some(Length::CqMin(100.0)),
181 "vmax" => Some(Length::VMax(100.0)),
182 "cqmax" => Some(Length::CqMax(100.0)),
183 "px" => Some(Length::Px(1.0)),
184 "full" => Some(Length::Percentage(100.0)),
185 "3xs" => Some(Length::Rem(16.0)),
186 "2xs" => Some(Length::Rem(18.0)),
187 "xs" => Some(Length::Rem(20.0)),
188 "sm" => Some(Length::Rem(24.0)),
189 "md" => Some(Length::Rem(28.0)),
190 "lg" => Some(Length::Rem(32.0)),
191 "xl" => Some(Length::Rem(36.0)),
192 "2xl" => Some(Length::Rem(42.0)),
193 "3xl" => Some(Length::Rem(48.0)),
194 "4xl" => Some(Length::Rem(56.0)),
195 "5xl" => Some(Length::Rem(64.0)),
196 "6xl" => Some(Length::Rem(72.0)),
197 "7xl" => Some(Length::Rem(80.0)),
198 _ => None,
199 }
200 }
201}
202
203impl<const DEFAULT_AUTO: bool> ToCss for Length<DEFAULT_AUTO> {
204 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
205 match self {
206 Self::Auto => dest.write_str("auto"),
207 Self::Percentage(v) => write!(dest, "{}%", v),
208 Self::Rem(v) => write!(dest, "{}rem", v),
209 Self::Em(v) => write!(dest, "{}em", v),
210 Self::Lh(v) => write!(dest, "{}lh", v),
211 Self::Rlh(v) => write!(dest, "{}rlh", v),
212 Self::Vh(v) => write!(dest, "{}vh", v),
213 Self::Vw(v) => write!(dest, "{}vw", v),
214 Self::CqH(v) => write!(dest, "{}cqh", v),
215 Self::CqW(v) => write!(dest, "{}cqw", v),
216 Self::CqMin(v) => write!(dest, "{}cqmin", v),
217 Self::CqMax(v) => write!(dest, "{}cqmax", v),
218 Self::VMin(v) => write!(dest, "{}vmin", v),
219 Self::VMax(v) => write!(dest, "{}vmax", v),
220 Self::Cm(v) => write!(dest, "{}cm", v),
221 Self::Mm(v) => write!(dest, "{}mm", v),
222 Self::In(v) => write!(dest, "{}in", v),
223 Self::Q(v) => write!(dest, "{}q", v),
224 Self::Pt(v) => write!(dest, "{}pt", v),
225 Self::Pc(v) => write!(dest, "{}pc", v),
226 Self::Px(v) => write!(dest, "{}px", v),
227 Self::Calc(f) => {
228 let terms: &[(&str, f32)] = &[
229 ("px", f.px),
230 ("%", f.percent * 100.0),
231 ("rem", f.rem),
232 ("em", f.em),
233 ("lh", f.lh),
234 ("rlh", f.rlh),
235 ("vh", f.vh),
236 ("vw", f.vw),
237 ("cqh", f.cqh),
238 ("cqw", f.cqw),
239 ("cqmin", f.cqmin),
240 ("cqmax", f.cqmax),
241 ("vmin", f.vmin),
242 ("vmax", f.vmax),
243 ("cm", f.cm),
244 ("mm", f.mm),
245 ("in", f.inch),
246 ("q", f.q),
247 ("pt", f.pt),
248 ("pc", f.pc),
249 ];
250 if terms.iter().all(|(_, v)| *v == 0.0) {
251 return dest.write_str("0px");
252 }
253 dest.write_str("calc(")?;
254 let mut first = true;
255 for (unit, value) in terms {
256 if *value == 0.0 {
257 continue;
258 }
259 if first {
260 if *value < 0.0 {
261 write!(dest, "-{}{}", -value, unit)?;
262 } else {
263 write!(dest, "{}{}", value, unit)?;
264 }
265 } else if *value < 0.0 {
266 write!(dest, " - {}{}", -value, unit)?;
267 } else {
268 write!(dest, " + {}{}", value, unit)?;
269 }
270 first = false;
271 }
272 dest.write_str(")")
273 }
274 }
275 }
276}
277
278impl<const DEFAULT_AUTO: bool> Neg for Length<DEFAULT_AUTO> {
279 type Output = Self;
280
281 fn neg(self) -> Self::Output {
282 self.negative()
283 }
284}
285
286impl<const DEFAULT_AUTO: bool> Length<DEFAULT_AUTO> {
287 pub const fn zero() -> Self {
289 Self::Px(0.0)
290 }
291
292 pub fn try_negative(self) -> Option<Self> {
294 if matches!(self, Length::Auto) {
295 return None;
296 }
297 Some(self.negative())
298 }
299
300 pub fn negative(self) -> Self {
302 match self {
303 Length::Auto => Length::Auto,
304 Length::Percentage(v) => Length::Percentage(-v),
305 Length::Rem(v) => Length::Rem(-v),
306 Length::Em(v) => Length::Em(-v),
307 Length::Lh(v) => Length::Lh(-v),
308 Length::Rlh(v) => Length::Rlh(-v),
309 Length::Vh(v) => Length::Vh(-v),
310 Length::Vw(v) => Length::Vw(-v),
311 Length::CqH(v) => Length::CqH(-v),
312 Length::CqW(v) => Length::CqW(-v),
313 Length::CqMin(v) => Length::CqMin(-v),
314 Length::CqMax(v) => Length::CqMax(-v),
315 Length::VMin(v) => Length::VMin(-v),
316 Length::VMax(v) => Length::VMax(-v),
317 Length::Cm(v) => Length::Cm(-v),
318 Length::Mm(v) => Length::Mm(-v),
319 Length::In(v) => Length::In(-v),
320 Length::Q(v) => Length::Q(-v),
321 Length::Pt(v) => Length::Pt(-v),
322 Length::Pc(v) => Length::Pc(-v),
323 Length::Px(v) => Length::Px(-v),
324 Length::Calc(formula) => Length::Calc(formula.neg()),
325 }
326 }
327}
328
329impl<const DEFAULT_AUTO: bool> From<f32> for Length<DEFAULT_AUTO> {
330 fn from(value: f32) -> Self {
331 Self::Px(value)
332 }
333}
334
335impl<'i, const DEFAULT_AUTO: bool> FromCss<'i> for Length<DEFAULT_AUTO> {
336 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
337 let location = input.current_source_location();
338 let token = input.next()?;
339
340 match token {
341 Token::Ident(unit) => match_ignore_ascii_case! {unit.as_ref(),
342 "auto" => Ok(Self::Auto),
343 _ => Err(unexpected_token!(location, token)),
344 },
345 Token::Function(function) if function.eq_ignore_ascii_case("calc") => {
346 match input.parse_nested_block(parse_calc_sum)? {
347 CalcValue::Number(value) => Ok(Self::Px(value)),
348 CalcValue::Formula(formula) => Ok(Self::Calc(formula)),
349 }
350 }
351 Token::Dimension { value, unit, .. } => length_from_dimension_unit(unit.as_ref(), *value)
352 .ok_or_else(|| unexpected_token!(location, token)),
353 Token::Percentage { unit_value, .. } => Ok(Self::Percentage(*unit_value * 100.0)),
354 Token::Number { value, .. } => Ok(Self::Px(*value)),
355 _ => Err(unexpected_token!(location, token)),
356 }
357 }
358
359 const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::Length)];
360}
361
362impl<const DEFAULT_AUTO: bool> Length<DEFAULT_AUTO> {
363 fn to_px_pre_dpr(self, sizing: &SizingContext, percentage_full_px: f32) -> f32 {
364 match self {
365 Length::Auto => 0.0,
366 Length::Px(value) => value,
367 Length::Percentage(value) => (value / 100.0) * percentage_full_px,
368 Length::Rem(value) => value * sizing.rem_basis(),
369 Length::Em(value) => value * sizing.font_size,
370 Length::Lh(value) => value * sizing.line_height,
371 Length::Rlh(value) => value * sizing.root_line_height_basis(),
372 Length::Vh(value) => value * sizing.viewport.size.height.unwrap_or_default() as f32 / 100.0,
373 Length::Vw(value) => value * sizing.viewport.size.width.unwrap_or_default() as f32 / 100.0,
374 Length::CqH(value) => value * sizing.query_container_height() / 100.0,
375 Length::CqW(value) => value * sizing.query_container_width() / 100.0,
376 Length::CqMin(value) => {
377 value
378 * sizing
379 .query_container_width()
380 .min(sizing.query_container_height())
381 / 100.0
382 }
383 Length::CqMax(value) => {
384 value
385 * sizing
386 .query_container_width()
387 .max(sizing.query_container_height())
388 / 100.0
389 }
390 Length::VMin(value) => {
391 let viewport_width = sizing.viewport.size.width.unwrap_or_default() as f32;
392 let viewport_height = sizing.viewport.size.height.unwrap_or_default() as f32;
393 value * viewport_width.min(viewport_height) / 100.0
394 }
395 Length::VMax(value) => {
396 let viewport_width = sizing.viewport.size.width.unwrap_or_default() as f32;
397 let viewport_height = sizing.viewport.size.height.unwrap_or_default() as f32;
398 value * viewport_width.max(viewport_height) / 100.0
399 }
400 Length::Cm(value) => value * ONE_CM_IN_PX,
401 Length::Mm(value) => value * ONE_MM_IN_PX,
402 Length::In(value) => value * ONE_IN_PX,
403 Length::Q(value) => value * ONE_Q_IN_PX,
404 Length::Pt(value) => value * ONE_PT_IN_PX,
405 Length::Pc(value) => value * ONE_PC_IN_PX,
406 Length::Calc(formula) => formula.resolve(sizing).resolve(percentage_full_px),
408 }
409 }
410
411 pub fn to_compact_length(self, sizing: &SizingContext) -> CompactLength {
412 match self {
413 Length::Auto => CompactLength::auto(),
414 Length::Percentage(value) => CompactLength::percent(value / 100.0),
415 Length::Rem(_)
416 | Length::Em(_)
417 | Length::Lh(_)
418 | Length::Rlh(_)
419 | Length::Vh(_)
420 | Length::Vw(_)
421 | Length::CqH(_)
422 | Length::CqW(_)
423 | Length::CqMin(_)
424 | Length::CqMax(_)
425 | Length::VMin(_)
426 | Length::VMax(_) => CompactLength::length(self.to_px_pre_dpr(sizing, 0.0)),
427 Length::Calc(formula) => {
428 let linear = formula.resolve(sizing);
429
430 if is_near_zero(linear.percent) {
431 return CompactLength::length(linear.px);
432 }
433
434 if is_near_zero(linear.px) {
435 return CompactLength::percent(linear.percent);
436 }
437
438 CompactLength::calc(sizing.calc_arena.register_linear(linear))
439 }
440 _ => CompactLength::length(self.to_px(
441 sizing,
442 sizing.viewport.size.width.unwrap_or_default() as f32,
443 )),
444 }
445 }
446
447 pub fn resolve_to_length_percentage(self, sizing: &SizingContext) -> LengthPercentage {
448 let compact_length = self.to_compact_length(sizing);
449
450 if compact_length.is_auto() {
451 return LengthPercentage::length(0.0);
452 }
453
454 unsafe { LengthPercentage::from_raw(compact_length) }
455 }
456
457 pub fn to_px(self, sizing: &SizingContext, percentage_full_px: f32) -> f32 {
458 let value = self.to_px_pre_dpr(sizing, percentage_full_px);
459
460 let dpr = sizing.viewport.device_pixel_ratio;
462 let dpr = if dpr > 0.0 { dpr } else { 1.0 };
463 let value = match self {
464 Length::Px(_)
465 | Length::Cm(_)
466 | Length::Mm(_)
467 | Length::In(_)
468 | Length::Q(_)
469 | Length::Pt(_)
470 | Length::Pc(_) => value * dpr,
471 _ => value,
472 };
473
474 clamp_px_for_integer_cast(value)
475 }
476
477 pub fn resolve_to_length_percentage_auto(self, sizing: &SizingContext) -> LengthPercentageAuto {
478 unsafe { LengthPercentageAuto::from_raw(self.to_compact_length(sizing)) }
479 }
480
481 pub fn resolve_to_dimension(self, sizing: &SizingContext) -> Dimension {
482 self.resolve_to_length_percentage_auto(sizing).into()
483 }
484}
485
486impl<const DEFAULT_AUTO: bool> MakeComputed for Length<DEFAULT_AUTO> {
487 fn make_computed(&mut self, sizing: &SizingContext) {
488 if let Self::Em(em) = *self {
489 let dpr = sizing.viewport.device_pixel_ratio;
490 let font_size = if dpr > 0.0 {
491 sizing.font_size / dpr
492 } else {
493 sizing.font_size
494 };
495
496 *self = Self::Px(em * font_size);
497 return;
498 }
499
500 if let Self::Lh(lh) = *self {
501 let dpr = sizing.viewport.device_pixel_ratio;
502 let line_height = if dpr > 0.0 {
503 sizing.line_height / dpr
504 } else {
505 sizing.line_height
506 };
507
508 *self = Self::Px(lh * line_height);
509 return;
510 }
511
512 if let Self::Rlh(rlh) = *self {
513 let dpr = sizing.viewport.device_pixel_ratio;
514 let basis = sizing.root_line_height_basis();
515 let line_height = if dpr > 0.0 { basis / dpr } else { basis };
516
517 *self = Self::Px(rlh * line_height);
518 return;
519 }
520
521 if let Self::Calc(formula) = *self {
522 let linear = formula.resolve(sizing);
523
524 if is_near_zero(linear.percent) {
525 let dpr = sizing.viewport.device_pixel_ratio;
526 *self = Self::Px(if dpr > 0.0 {
527 linear.px / dpr
528 } else {
529 linear.px
530 });
531 return;
532 }
533
534 if is_near_zero(linear.px) {
535 *self = Self::Percentage(linear.percent * 100.0);
536 }
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use std::{assert_matches, rc::Rc};
544
545 use taffy::Size;
546
547 use super::*;
548 use crate::{Viewport, style::calc::CalcArena};
549
550 fn sizing() -> SizingContext {
551 SizingContext {
552 viewport: Viewport {
553 size: (200, 100).into(),
554 font_size: 16.0,
555 device_pixel_ratio: 2.0,
556 },
557 container_size: Size::NONE,
558 font_size: 10.0,
559 root_font_size: None,
560 line_height: 30.0,
561 root_line_height: Some(40.0),
562 calc_arena: Rc::new(CalcArena::default()),
563 }
564 }
565
566 fn assert_near(lhs: f32, rhs: f32) {
567 let diff = (lhs - rhs).abs();
568 assert!(diff < 0.0001, "lhs={lhs}, rhs={rhs}, diff={diff}");
569 }
570
571 #[test]
572 fn parse_calc_mixed_returns_formula() {
573 assert_eq!(
574 Length::<true>::from_str("calc(100% - 12px)"),
575 Ok(Length::Calc(CalcFormula {
576 percent: 1.0,
577 px: -12.0,
578 ..Default::default()
579 }))
580 );
581 }
582
583 #[test]
584 fn parse_calc_number_expression_becomes_px() {
585 let parsed = Length::<true>::from_str("calc(1 + 2)");
586 assert_eq!(parsed, Ok(Length::Px(3.0)));
587 }
588
589 #[test]
590 fn parse_calc_rejects_number_plus_length() {
591 let parsed = Length::<true>::from_str("calc(1 + 2px)");
592 assert!(parsed.is_err());
593 }
594
595 #[test]
596 fn parse_calc_rejects_division_by_zero() {
597 let parsed = Length::<true>::from_str("calc(10px / 0)");
598 assert!(parsed.is_err());
599 }
600
601 #[test]
602 fn negative_calc_keeps_value_sign_consistent() {
603 let value: Length<true> = Length::Calc(CalcFormula {
604 percent: 0.5,
605 px: 10.0,
606 ..Default::default()
607 });
608 let negated = -value;
609 let sizing = sizing();
610 assert_near(value.to_px(&sizing, 200.0), 120.0);
611 assert_near(negated.to_px(&sizing, 200.0), -120.0);
612 }
613
614 #[test]
615 fn make_computed_collapses_formula_without_percent_to_px() {
616 let mut value: Length<true> = Length::Calc(CalcFormula {
617 rem: 1.0,
618 px: 5.0,
619 ..Default::default()
620 });
621 value.make_computed(&sizing());
622 assert_eq!(value, Length::Px(21.0));
623 }
624
625 #[test]
626 fn make_computed_collapsed_px_applies_dpr_only_once_in_to_px() {
627 let mut value: Length<true> = Length::Calc(CalcFormula {
628 rem: 1.0,
629 px: 5.0,
630 ..Default::default()
631 });
632 let sizing = sizing();
633 value.make_computed(&sizing);
634
635 assert_eq!(value, Length::Px(21.0));
636 assert_eq!(value.to_px(&sizing, 0.0), 42.0);
637 }
638
639 #[test]
640 fn make_computed_collapses_formula_with_only_percent_to_percentage() {
641 let mut value: Length<true> = Length::Calc(CalcFormula {
642 percent: 0.5,
643 ..Default::default()
644 });
645 value.make_computed(&sizing());
646 assert_eq!(value, Length::Percentage(50.0));
647 }
648
649 #[test]
650 fn make_computed_keeps_mixed_formula_as_calc() {
651 let mut value: Length<true> = Length::Calc(CalcFormula {
652 percent: 0.5,
653 px: 10.0,
654 ..Default::default()
655 });
656 value.make_computed(&sizing());
657 assert_eq!(
658 value,
659 Length::Calc(CalcFormula {
660 percent: 0.5,
661 px: 10.0,
662 ..Default::default()
663 })
664 );
665 }
666
667 #[test]
668 fn compact_length_calc_pointer_resolves_through_callback() {
669 let value: Length<true> = Length::Calc(CalcFormula {
670 percent: 0.5,
671 px: 10.0,
672 ..Default::default()
673 });
674 let sizing = sizing();
675 let compact = value.to_compact_length(&sizing);
676 assert!(compact.is_calc());
677 let resolved = sizing
678 .calc_arena
679 .resolve_calc_value(compact.calc_value(), 200.0);
680 assert_near(resolved, 120.0);
681 }
682
683 #[test]
684 fn compact_length_percent_does_not_use_calc_pointer() {
685 let sizing = sizing();
686 let compact = Length::<true>::Percentage(50.0).to_compact_length(&sizing);
687 assert!(!compact.is_calc());
688 assert_eq!(compact.tag(), CompactLength::PERCENT_TAG);
689 assert_near(compact.value(), 0.5);
690 }
691
692 #[test]
693 fn to_px_applies_device_pixel_ratio_for_absolute_units() {
694 let px = Length::<true>::Rem(2.0).to_px(&sizing(), 100.0);
695 assert_near(px, 64.0);
696 }
697
698 fn descendant_sizing() -> SizingContext {
699 let mut sizing = sizing();
700 sizing.root_font_size = Some(32.0);
701 sizing
702 }
703
704 #[test]
705 fn rem_to_px_does_not_double_apply_dpr_when_root_font_size_set() {
706 let sizing = descendant_sizing();
707 assert_near(Length::<true>::Rem(1.0).to_px(&sizing, 0.0), 32.0);
708 assert_near(Length::<true>::Rem(2.0).to_px(&sizing, 0.0), 64.0);
709 assert_near(Length::<true>::Rem(0.5).to_px(&sizing, 0.0), 16.0);
710 }
711
712 #[test]
713 fn rem_to_compact_length_does_not_double_apply_dpr_when_root_font_size_set() {
714 let sizing = descendant_sizing();
715 let compact = Length::<true>::Rem(1.0).to_compact_length(&sizing);
716 assert_near(compact.value(), 32.0);
717 }
718
719 #[test]
720 fn calc_with_rem_does_not_double_apply_dpr_when_root_font_size_set() {
721 let sizing = descendant_sizing();
722 let value: Length<true> = Length::Calc(CalcFormula {
723 rem: 1.0,
724 ..Default::default()
725 });
726 assert_near(value.to_px(&sizing, 0.0), 32.0);
727 }
728
729 #[test]
730 fn calc_with_rem_and_px_does_not_double_apply_dpr_when_root_font_size_set() {
731 let sizing = descendant_sizing();
732 let value: Length<true> = Length::Calc(CalcFormula {
733 rem: 1.0,
734 px: 5.0,
735 ..Default::default()
736 });
737 assert_near(value.to_px(&sizing, 0.0), 42.0);
738 }
739
740 #[test]
741 fn make_computed_calc_with_rem_collapses_correctly_when_root_font_size_set() {
742 let mut value: Length<true> = Length::Calc(CalcFormula {
743 rem: 1.0,
744 px: 5.0,
745 ..Default::default()
746 });
747 let sizing = descendant_sizing();
748 value.make_computed(&sizing);
749 assert_eq!(value, Length::Px(21.0));
750 assert_near(value.to_px(&sizing, 0.0), 42.0);
751 }
752
753 #[test]
754 fn make_computed_em_applies_dpr_only_once_in_to_px() {
755 let mut value: Length<true> = Length::Em(1.5);
756 let sizing = sizing();
757 value.make_computed(&sizing);
758 assert_eq!(value, Length::Px(7.5));
759 assert_eq!(value.to_px(&sizing, 0.0), 15.0);
760 }
761
762 #[test]
763 fn parse_supports_modern_viewport_and_container_units() {
764 assert_eq!(Length::<true>::from_str("12dvw"), Ok(Length::Vw(12.0)));
765 assert_eq!(Length::<true>::from_str("12svw"), Ok(Length::Vw(12.0)));
766 assert_eq!(Length::<true>::from_str("12lvw"), Ok(Length::Vw(12.0)));
767 assert_eq!(Length::<true>::from_str("12cqw"), Ok(Length::CqW(12.0)));
768 assert_eq!(Length::<true>::from_str("12cqi"), Ok(Length::CqW(12.0)));
769 assert_eq!(Length::<true>::from_str("12vi"), Ok(Length::Vw(12.0)));
770 assert_eq!(Length::<true>::from_str("12dvh"), Ok(Length::Vh(12.0)));
771 assert_eq!(Length::<true>::from_str("12svh"), Ok(Length::Vh(12.0)));
772 assert_eq!(Length::<true>::from_str("12lvh"), Ok(Length::Vh(12.0)));
773 assert_eq!(Length::<true>::from_str("12cqh"), Ok(Length::CqH(12.0)));
774 assert_eq!(Length::<true>::from_str("12cqb"), Ok(Length::CqH(12.0)));
775 assert_eq!(Length::<true>::from_str("12vb"), Ok(Length::Vh(12.0)));
776 assert_eq!(Length::<true>::from_str("12vmin"), Ok(Length::VMin(12.0)));
777 assert_eq!(Length::<true>::from_str("12cqmin"), Ok(Length::CqMin(12.0)));
778 assert_eq!(Length::<true>::from_str("12vmax"), Ok(Length::VMax(12.0)));
779 assert_eq!(Length::<true>::from_str("12cqmax"), Ok(Length::CqMax(12.0)));
780 }
781
782 #[test]
783 fn parse_supports_lh_and_rlh_units() {
784 assert_eq!(Length::<true>::from_str("1.5lh"), Ok(Length::Lh(1.5)));
785 assert_eq!(Length::<true>::from_str("2rlh"), Ok(Length::Rlh(2.0)));
786 }
787
788 #[test]
789 fn lh_and_rlh_resolve_to_line_height_basis() {
790 let sizing = sizing();
791 assert_near(Length::<true>::Lh(1.0).to_px(&sizing, 0.0), 30.0);
792 assert_near(Length::<true>::Lh(2.0).to_px(&sizing, 0.0), 60.0);
793 assert_near(Length::<true>::Rlh(1.0).to_px(&sizing, 0.0), 40.0);
794 assert_near(Length::<true>::Rlh(0.5).to_px(&sizing, 0.0), 20.0);
795 }
796
797 #[test]
798 fn rlh_falls_back_to_element_line_height_when_root_unresolved() {
799 let mut sizing = sizing();
800 sizing.root_line_height = None;
801 assert_near(Length::<true>::Rlh(1.0).to_px(&sizing, 0.0), 30.0);
802 }
803
804 #[test]
805 fn parse_calc_supports_lh_and_rlh() {
806 let parsed = Length::<true>::from_str("calc(1lh + 2rlh - 3px)");
807 assert_eq!(
808 parsed,
809 Ok(Length::Calc(CalcFormula {
810 lh: 1.0,
811 rlh: 2.0,
812 px: -3.0,
813 ..Default::default()
814 }))
815 );
816 }
817
818 #[test]
819 fn calc_lh_resolves_through_line_height_basis() {
820 let sizing = sizing();
821 let parsed = Length::<true>::from_str("calc(1lh + 2px)");
822 assert_eq!(
823 parsed,
824 Ok(Length::Calc(CalcFormula {
825 lh: 1.0,
826 px: 2.0,
827 ..Default::default()
828 }))
829 );
830 if let Ok(value) = parsed {
831 assert_near(value.to_px(&sizing, 0.0), 34.0);
832 }
833 }
834
835 #[test]
836 fn make_computed_lh_collapses_to_px_in_pre_dpr_space() {
837 let mut value: Length<true> = Length::Lh(1.5);
838 let sizing = sizing();
839 value.make_computed(&sizing);
840 assert_eq!(value, Length::Px(22.5));
841 assert_eq!(value.to_px(&sizing, 0.0), 45.0);
842 }
843
844 #[test]
845 fn parse_calc_supports_modern_viewport_and_container_units() {
846 let parsed = Length::<true>::from_str("calc(20cqmax + 5px - 2cqb)");
847 assert_eq!(
848 parsed,
849 Ok(Length::Calc(CalcFormula {
850 cqmax: 20.0,
851 cqh: -2.0,
852 px: 5.0,
853 ..Default::default()
854 }))
855 );
856 }
857
858 #[test]
859 fn cq_lengths_use_container_size() {
860 let mut sizing = sizing();
861 sizing.container_size = Size {
862 width: Some(80.0),
863 height: Some(40.0),
864 };
865 assert_near(Length::<true>::CqW(50.0).to_px(&sizing, 0.0), 40.0);
866 assert_near(Length::<true>::CqH(50.0).to_px(&sizing, 0.0), 20.0);
867 assert_near(Length::<true>::CqMin(50.0).to_px(&sizing, 0.0), 20.0);
868 assert_near(Length::<true>::CqMax(50.0).to_px(&sizing, 0.0), 40.0);
869 }
870
871 #[test]
872 fn vmin_and_vmax_resolve_to_expected_pixels() {
873 let sizing = sizing();
874 assert_near(Length::<true>::VMin(50.0).to_px(&sizing, 0.0), 50.0);
875 assert_near(Length::<true>::VMax(50.0).to_px(&sizing, 0.0), 100.0);
876 }
877
878 #[test]
879 fn parse_calc_supports_constants() {
880 assert_eq!(
881 Length::<true>::from_str("calc(pi)").as_ref(),
882 Ok(&Length::Px(std::f32::consts::PI))
883 );
884 assert_eq!(
885 Length::<true>::from_str("calc(e)").as_ref(),
886 Ok(&Length::Px(std::f32::consts::E))
887 );
888
889 let inf = Length::<true>::from_str("calc(infinity)");
890 assert_matches!(inf, Ok(Length::Px(v)) if v.is_infinite() && v.is_sign_positive());
891
892 let neg_inf = Length::<true>::from_str("calc(-infinity)");
893 assert_matches!(neg_inf, Ok(Length::Px(v)) if v.is_infinite() && v.is_sign_negative());
894
895 let nan = Length::<true>::from_str("calc(nan)");
896 assert_matches!(nan, Ok(Length::Px(v)) if v.is_nan());
897 }
898
899 #[test]
900 fn parse_calc_infinity_times_length_clamps_in_to_px() {
901 let parsed = Length::<true>::from_str("calc(infinity * 1px)");
902 let sizing = sizing();
903 assert!(parsed.is_ok(), "expected successful parse, got {parsed:?}");
904 let Ok(length) = parsed else {
905 return;
906 };
907 let resolved = length.to_px(&sizing, 200.0);
908
909 assert_eq!(resolved, SAFE_INT_MAX_PX);
910 assert!(resolved.is_finite());
911 }
912}