css_ast/units/
length.rs

1use css_parse::{Build, Cursor, Parser, Peek, T, ToNumberValue};
2use csskit_derives::{IntoCursor, Peek, ToCursors, Visitable};
3
4use super::Flex;
5
6macro_rules! apply_lengths {
7	($ident: ident) => {
8		$ident! {
9			// https://drafts.csswg.org/css-values/#font-relative-lengths
10			Em,
11			Rem,
12			Ex,
13			Rex,
14			Cap,
15			Rcap,
16			Ch,
17			Rch,
18			Ic,
19			Ric,
20			Lh,
21			Rlh,
22
23			// https://drafts.csswg.org/css-values/#viewport-relative-units
24			Vw,
25			Svw,
26			Lvw,
27			Dvw,
28			Vh,
29			Svh,
30			Lvh,
31			Dvh,
32			Vi,
33			Svi,
34			Lvi,
35			Dvi,
36			Vb,
37			Svb,
38			Lvb,
39			Dvb,
40			Vmin,
41			Svmin,
42			Lvmin,
43			Dvmin,
44			Vmax,
45			Svmax,
46			Lvmax,
47			Dvmax,
48
49			// https://drafts.csswg.org/css-values/#absolute-lengths
50			Cm,
51			Mm,
52			Q,
53			In,
54			Pc,
55			Pt,
56			Px,
57
58			// https://www.w3.org/TR/css-contain-3/#container-lengths
59			Cqw,
60			Cqh,
61			Cqi,
62			Cqb,
63			Cqmin,
64			Cqmax,
65		}
66	};
67}
68
69macro_rules! define_length {
70	( $($name: ident),+ $(,)* ) => {
71		#[derive(ToCursors, IntoCursor, Visitable, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
72		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(tag = "type", content = "value", rename_all = "kebab-case"))]
73		#[visit(self)]
74		pub enum Length {
75			Zero(T![Number]),
76			$($name(T![Dimension::$name]),)+
77		}
78	}
79}
80apply_lengths!(define_length);
81
82impl Length {
83	const PX_CM: f32 = Self::PX_IN / 2.54;
84	const PX_MM: f32 = Self::PX_IN / 25.4;
85	const PX_Q: f32 = Self::PX_MM / 4.0;
86	const PX_IN: f32 = 96.0;
87	const PX_PC: f32 = Self::PX_IN / 6.0;
88	const PX_PT: f32 = Self::PX_IN / 72.0;
89
90	pub fn to_px(&self) -> Option<f32> {
91		match self {
92			Self::Zero(_) => Some(0.0),
93			Self::Cm(d) => Some(Into::<f32>::into(*d) * Self::PX_CM),
94			Self::Mm(d) => Some(Into::<f32>::into(*d) * Self::PX_MM),
95			Self::Q(d) => Some(Into::<f32>::into(*d) * Self::PX_Q),
96			Self::In(d) => Some(Into::<f32>::into(*d) * Self::PX_IN),
97			Self::Pc(d) => Some(Into::<f32>::into(*d) * Self::PX_PC),
98			Self::Pt(d) => Some(Into::<f32>::into(*d) * Self::PX_PT),
99			_ => None,
100		}
101	}
102}
103
104impl From<Length> for f32 {
105	fn from(val: Length) -> Self {
106		macro_rules! match_length {
107			( $($name: ident),+ $(,)* ) => {
108				match val {
109					Length::Zero(_) => 0.0,
110					$(Length::$name(f) => f.into()),+
111				}
112			}
113		}
114		apply_lengths!(match_length)
115	}
116}
117
118impl PartialEq<f32> for Length {
119	fn eq(&self, other: &f32) -> bool {
120		let f: f32 = (*self).into();
121		f == *other
122	}
123}
124
125impl ToNumberValue for Length {
126	fn to_number_value(&self) -> Option<f32> {
127		Some((*self).into())
128	}
129}
130
131impl<'a> Peek<'a> for Length {
132	fn peek(p: &Parser<'a>, c: Cursor) -> bool {
133		macro_rules! is_checks {
134			( $($name: ident),+ $(,)* ) => {
135				(<T![Number]>::peek(p, c) && c.token().value() == 0.0)
136					$(|| <T![Dimension::$name]>::peek(p, c))+
137			}
138		}
139		apply_lengths!(is_checks)
140	}
141}
142
143impl<'a> Build<'a> for Length {
144	fn build(p: &Parser<'a>, c: Cursor) -> Self {
145		debug_assert!(Self::peek(p, c));
146		macro_rules! build_steps {
147			( $($name: ident),+ $(,)* ) => {
148				$(if <T![Dimension::$name]>::peek(p, c) {
149					Self::$name(<T![Dimension::$name]>::build(p, c))
150				} else )+ {
151					Self::Zero(<T![Number]>::build(p, c))
152				}
153			}
154		}
155		apply_lengths!(build_steps)
156	}
157}
158
159macro_rules! define_length_percentage {
160	( $($name: ident),+ $(,)* ) => {
161		#[derive(ToCursors, IntoCursor, Visitable, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
162		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(tag = "type", content = "value", rename_all = "kebab-case"))]
163		#[visit(self)]
164		pub enum LengthPercentage {
165			Zero(T![Number]),
166			$($name(T![Dimension::$name]),)+
167			Percent(T![Dimension::%]),
168		}
169	}
170}
171apply_lengths!(define_length_percentage);
172
173impl From<LengthPercentage> for f32 {
174	fn from(val: LengthPercentage) -> Self {
175		macro_rules! match_length {
176			( $($name: ident),+ $(,)* ) => {
177				match val {
178					LengthPercentage::Zero(_) => 0.0,
179					LengthPercentage::Percent(f) => f.into(),
180					$(LengthPercentage::$name(f) => f.into()),+
181				}
182			}
183		}
184		apply_lengths!(match_length)
185	}
186}
187
188impl ToNumberValue for LengthPercentage {
189	fn to_number_value(&self) -> Option<f32> {
190		Some((*self).into())
191	}
192}
193
194impl<'a> Peek<'a> for LengthPercentage {
195	fn peek(p: &Parser<'a>, c: Cursor) -> bool {
196		macro_rules! is_checks {
197			( $($name: ident),+ $(,)* ) => {
198				(<T![Number]>::peek(p, c) && c.token().value() == 0.0)
199				|| <T![Dimension::%]>::peek(p, c)
200					$(|| <T![Dimension::$name]>::peek(p, c))+
201			}
202		}
203		apply_lengths!(is_checks)
204	}
205}
206
207impl<'a> Build<'a> for LengthPercentage {
208	fn build(p: &Parser<'a>, c: Cursor) -> Self {
209		debug_assert!(Self::peek(p, c));
210		macro_rules! build_steps {
211			( $($name: ident),+ $(,)* ) => {
212				$(if <T![Dimension::$name]>::peek(p, c) {
213					Self::$name(<T![Dimension::$name]>::build(p, c))
214				} else )+ if <T![Dimension::%]>::peek(p, c) {
215					Self::Percent(<T![Dimension::%]>::build(p, c))
216				} else {
217					Self::Zero(<T![Number]>::build(p, c))
218				}
219			}
220		}
221		apply_lengths!(build_steps)
222	}
223}
224
225#[derive(IntoCursor, Peek, ToCursors, Visitable, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
226#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(rename_all = "kebab-case"))]
227#[visit(children)]
228pub enum LengthPercentageOrFlex {
229	Flex(Flex),
230	LengthPercentage(LengthPercentage),
231}
232
233impl<'a> Build<'a> for LengthPercentageOrFlex {
234	fn build(p: &Parser<'a>, c: Cursor) -> Self {
235		debug_assert!(Self::peek(p, c));
236		if Flex::peek(p, c) {
237			Self::Flex(Flex::build(p, c))
238		} else {
239			Self::LengthPercentage(LengthPercentage::build(p, c))
240		}
241	}
242}
243
244#[derive(Peek, ToCursors, IntoCursor, Visitable, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
245#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
246#[visit]
247pub enum NumberLength {
248	#[visit(skip)]
249	Number(T![Number]),
250	Length(Length),
251}
252
253impl From<NumberLength> for f32 {
254	fn from(val: NumberLength) -> Self {
255		match val {
256			NumberLength::Number(n) => n.into(),
257			NumberLength::Length(n) => n.into(),
258		}
259	}
260}
261
262impl ToNumberValue for NumberLength {
263	fn to_number_value(&self) -> Option<f32> {
264		Some((*self).into())
265	}
266}
267
268impl<'a> Build<'a> for NumberLength {
269	fn build(p: &Parser<'a>, c: Cursor) -> Self {
270		debug_assert!(Self::peek(p, c));
271		if Length::peek(p, c) { Self::Length(Length::build(p, c)) } else { Self::Number(<T![Number]>::build(p, c)) }
272	}
273}
274
275#[derive(Peek, ToCursors, IntoCursor, Visitable, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
276#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
277#[visit(self)]
278pub enum NumberPercentage {
279	Number(T![Number]),
280	Percentage(T![Dimension::%]),
281}
282
283impl From<NumberPercentage> for f32 {
284	fn from(val: NumberPercentage) -> Self {
285		match val {
286			NumberPercentage::Number(n) => n.into(),
287			NumberPercentage::Percentage(n) => n.into(),
288		}
289	}
290}
291
292impl ToNumberValue for NumberPercentage {
293	fn to_number_value(&self) -> Option<f32> {
294		Some((*self).into())
295	}
296}
297
298impl<'a> Build<'a> for NumberPercentage {
299	fn build(p: &Parser<'a>, c: Cursor) -> Self {
300		debug_assert!(Self::peek(p, c));
301		if <T![Number]>::peek(p, c) {
302			Self::Number(<T![Number]>::build(p, c))
303		} else {
304			Self::Percentage(<T![Dimension::%]>::build(p, c))
305		}
306	}
307}
308
309#[cfg(test)]
310mod tests {
311	use super::*;
312	use css_parse::assert_parse;
313
314	#[test]
315	fn size_test() {
316		assert_eq!(std::mem::size_of::<Length>(), 16);
317		assert_eq!(std::mem::size_of::<LengthPercentage>(), 16);
318		assert_eq!(std::mem::size_of::<NumberLength>(), 16);
319		assert_eq!(std::mem::size_of::<NumberPercentage>(), 16);
320	}
321
322	#[test]
323	fn test_writes() {
324		assert_parse!(Length, "10px");
325		// Truncates to 7dp
326		assert_parse!(Length, "1.2345679px");
327		// Removes redundant dp
328		assert_parse!(Length, "-1px");
329		// Percent
330		assert_parse!(LengthPercentage, "1%");
331	}
332}