⎇ 4a56ad2392dd569cc975530b5ae8cf0536f7f3b2
src/style/alignment.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
Implement `safe` and `unsafe` overflow-position alignment keywords (#952)
1//! Style types for controlling alignment.
2//!
3//! The public alignment types ([`AlignItems`], [`AlignContent`], and their aliases) are
4//! structs with two orthogonal fields: a *position* keyword
5//! ([`AlignItemsKeyword`] / [`AlignContentKeyword`]) and an *overflow-position*
6//! modifier ([`AlignmentSafety`]). The pre-existing CSS spellings — `Start`, `End`,
7//! `FlexStart`, `FlexEnd`, `Center`, `Stretch`, `SpaceBetween`, …, `SafeStart`,
8//! `SafeEnd`, `SafeFlexStart`, `SafeFlexEnd`, `SafeCenter` — are exposed as associated
9//! constants on the structs, so call sites read identically to the previous enum form.
10
11#[cfg(feature = "parse")]
12use crate::util::parse::{CssParseResult, FromCss, Parser, Token};
13
14/// The position-keyword half of [`AlignItems`] (and its aliases `AlignSelf`,
15/// `JustifyItems`, `JustifySelf`).
16///
17/// Compute paths match on this enum directly so every match is exhaustive and
18/// requires no `Safe*` siblings.
19#[derive(Copy, Clone, PartialEq, Eq, Debug)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21#[repr(u8)]
22pub enum AlignItemsKeyword {
23 /// Items are packed toward the start of the axis.
24 Start,
25 /// Items are packed toward the end of the axis.
26 End,
27 /// Items are packed towards the flex-relative start of the axis.
28 ///
29 /// For flex containers with flex_direction RowReverse or ColumnReverse this is
30 /// equivalent to End. In all other cases it is equivalent to Start.
31 FlexStart,
32 /// Items are packed towards the flex-relative end of the axis.
33 ///
34 /// For flex containers with flex_direction RowReverse or ColumnReverse this is
35 /// equivalent to Start. In all other cases it is equivalent to End.
36 FlexEnd,
37 /// Items are packed along the center of the cross axis.
38 Center,
39 /// Items are aligned such as their baselines align.
40 Baseline,
41 /// Stretch to fill the container.
42 Stretch,
43}
44
45/// The position-keyword half of [`AlignContent`] (and its alias `JustifyContent`).
46///
47/// Compute paths match on this enum directly so every match is exhaustive and
48/// requires no `Safe*` siblings.
49#[derive(Copy, Clone, PartialEq, Eq, Debug)]
50#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
51#[repr(u8)]
52pub enum AlignContentKeyword {
53 /// Items are packed toward the start of the axis.
54 Start,
55 /// Items are packed toward the end of the axis.
56 End,
57 /// Items are packed towards the flex-relative start of the axis.
58 FlexStart,
59 /// Items are packed towards the flex-relative end of the axis.
60 FlexEnd,
61 /// Items are centered around the middle of the axis.
62 Center,
63 /// Items are stretched to fill the container.
64 Stretch,
65 /// The first and last items are aligned flush with the edges of the container
66 /// (no gap). The gap between items is distributed evenly.
67 SpaceBetween,
68 /// The gap between the first and last items is exactly THE SAME as the gap
69 /// between items. The gaps are distributed evenly.
70 SpaceEvenly,
71 /// The gap between the first and last items is exactly HALF the gap between
72 /// items. The gaps are distributed evenly in proportion to these ratios.
73 SpaceAround,
74}
75
76impl AlignContentKeyword {
77 /// Returns the reversed keyword for RTL (right-to-left) contexts: `Start`↔`End`,
78 /// `FlexStart`↔`FlexEnd`. `Stretch` maps to `End` to preserve the layout
79 /// algorithms' historical handling. Center and the distribution keywords
80 /// (`SpaceBetween`, `SpaceEvenly`, `SpaceAround`) are unaffected because their
81 /// visual placement is direction-symmetric.
82 pub(crate) fn reversed(self) -> Self {
83 match self {
84 Self::Start => Self::End,
85 Self::End => Self::Start,
86 Self::FlexStart => Self::FlexEnd,
87 Self::FlexEnd => Self::FlexStart,
88 Self::Stretch => Self::End,
89 Self::Center | Self::SpaceBetween | Self::SpaceEvenly | Self::SpaceAround => self,
90 }
91 }
92}
93
94/// The overflow-position modifier per [CSS Box Alignment §4.3][css-align-overflow].
95///
96/// `Safe` falls back to start-edge alignment when the alignment subject would
97/// overflow the alignment container, so the start of the content stays visible.
98/// `Unsafe` (the default) keeps the requested alignment even when that causes
99/// overflow at the start edge.
100///
101/// CSS only defines `safe` / `unsafe` against the position values `start`, `end`,
102/// `flex-start`, `flex-end`, `center`. The struct shape does not enforce that
103/// constraint at the type level — the parser rejects invalid combinations, and
104/// the compute pass treats `Safe` paired with a non-position keyword (`Stretch`,
105/// `Baseline`, `Space*`) the same as `Unsafe`.
106///
107/// [css-align-overflow]: https://www.w3.org/TR/css-align-3/#overflow-values
108#[derive(Copy, Clone, PartialEq, Eq, Debug)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110#[repr(u8)]
111pub enum AlignmentSafety {
112 /// Default — keeps the requested alignment even when the subject overflows the
113 /// alignment container at the start edge.
114 Unsafe,
115 /// Falls back to the start edge when the subject would overflow, to avoid data
116 /// loss.
117 Safe,
118}
119
120/// Used to control how child nodes are aligned.
121/// For Flexbox it controls alignment in the cross axis.
122/// For Grid it controls alignment in the block axis.
123///
124/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
125#[derive(Copy, Clone, PartialEq, Eq, Debug)]
126pub struct AlignItems {
127 /// Position keyword.
128 pub keyword: AlignItemsKeyword,
129 /// Overflow-position modifier (`safe` / `unsafe`).
130 pub safety: AlignmentSafety,
131}
132
133#[allow(non_upper_case_globals)]
134impl AlignItems {
135 /// Items are packed toward the start of the axis.
136 pub const Start: Self = Self { keyword: AlignItemsKeyword::Start, safety: AlignmentSafety::Unsafe };
137 /// Items are packed toward the end of the axis.
138 pub const End: Self = Self { keyword: AlignItemsKeyword::End, safety: AlignmentSafety::Unsafe };
139 /// Items are packed towards the flex-relative start of the axis.
140 pub const FlexStart: Self = Self { keyword: AlignItemsKeyword::FlexStart, safety: AlignmentSafety::Unsafe };
141 /// Items are packed towards the flex-relative end of the axis.
142 pub const FlexEnd: Self = Self { keyword: AlignItemsKeyword::FlexEnd, safety: AlignmentSafety::Unsafe };
143 /// Items are packed along the center of the cross axis.
144 pub const Center: Self = Self { keyword: AlignItemsKeyword::Center, safety: AlignmentSafety::Unsafe };
145 /// Items are aligned such as their baselines align.
146 pub const Baseline: Self = Self { keyword: AlignItemsKeyword::Baseline, safety: AlignmentSafety::Unsafe };
147 /// Stretch to fill the container.
148 pub const Stretch: Self = Self { keyword: AlignItemsKeyword::Stretch, safety: AlignmentSafety::Unsafe };
149 /// Like [`AlignItems::Start`], but falls back to [`AlignItems::Start`] when the
150 /// alignment subject overflows the alignment container, to avoid data loss.
151 pub const SafeStart: Self = Self { keyword: AlignItemsKeyword::Start, safety: AlignmentSafety::Safe };
152 /// Like [`AlignItems::End`], but falls back to [`AlignItems::Start`] when the
153 /// alignment subject overflows the alignment container, to avoid data loss.
154 pub const SafeEnd: Self = Self { keyword: AlignItemsKeyword::End, safety: AlignmentSafety::Safe };
155 /// Like [`AlignItems::FlexStart`], but falls back to [`AlignItems::Start`] when the
156 /// alignment subject overflows the alignment container, to avoid data loss.
157 pub const SafeFlexStart: Self = Self { keyword: AlignItemsKeyword::FlexStart, safety: AlignmentSafety::Safe };
158 /// Like [`AlignItems::FlexEnd`], but falls back to [`AlignItems::Start`] when the
159 /// alignment subject overflows the alignment container, to avoid data loss.
160 pub const SafeFlexEnd: Self = Self { keyword: AlignItemsKeyword::FlexEnd, safety: AlignmentSafety::Safe };
161 /// Like [`AlignItems::Center`], but falls back to [`AlignItems::Start`] when the
162 /// alignment subject overflows the alignment container, to avoid data loss.
163 pub const SafeCenter: Self = Self { keyword: AlignItemsKeyword::Center, safety: AlignmentSafety::Safe };
164
165 /// Returns `true` iff this carries the `safe` overflow-position modifier.
166 #[inline]
167 pub const fn is_safe(self) -> bool {
168 matches!(self.safety, AlignmentSafety::Safe)
169 }
170
171 /// Returns the underlying position keyword, discarding the safety modifier.
172 #[inline]
173 pub const fn keyword(self) -> AlignItemsKeyword {
174 self.keyword
175 }
176}
177
178#[cfg(feature = "parse")]
179impl FromCss for AlignItems {
180 fn from_css<'i>(input: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
181 let first = input.expect_ident()?.clone();
182 cssparser::match_ignore_ascii_case! { &*first,
183 "safe" => {
184 let pos = input.expect_ident()?.clone();
185 cssparser::match_ignore_ascii_case! { &*pos,
186 "start" => Ok(Self::SafeStart),
187 "end" => Ok(Self::SafeEnd),
188 "flex-start" => Ok(Self::SafeFlexStart),
189 "flex-end" => Ok(Self::SafeFlexEnd),
190 "center" => Ok(Self::SafeCenter),
191 _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
192 }
193 },
194 "unsafe" => {
195 let pos = input.expect_ident()?.clone();
196 cssparser::match_ignore_ascii_case! { &*pos,
197 "start" => Ok(Self::Start),
198 "end" => Ok(Self::End),
199 "flex-start" => Ok(Self::FlexStart),
200 "flex-end" => Ok(Self::FlexEnd),
201 "center" => Ok(Self::Center),
202 _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
203 }
204 },
205 "start" => Ok(Self::Start),
206 "end" => Ok(Self::End),
207 "flex-start" => Ok(Self::FlexStart),
208 "flex-end" => Ok(Self::FlexEnd),
209 "center" => Ok(Self::Center),
210 "baseline" => Ok(Self::Baseline),
211 "stretch" => Ok(Self::Stretch),
212 _ => Err(input.new_unexpected_token_error(Token::Ident(first))),
213 }
214 }
215}
216
217#[cfg(feature = "parse")]
218crate::util::parse::from_str_from_css!(AlignItems);
219
220/// Used to control how child nodes are aligned.
221/// Does not apply to Flexbox, and will be ignored if specified on a flex container.
222/// For Grid it controls alignment in the inline axis.
223///
224/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
225pub type JustifyItems = AlignItems;
226/// Controls alignment of an individual node.
227///
228/// Overrides the parent Node's `AlignItems` property.
229/// For Flexbox it controls alignment in the cross axis.
230/// For Grid it controls alignment in the block axis.
231///
232/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
233pub type AlignSelf = AlignItems;
234/// Controls alignment of an individual node.
235///
236/// Overrides the parent Node's `JustifyItems` property.
237/// Does not apply to Flexbox, and will be ignored if specified on a flex child.
238/// For Grid it controls alignment in the inline axis.
239///
240/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
241pub type JustifySelf = AlignItems;
242
243/// Sets the distribution of space between and around content items.
244/// For Flexbox it controls alignment in the cross axis.
245/// For Grid it controls alignment in the block axis.
246///
247/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
248#[derive(Copy, Clone, PartialEq, Eq, Debug)]
249pub struct AlignContent {
250 /// Position keyword.
251 pub keyword: AlignContentKeyword,
252 /// Overflow-position modifier (`safe` / `unsafe`).
253 pub safety: AlignmentSafety,
254}
255
256#[allow(non_upper_case_globals)]
257impl AlignContent {
258 /// Items are packed toward the start of the axis.
259 pub const Start: Self = Self { keyword: AlignContentKeyword::Start, safety: AlignmentSafety::Unsafe };
260 /// Items are packed toward the end of the axis.
261 pub const End: Self = Self { keyword: AlignContentKeyword::End, safety: AlignmentSafety::Unsafe };
262 /// Items are packed towards the flex-relative start of the axis.
263 pub const FlexStart: Self = Self { keyword: AlignContentKeyword::FlexStart, safety: AlignmentSafety::Unsafe };
264 /// Items are packed towards the flex-relative end of the axis.
265 pub const FlexEnd: Self = Self { keyword: AlignContentKeyword::FlexEnd, safety: AlignmentSafety::Unsafe };
266 /// Items are centered around the middle of the axis.
267 pub const Center: Self = Self { keyword: AlignContentKeyword::Center, safety: AlignmentSafety::Unsafe };
268 /// Items are stretched to fill the container.
269 pub const Stretch: Self = Self { keyword: AlignContentKeyword::Stretch, safety: AlignmentSafety::Unsafe };
270 /// The first and last items are aligned flush with the edges of the container.
271 pub const SpaceBetween: Self = Self { keyword: AlignContentKeyword::SpaceBetween, safety: AlignmentSafety::Unsafe };
272 /// The gap between the first and last items equals the gap between items.
273 pub const SpaceEvenly: Self = Self { keyword: AlignContentKeyword::SpaceEvenly, safety: AlignmentSafety::Unsafe };
274 /// The gap between the first and last items is half the gap between items.
275 pub const SpaceAround: Self = Self { keyword: AlignContentKeyword::SpaceAround, safety: AlignmentSafety::Unsafe };
276 /// Like [`AlignContent::Start`], but falls back to [`AlignContent::Start`] when the
277 /// content overflows the alignment container, to avoid data loss.
278 pub const SafeStart: Self = Self { keyword: AlignContentKeyword::Start, safety: AlignmentSafety::Safe };
279 /// Like [`AlignContent::End`], but falls back to [`AlignContent::Start`] when the
280 /// content overflows the alignment container, to avoid data loss.
281 pub const SafeEnd: Self = Self { keyword: AlignContentKeyword::End, safety: AlignmentSafety::Safe };
282 /// Like [`AlignContent::FlexStart`], but falls back to [`AlignContent::Start`] when
283 /// the content overflows the alignment container, to avoid data loss.
284 pub const SafeFlexStart: Self = Self { keyword: AlignContentKeyword::FlexStart, safety: AlignmentSafety::Safe };
285 /// Like [`AlignContent::FlexEnd`], but falls back to [`AlignContent::Start`] when the
286 /// content overflows the alignment container, to avoid data loss.
287 pub const SafeFlexEnd: Self = Self { keyword: AlignContentKeyword::FlexEnd, safety: AlignmentSafety::Safe };
288 /// Like [`AlignContent::Center`], but falls back to [`AlignContent::Start`] when the
289 /// content overflows the alignment container, to avoid data loss.
290 pub const SafeCenter: Self = Self { keyword: AlignContentKeyword::Center, safety: AlignmentSafety::Safe };
291
292 /// Returns `true` iff this carries the `safe` overflow-position modifier.
293 #[inline]
294 pub const fn is_safe(self) -> bool {
295 matches!(self.safety, AlignmentSafety::Safe)
296 }
297
298 /// Returns the underlying position keyword, discarding the safety modifier.
299 #[inline]
300 pub const fn keyword(self) -> AlignContentKeyword {
301 self.keyword
302 }
303}
304
305#[cfg(feature = "parse")]
306impl FromCss for AlignContent {
307 fn from_css<'i>(input: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
308 let first = input.expect_ident()?.clone();
309 cssparser::match_ignore_ascii_case! { &*first,
310 "safe" => {
311 let pos = input.expect_ident()?.clone();
312 cssparser::match_ignore_ascii_case! { &*pos,
313 "start" => Ok(Self::SafeStart),
314 "end" => Ok(Self::SafeEnd),
315 "flex-start" => Ok(Self::SafeFlexStart),
316 "flex-end" => Ok(Self::SafeFlexEnd),
317 "center" => Ok(Self::SafeCenter),
318 _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
319 }
320 },
321 "unsafe" => {
322 let pos = input.expect_ident()?.clone();
323 cssparser::match_ignore_ascii_case! { &*pos,
324 "start" => Ok(Self::Start),
325 "end" => Ok(Self::End),
326 "flex-start" => Ok(Self::FlexStart),
327 "flex-end" => Ok(Self::FlexEnd),
328 "center" => Ok(Self::Center),
329 _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
330 }
331 },
332 "start" => Ok(Self::Start),
333 "end" => Ok(Self::End),
334 "flex-start" => Ok(Self::FlexStart),
335 "flex-end" => Ok(Self::FlexEnd),
336 "center" => Ok(Self::Center),
337 "stretch" => Ok(Self::Stretch),
338 "space-between" => Ok(Self::SpaceBetween),
339 "space-evenly" => Ok(Self::SpaceEvenly),
340 "space-around" => Ok(Self::SpaceAround),
341 _ => Err(input.new_unexpected_token_error(Token::Ident(first))),
342 }
343 }
344}
345
346#[cfg(feature = "parse")]
347crate::util::parse::from_str_from_css!(AlignContent);
348
349/// Sets the distribution of space between and around content items.
350/// For Flexbox it controls alignment in the main axis.
351/// For Grid it controls alignment in the inline axis.
352///
353/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
354pub type JustifyContent = AlignContent;
355
356// ---------------------------------------------------------------------------
357// Serde — custom impls preserve the pre-struct wire format (single tag string
358// per public spelling) so consumers reading data serialized before the refactor
359// continue to deserialize correctly.
360// ---------------------------------------------------------------------------
361
362/// Canonical tag-string set accepted by [`AlignItems`] serde deserialization, used in
363/// `unknown_variant` errors. Mirrors the spellings produced by `Serialize`.
364#[cfg(feature = "serde")]
365const ALIGN_ITEMS_NAMES: &[&str] = &[
366 "Start",
367 "End",
368 "FlexStart",
369 "FlexEnd",
370 "Center",
371 "Baseline",
372 "Stretch",
373 "SafeStart",
374 "SafeEnd",
375 "SafeFlexStart",
376 "SafeFlexEnd",
377 "SafeCenter",
378];
379
380#[cfg(feature = "serde")]
381impl serde::Serialize for AlignItems {
382 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
383 let name = match (self.keyword, self.safety) {
384 (AlignItemsKeyword::Start, AlignmentSafety::Unsafe) => "Start",
385 (AlignItemsKeyword::End, AlignmentSafety::Unsafe) => "End",
386 (AlignItemsKeyword::FlexStart, AlignmentSafety::Unsafe) => "FlexStart",
387 (AlignItemsKeyword::FlexEnd, AlignmentSafety::Unsafe) => "FlexEnd",
388 (AlignItemsKeyword::Center, AlignmentSafety::Unsafe) => "Center",
389 (AlignItemsKeyword::Baseline, _) => "Baseline",
390 (AlignItemsKeyword::Stretch, _) => "Stretch",
391 (AlignItemsKeyword::Start, AlignmentSafety::Safe) => "SafeStart",
392 (AlignItemsKeyword::End, AlignmentSafety::Safe) => "SafeEnd",
393 (AlignItemsKeyword::FlexStart, AlignmentSafety::Safe) => "SafeFlexStart",
394 (AlignItemsKeyword::FlexEnd, AlignmentSafety::Safe) => "SafeFlexEnd",
395 (AlignItemsKeyword::Center, AlignmentSafety::Safe) => "SafeCenter",
396 };
397 serializer.serialize_str(name)
398 }
399}
400
401#[cfg(feature = "serde")]
402impl<'de> serde::Deserialize<'de> for AlignItems {
403 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
404 struct AlignItemsVisitor;
405 impl<'de> serde::de::Visitor<'de> for AlignItemsVisitor {
406 type Value = AlignItems;
407 fn expecting(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
408 fmt.write_str("an AlignItems variant tag string")
409 }
410 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
411 Ok(match v {
412 "Start" => AlignItems::Start,
413 "End" => AlignItems::End,
414 "FlexStart" => AlignItems::FlexStart,
415 "FlexEnd" => AlignItems::FlexEnd,
416 "Center" => AlignItems::Center,
417 "Baseline" => AlignItems::Baseline,
418 "Stretch" => AlignItems::Stretch,
419 "SafeStart" => AlignItems::SafeStart,
420 "SafeEnd" => AlignItems::SafeEnd,
421 "SafeFlexStart" => AlignItems::SafeFlexStart,
422 "SafeFlexEnd" => AlignItems::SafeFlexEnd,
423 "SafeCenter" => AlignItems::SafeCenter,
424 other => return Err(E::unknown_variant(other, ALIGN_ITEMS_NAMES)),
425 })
426 }
427 }
428 deserializer.deserialize_str(AlignItemsVisitor)
429 }
430}
431
432/// Canonical tag-string set accepted by [`AlignContent`] serde deserialization, used in
433/// `unknown_variant` errors. Mirrors the spellings produced by `Serialize`.
434#[cfg(feature = "serde")]
435const ALIGN_CONTENT_NAMES: &[&str] = &[
436 "Start",
437 "End",
438 "FlexStart",
439 "FlexEnd",
440 "Center",
441 "Stretch",
442 "SpaceBetween",
443 "SpaceEvenly",
444 "SpaceAround",
445 "SafeStart",
446 "SafeEnd",
447 "SafeFlexStart",
448 "SafeFlexEnd",
449 "SafeCenter",
450];
451
452#[cfg(feature = "serde")]
453impl serde::Serialize for AlignContent {
454 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
455 let name = match (self.keyword, self.safety) {
456 (AlignContentKeyword::Start, AlignmentSafety::Unsafe) => "Start",
457 (AlignContentKeyword::End, AlignmentSafety::Unsafe) => "End",
458 (AlignContentKeyword::FlexStart, AlignmentSafety::Unsafe) => "FlexStart",
459 (AlignContentKeyword::FlexEnd, AlignmentSafety::Unsafe) => "FlexEnd",
460 (AlignContentKeyword::Center, AlignmentSafety::Unsafe) => "Center",
461 (AlignContentKeyword::Stretch, _) => "Stretch",
462 (AlignContentKeyword::SpaceBetween, _) => "SpaceBetween",
463 (AlignContentKeyword::SpaceEvenly, _) => "SpaceEvenly",
464 (AlignContentKeyword::SpaceAround, _) => "SpaceAround",
465 (AlignContentKeyword::Start, AlignmentSafety::Safe) => "SafeStart",
466 (AlignContentKeyword::End, AlignmentSafety::Safe) => "SafeEnd",
467 (AlignContentKeyword::FlexStart, AlignmentSafety::Safe) => "SafeFlexStart",
468 (AlignContentKeyword::FlexEnd, AlignmentSafety::Safe) => "SafeFlexEnd",
469 (AlignContentKeyword::Center, AlignmentSafety::Safe) => "SafeCenter",
470 };
471 serializer.serialize_str(name)
472 }
473}
474
475#[cfg(feature = "serde")]
476impl<'de> serde::Deserialize<'de> for AlignContent {
477 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
478 struct AlignContentVisitor;
479 impl<'de> serde::de::Visitor<'de> for AlignContentVisitor {
480 type Value = AlignContent;
481 fn expecting(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
482 fmt.write_str("an AlignContent variant tag string")
483 }
484 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
485 Ok(match v {
486 "Start" => AlignContent::Start,
487 "End" => AlignContent::End,
488 "FlexStart" => AlignContent::FlexStart,
489 "FlexEnd" => AlignContent::FlexEnd,
490 "Center" => AlignContent::Center,
491 "Stretch" => AlignContent::Stretch,
492 "SpaceBetween" => AlignContent::SpaceBetween,
493 "SpaceEvenly" => AlignContent::SpaceEvenly,
494 "SpaceAround" => AlignContent::SpaceAround,
495 "SafeStart" => AlignContent::SafeStart,
496 "SafeEnd" => AlignContent::SafeEnd,
497 "SafeFlexStart" => AlignContent::SafeFlexStart,
498 "SafeFlexEnd" => AlignContent::SafeFlexEnd,
499 "SafeCenter" => AlignContent::SafeCenter,
500 other => return Err(E::unknown_variant(other, ALIGN_CONTENT_NAMES)),
501 })
502 }
503 }
504 deserializer.deserialize_str(AlignContentVisitor)
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use core::mem::size_of;
512
513 // Size budget — struct = 1B keyword + 1B safety, no niche packing.
514 // Pre-refactor each was a single-byte enum; spec §V13 caps regression at +2B.
515 #[test]
516 fn align_types_within_size_budget() {
517 assert!(size_of::<AlignItems>() <= 2, "AlignItems grew to {}", size_of::<AlignItems>());
518 assert!(size_of::<AlignContent>() <= 2, "AlignContent grew to {}", size_of::<AlignContent>());
519 assert!(size_of::<Option<AlignItems>>() <= 3);
520 assert!(size_of::<Option<AlignContent>>() <= 3);
521 }
522
523 #[test]
524 fn align_items_is_safe() {
525 assert!(AlignItems::SafeStart.is_safe());
526 assert!(AlignItems::SafeEnd.is_safe());
527 assert!(AlignItems::SafeFlexStart.is_safe());
528 assert!(AlignItems::SafeFlexEnd.is_safe());
529 assert!(AlignItems::SafeCenter.is_safe());
530 assert!(!AlignItems::Start.is_safe());
531 assert!(!AlignItems::End.is_safe());
532 assert!(!AlignItems::FlexStart.is_safe());
533 assert!(!AlignItems::FlexEnd.is_safe());
534 assert!(!AlignItems::Center.is_safe());
535 assert!(!AlignItems::Baseline.is_safe());
536 assert!(!AlignItems::Stretch.is_safe());
537 }
538
539 #[test]
540 fn align_items_keyword_strips_safe() {
541 assert_eq!(AlignItems::SafeStart.keyword(), AlignItemsKeyword::Start);
542 assert_eq!(AlignItems::SafeEnd.keyword(), AlignItemsKeyword::End);
543 assert_eq!(AlignItems::SafeFlexStart.keyword(), AlignItemsKeyword::FlexStart);
544 assert_eq!(AlignItems::SafeFlexEnd.keyword(), AlignItemsKeyword::FlexEnd);
545 assert_eq!(AlignItems::SafeCenter.keyword(), AlignItemsKeyword::Center);
546 }
547
548 #[test]
549 fn align_items_keyword_passthrough() {
550 assert_eq!(AlignItems::Start.keyword(), AlignItemsKeyword::Start);
551 assert_eq!(AlignItems::Stretch.keyword(), AlignItemsKeyword::Stretch);
552 assert_eq!(AlignItems::Baseline.keyword(), AlignItemsKeyword::Baseline);
553 assert_eq!(AlignItems::FlexStart.keyword(), AlignItemsKeyword::FlexStart);
554 }
555
556 #[test]
557 fn align_content_is_safe() {
558 assert!(AlignContent::SafeStart.is_safe());
559 assert!(AlignContent::SafeCenter.is_safe());
560 assert!(!AlignContent::SpaceBetween.is_safe());
561 assert!(!AlignContent::Stretch.is_safe());
562 }
563
564 #[test]
565 fn align_content_keyword_strips_safe() {
566 assert_eq!(AlignContent::SafeStart.keyword(), AlignContentKeyword::Start);
567 assert_eq!(AlignContent::SafeFlexEnd.keyword(), AlignContentKeyword::FlexEnd);
568 assert_eq!(AlignContent::SafeCenter.keyword(), AlignContentKeyword::Center);
569 assert_eq!(AlignContent::SpaceBetween.keyword(), AlignContentKeyword::SpaceBetween);
570 }
571
572 #[test]
573 fn align_content_keyword_reversed_swaps_start_end() {
574 assert_eq!(AlignContentKeyword::Start.reversed(), AlignContentKeyword::End);
575 assert_eq!(AlignContentKeyword::End.reversed(), AlignContentKeyword::Start);
576 assert_eq!(AlignContentKeyword::FlexStart.reversed(), AlignContentKeyword::FlexEnd);
577 assert_eq!(AlignContentKeyword::FlexEnd.reversed(), AlignContentKeyword::FlexStart);
578 // Stretch reverses to End — preserves pre-refactor behaviour.
579 assert_eq!(AlignContentKeyword::Stretch.reversed(), AlignContentKeyword::End);
580 assert_eq!(AlignContentKeyword::Center.reversed(), AlignContentKeyword::Center);
581 assert_eq!(AlignContentKeyword::SpaceBetween.reversed(), AlignContentKeyword::SpaceBetween);
582 assert_eq!(AlignContentKeyword::SpaceEvenly.reversed(), AlignContentKeyword::SpaceEvenly);
583 assert_eq!(AlignContentKeyword::SpaceAround.reversed(), AlignContentKeyword::SpaceAround);
584 }
585
586 #[cfg(feature = "parse")]
587 #[test]
588 fn parse_align_items_plain() {
589 assert_eq!("start".parse::<AlignItems>().unwrap(), AlignItems::Start);
590 assert_eq!("end".parse::<AlignItems>().unwrap(), AlignItems::End);
591 assert_eq!("flex-start".parse::<AlignItems>().unwrap(), AlignItems::FlexStart);
592 assert_eq!("flex-end".parse::<AlignItems>().unwrap(), AlignItems::FlexEnd);
593 assert_eq!("center".parse::<AlignItems>().unwrap(), AlignItems::Center);
594 assert_eq!("baseline".parse::<AlignItems>().unwrap(), AlignItems::Baseline);
595 assert_eq!("stretch".parse::<AlignItems>().unwrap(), AlignItems::Stretch);
596 }
597
598 #[cfg(feature = "parse")]
599 #[test]
600 fn parse_align_items_safe() {
601 assert_eq!("safe start".parse::<AlignItems>().unwrap(), AlignItems::SafeStart);
602 assert_eq!("safe end".parse::<AlignItems>().unwrap(), AlignItems::SafeEnd);
603 assert_eq!("safe flex-start".parse::<AlignItems>().unwrap(), AlignItems::SafeFlexStart);
604 assert_eq!("safe flex-end".parse::<AlignItems>().unwrap(), AlignItems::SafeFlexEnd);
605 assert_eq!("safe center".parse::<AlignItems>().unwrap(), AlignItems::SafeCenter);
606 }
607
608 #[cfg(feature = "parse")]
609 #[test]
610 fn parse_align_items_safe_case_insensitive() {
611 assert_eq!("SAFE Start".parse::<AlignItems>().unwrap(), AlignItems::SafeStart);
612 assert_eq!("Safe FLEX-end".parse::<AlignItems>().unwrap(), AlignItems::SafeFlexEnd);
613 }
614
615 #[cfg(feature = "parse")]
616 #[test]
617 fn parse_align_items_unsafe_drops_modifier() {
618 assert_eq!("unsafe start".parse::<AlignItems>().unwrap(), AlignItems::Start);
619 assert_eq!("unsafe end".parse::<AlignItems>().unwrap(), AlignItems::End);
620 assert_eq!("unsafe center".parse::<AlignItems>().unwrap(), AlignItems::Center);
621 }
622
623 #[cfg(feature = "parse")]
624 #[test]
625 fn parse_align_items_rejects_invalid_safe_combos() {
626 assert!("safe stretch".parse::<AlignItems>().is_err());
627 assert!("safe baseline".parse::<AlignItems>().is_err());
628 assert!("safe space-between".parse::<AlignItems>().is_err());
629 assert!("safe".parse::<AlignItems>().is_err());
630 assert!("safe garbage".parse::<AlignItems>().is_err());
631 assert!("unsafe stretch".parse::<AlignItems>().is_err());
632 assert!("unsafe baseline".parse::<AlignItems>().is_err());
633 }
634
635 #[cfg(feature = "parse")]
636 #[test]
637 fn parse_align_content_plain() {
638 assert_eq!("start".parse::<AlignContent>().unwrap(), AlignContent::Start);
639 assert_eq!("space-between".parse::<AlignContent>().unwrap(), AlignContent::SpaceBetween);
640 assert_eq!("space-evenly".parse::<AlignContent>().unwrap(), AlignContent::SpaceEvenly);
641 assert_eq!("space-around".parse::<AlignContent>().unwrap(), AlignContent::SpaceAround);
642 assert_eq!("stretch".parse::<AlignContent>().unwrap(), AlignContent::Stretch);
643 }
644
645 #[cfg(feature = "parse")]
646 #[test]
647 fn parse_align_content_safe() {
648 assert_eq!("safe start".parse::<AlignContent>().unwrap(), AlignContent::SafeStart);
649 assert_eq!("safe end".parse::<AlignContent>().unwrap(), AlignContent::SafeEnd);
650 assert_eq!("safe flex-start".parse::<AlignContent>().unwrap(), AlignContent::SafeFlexStart);
651 assert_eq!("safe flex-end".parse::<AlignContent>().unwrap(), AlignContent::SafeFlexEnd);
652 assert_eq!("safe center".parse::<AlignContent>().unwrap(), AlignContent::SafeCenter);
653 }
654
655 #[cfg(feature = "parse")]
656 #[test]
657 fn parse_align_content_unsafe_drops_modifier() {
658 assert_eq!("unsafe start".parse::<AlignContent>().unwrap(), AlignContent::Start);
659 assert_eq!("unsafe flex-end".parse::<AlignContent>().unwrap(), AlignContent::FlexEnd);
660 }
661
662 #[cfg(feature = "parse")]
663 #[test]
664 fn parse_align_content_rejects_invalid_safe_combos() {
665 assert!("safe stretch".parse::<AlignContent>().is_err());
666 assert!("safe space-between".parse::<AlignContent>().is_err());
667 assert!("safe space-evenly".parse::<AlignContent>().is_err());
668 assert!("safe space-around".parse::<AlignContent>().is_err());
669 assert!("safe".parse::<AlignContent>().is_err());
670 assert!("unsafe stretch".parse::<AlignContent>().is_err());
671 assert!("unsafe space-between".parse::<AlignContent>().is_err());
672 }
673
674 #[cfg(feature = "serde")]
675 #[test]
676 fn serde_align_items_round_trip() {
677 let cases = [
678 (AlignItems::Start, "\"Start\""),
679 (AlignItems::End, "\"End\""),
680 (AlignItems::FlexStart, "\"FlexStart\""),
681 (AlignItems::FlexEnd, "\"FlexEnd\""),
682 (AlignItems::Center, "\"Center\""),
683 (AlignItems::Baseline, "\"Baseline\""),
684 (AlignItems::Stretch, "\"Stretch\""),
685 (AlignItems::SafeStart, "\"SafeStart\""),
686 (AlignItems::SafeEnd, "\"SafeEnd\""),
687 (AlignItems::SafeFlexStart, "\"SafeFlexStart\""),
688 (AlignItems::SafeFlexEnd, "\"SafeFlexEnd\""),
689 (AlignItems::SafeCenter, "\"SafeCenter\""),
690 ];
691 for (value, expected) in cases {
692 let serialized = serde_json::to_string(&value).unwrap();
693 assert_eq!(serialized, expected, "serialize {:?}", value);
694 let deserialized: AlignItems = serde_json::from_str(expected).unwrap();
695 assert_eq!(deserialized, value, "round-trip {:?}", value);
696 }
697 assert!(serde_json::from_str::<AlignItems>("\"NotAVariant\"").is_err());
698 }
699
700 #[cfg(feature = "serde")]
701 #[test]
702 fn serde_align_content_round_trip() {
703 let cases = [
704 (AlignContent::Start, "\"Start\""),
705 (AlignContent::End, "\"End\""),
706 (AlignContent::FlexStart, "\"FlexStart\""),
707 (AlignContent::FlexEnd, "\"FlexEnd\""),
708 (AlignContent::Center, "\"Center\""),
709 (AlignContent::Stretch, "\"Stretch\""),
710 (AlignContent::SpaceBetween, "\"SpaceBetween\""),
711 (AlignContent::SpaceEvenly, "\"SpaceEvenly\""),
712 (AlignContent::SpaceAround, "\"SpaceAround\""),
713 (AlignContent::SafeStart, "\"SafeStart\""),
714 (AlignContent::SafeEnd, "\"SafeEnd\""),
715 (AlignContent::SafeFlexStart, "\"SafeFlexStart\""),
716 (AlignContent::SafeFlexEnd, "\"SafeFlexEnd\""),
717 (AlignContent::SafeCenter, "\"SafeCenter\""),
718 ];
719 for (value, expected) in cases {
720 let serialized = serde_json::to_string(&value).unwrap();
721 assert_eq!(serialized, expected, "serialize {:?}", value);
722 let deserialized: AlignContent = serde_json::from_str(expected).unwrap();
723 assert_eq!(deserialized, value, "round-trip {:?}", value);
724 }
725 assert!(serde_json::from_str::<AlignContent>("\"NotAVariant\"").is_err());
726 }
727}