Skip to main content

takumi_css/style/properties/
length.rs

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
23/// Maps a CSS dimension unit (incl. aliases like `dvw`/`cqi`) to its canonical `Length` variant.
24pub(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
82/// A length value that defaults to zero instead of auto.
83pub type LengthDefaultsToZero = Length<false>;
84
85/// Represents a value that can be a specific length, percentage, or automatic.
86#[derive(Debug, Clone, PartialEq, Copy)]
87#[non_exhaustive]
88pub enum Length<const DEFAULT_AUTO: bool = true> {
89  /// Automatic sizing based on content
90  Auto,
91  /// Percentage value relative to parent container (0-100)
92  Percentage(f32),
93  /// Rem value relative to the root font size
94  Rem(f32),
95  /// Em value relative to the font size
96  Em(f32),
97  /// Lh value relative to the element's computed line-height
98  Lh(f32),
99  /// Rlh value relative to the root element's computed line-height
100  Rlh(f32),
101  /// Vh value relative to the viewport height (0-100)
102  Vh(f32),
103  /// Vw value relative to the viewport width (0-100)
104  Vw(f32),
105  /// Cqh value relative to the query container height (0-100)
106  CqH(f32),
107  /// Cqw value relative to the query container width (0-100)
108  CqW(f32),
109  /// Cqmin value relative to the query container smaller dimension (0-100)
110  CqMin(f32),
111  /// Cqmax value relative to the query container larger dimension (0-100)
112  CqMax(f32),
113  /// Vmin value relative to the smaller viewport dimension (0-100)
114  VMin(f32),
115  /// Vmax value relative to the larger viewport dimension (0-100)
116  VMax(f32),
117  /// Centimeter value
118  Cm(f32),
119  /// Millimeter value
120  Mm(f32),
121  /// Inch value
122  In(f32),
123  /// Quarter value
124  Q(f32),
125  /// Point value
126  Pt(f32),
127  /// Picas value
128  Pc(f32),
129  /// Specific pixel value
130  Px(f32),
131  /// calc(...) expression
132  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  /// Construct a length from a Tailwind spacing-scale multiplier.
147  #[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  /// Returns a zero pixel length unit.
288  pub const fn zero() -> Self {
289    Self::Px(0.0)
290  }
291
292  /// Negated value, or `None` for non-negatable forms like `auto`.
293  pub fn try_negative(self) -> Option<Self> {
294    if matches!(self, Length::Auto) {
295      return None;
296    }
297    Some(self.negative())
298  }
299
300  /// Returns a negative length unit.
301  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      // Calc linear values are already in device pixels.
407      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    // Only absolute units carry a device-pixel-ratio factor.
461    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}