whisker_css/data_type/length.rs
1//! `<length>` — a distance.
2//!
3//! Lynx reference: <https://lynxjs.org/api/css/data-type/length.html>
4//!
5//! Lynx supports a subset of CSS length units:
6//!
7//! | Variant | CSS | Reference |
8//! |---|---|---|
9//! | [`Length::Px`] | `px` | iOS points / Android dp |
10//! | [`Length::Rpx`] | `rpx` | Lynx-specific: `750rpx` = device width |
11//! | [`Length::Ppx`] | `ppx` | Physical pixels (device resolution) |
12//! | [`Length::Em`] | `em` | The element's computed `font-size` |
13//! | [`Length::Rem`] | `rem` | The root element's computed `font-size` |
14//! | [`Length::Vh`] | `vh` | 1% of viewport height |
15//! | [`Length::Vw`] | `vw` | 1% of viewport width |
16//! | [`Length::Zero`]| `0` | The unitless zero — only zero is allowed without a unit |
17//!
18//! The web units `cm`, `mm`, `in`, `pt`, `pc`, `ch`, `ex`, `lh`,
19//! `rlh` are **not** part of Lynx and are intentionally absent from
20//! this enum.
21
22use core::fmt;
23
24use crate::to_css::{write_number, ToCss};
25
26/// A CSS `<length>` value.
27#[derive(Copy, Clone, Debug, PartialEq)]
28pub enum Length {
29 /// Logical pixels (`px`). Maps to iOS points and Android dp.
30 Px(f32),
31 /// Lynx-specific responsive pixel (`rpx`). `750rpx` equals the
32 /// device's screen width regardless of pixel density.
33 Rpx(f32),
34 /// Physical pixels (`ppx`). One device pixel.
35 Ppx(f32),
36 /// Em (`em`) — relative to the element's computed `font-size`.
37 Em(f32),
38 /// Root em (`rem`) — relative to the root element's
39 /// computed `font-size`.
40 Rem(f32),
41 /// Viewport height (`vh`) — 1% of the viewport's height.
42 Vh(f32),
43 /// Viewport width (`vw`) — 1% of the viewport's width.
44 Vw(f32),
45 /// The unitless zero. CSS allows `0` (and only `0`) without a
46 /// unit; this variant covers that case so a non-zero unit-less
47 /// length is unrepresentable.
48 Zero,
49}
50
51impl Length {
52 /// Returns `true` when the length is exactly zero, regardless of
53 /// the unit chosen at construction.
54 pub fn is_zero(self) -> bool {
55 match self {
56 Length::Zero => true,
57 Length::Px(v)
58 | Length::Rpx(v)
59 | Length::Ppx(v)
60 | Length::Em(v)
61 | Length::Rem(v)
62 | Length::Vh(v)
63 | Length::Vw(v) => v == 0.0,
64 }
65 }
66}
67
68impl ToCss for Length {
69 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
70 let (v, unit) = match *self {
71 Length::Zero => return dest.write_char('0'),
72 Length::Px(v) => (v, "px"),
73 Length::Rpx(v) => (v, "rpx"),
74 Length::Ppx(v) => (v, "ppx"),
75 Length::Em(v) => (v, "em"),
76 Length::Rem(v) => (v, "rem"),
77 Length::Vh(v) => (v, "vh"),
78 Length::Vw(v) => (v, "vw"),
79 };
80 write_number(dest, v)?;
81 dest.write_str(unit)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn each_unit_serializes() {
91 assert_eq!(Length::Px(8.0).to_css_string(), "8px");
92 assert_eq!(Length::Rpx(750.0).to_css_string(), "750rpx");
93 assert_eq!(Length::Ppx(2.0).to_css_string(), "2ppx");
94 assert_eq!(Length::Em(1.5).to_css_string(), "1.5em");
95 assert_eq!(Length::Rem(1.0).to_css_string(), "1rem");
96 assert_eq!(Length::Vh(50.0).to_css_string(), "50vh");
97 assert_eq!(Length::Vw(100.0).to_css_string(), "100vw");
98 }
99
100 #[test]
101 fn zero_serializes_unitless() {
102 assert_eq!(Length::Zero.to_css_string(), "0");
103 }
104
105 #[test]
106 fn fractional_values_keep_decimal() {
107 assert_eq!(Length::Px(0.5).to_css_string(), "0.5px");
108 assert_eq!(Length::Px(-1.25).to_css_string(), "-1.25px");
109 }
110
111 #[test]
112 fn is_zero_detects_all_variants() {
113 assert!(Length::Zero.is_zero());
114 assert!(Length::Px(0.0).is_zero());
115 assert!(Length::Vh(0.0).is_zero());
116 assert!(!Length::Px(0.1).is_zero());
117 assert!(!Length::Rpx(1.0).is_zero());
118 }
119}