Skip to main content

taffy/style/
alignment.rs

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
133impl AlignItems {
134    /// Items are packed toward the start of the axis.
135    pub const START: Self = Self { keyword: AlignItemsKeyword::Start, safety: AlignmentSafety::Unsafe };
136    /// Items are packed toward the end of the axis.
137    pub const END: Self = Self { keyword: AlignItemsKeyword::End, safety: AlignmentSafety::Unsafe };
138    /// Items are packed towards the flex-relative start of the axis.
139    pub const FLEX_START: Self = Self { keyword: AlignItemsKeyword::FlexStart, safety: AlignmentSafety::Unsafe };
140    /// Items are packed towards the flex-relative end of the axis.
141    pub const FLEX_END: Self = Self { keyword: AlignItemsKeyword::FlexEnd, safety: AlignmentSafety::Unsafe };
142    /// Items are packed along the center of the cross axis.
143    pub const CENTER: Self = Self { keyword: AlignItemsKeyword::Center, safety: AlignmentSafety::Unsafe };
144    /// Items are aligned such as their baselines align.
145    pub const BASELINE: Self = Self { keyword: AlignItemsKeyword::Baseline, safety: AlignmentSafety::Unsafe };
146    /// Stretch to fill the container.
147    pub const STRETCH: Self = Self { keyword: AlignItemsKeyword::Stretch, safety: AlignmentSafety::Unsafe };
148    /// Like [`AlignItems::START`], but falls back to [`AlignItems::START`] when the
149    /// alignment subject overflows the alignment container, to avoid data loss.
150    pub const SAFE_START: Self = Self { keyword: AlignItemsKeyword::Start, safety: AlignmentSafety::Safe };
151    /// Like [`AlignItems::END`], but falls back to [`AlignItems::START`] when the
152    /// alignment subject overflows the alignment container, to avoid data loss.
153    pub const SAFE_END: Self = Self { keyword: AlignItemsKeyword::End, safety: AlignmentSafety::Safe };
154    /// Like [`AlignItems::FLEX_START`], but falls back to [`AlignItems::START`] when the
155    /// alignment subject overflows the alignment container, to avoid data loss.
156    pub const SAFE_FLEX_START: Self = Self { keyword: AlignItemsKeyword::FlexStart, safety: AlignmentSafety::Safe };
157    /// Like [`AlignItems::FLEX_END`], but falls back to [`AlignItems::START`] when the
158    /// alignment subject overflows the alignment container, to avoid data loss.
159    pub const SAFE_FLEX_END: Self = Self { keyword: AlignItemsKeyword::FlexEnd, safety: AlignmentSafety::Safe };
160    /// Like [`AlignItems::CENTER`], but falls back to [`AlignItems::START`] when the
161    /// alignment subject overflows the alignment container, to avoid data loss.
162    pub const SAFE_CENTER: Self = Self { keyword: AlignItemsKeyword::Center, safety: AlignmentSafety::Safe };
163
164    /// Returns `true` iff this carries the `safe` overflow-position modifier.
165    #[inline]
166    pub const fn is_safe(self) -> bool {
167        matches!(self.safety, AlignmentSafety::Safe)
168    }
169
170    /// Returns the underlying position keyword, discarding the safety modifier.
171    #[inline]
172    pub const fn keyword(self) -> AlignItemsKeyword {
173        self.keyword
174    }
175}
176
177#[cfg(feature = "parse")]
178impl FromCss for AlignItems {
179    fn from_css<'i>(input: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
180        let first = input.expect_ident()?.clone();
181        cssparser::match_ignore_ascii_case! { &*first,
182            "safe" => {
183                let pos = input.expect_ident()?.clone();
184                cssparser::match_ignore_ascii_case! { &*pos,
185                    "start" => Ok(Self::SAFE_START),
186                    "end" => Ok(Self::SAFE_END),
187                    "flex-start" => Ok(Self::SAFE_FLEX_START),
188                    "flex-end" => Ok(Self::SAFE_FLEX_END),
189                    "center" => Ok(Self::SAFE_CENTER),
190                    _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
191                }
192            },
193            "unsafe" => {
194                let pos = input.expect_ident()?.clone();
195                cssparser::match_ignore_ascii_case! { &*pos,
196                    "start" => Ok(Self::START),
197                    "end" => Ok(Self::END),
198                    "flex-start" => Ok(Self::FLEX_START),
199                    "flex-end" => Ok(Self::FLEX_END),
200                    "center" => Ok(Self::CENTER),
201                    _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
202                }
203            },
204            "start" => Ok(Self::START),
205            "end" => Ok(Self::END),
206            "flex-start" => Ok(Self::FLEX_START),
207            "flex-end" => Ok(Self::FLEX_END),
208            "center" => Ok(Self::CENTER),
209            "baseline" => Ok(Self::BASELINE),
210            "stretch" => Ok(Self::STRETCH),
211            _ => Err(input.new_unexpected_token_error(Token::Ident(first))),
212        }
213    }
214}
215
216#[cfg(feature = "parse")]
217crate::util::parse::from_str_from_css!(AlignItems);
218
219/// Used to control how child nodes are aligned.
220/// Does not apply to Flexbox, and will be ignored if specified on a flex container.
221/// For Grid it controls alignment in the inline axis.
222///
223/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
224pub type JustifyItems = AlignItems;
225/// Controls alignment of an individual node.
226///
227/// Overrides the parent Node's `AlignItems` property.
228/// For Flexbox it controls alignment in the cross axis.
229/// For Grid it controls alignment in the block axis.
230///
231/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
232pub type AlignSelf = AlignItems;
233/// Controls alignment of an individual node.
234///
235/// Overrides the parent Node's `JustifyItems` property.
236/// Does not apply to Flexbox, and will be ignored if specified on a flex child.
237/// For Grid it controls alignment in the inline axis.
238///
239/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
240pub type JustifySelf = AlignItems;
241
242/// Sets the distribution of space between and around content items.
243/// For Flexbox it controls alignment in the cross axis.
244/// For Grid it controls alignment in the block axis.
245///
246/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
247#[derive(Copy, Clone, PartialEq, Eq, Debug)]
248pub struct AlignContent {
249    /// Position keyword.
250    pub keyword: AlignContentKeyword,
251    /// Overflow-position modifier (`safe` / `unsafe`).
252    pub safety: AlignmentSafety,
253}
254
255impl AlignContent {
256    /// Items are packed toward the start of the axis.
257    pub const START: Self = Self { keyword: AlignContentKeyword::Start, safety: AlignmentSafety::Unsafe };
258    /// Items are packed toward the end of the axis.
259    pub const END: Self = Self { keyword: AlignContentKeyword::End, safety: AlignmentSafety::Unsafe };
260    /// Items are packed towards the flex-relative start of the axis.
261    pub const FLEX_START: Self = Self { keyword: AlignContentKeyword::FlexStart, safety: AlignmentSafety::Unsafe };
262    /// Items are packed towards the flex-relative end of the axis.
263    pub const FLEX_END: Self = Self { keyword: AlignContentKeyword::FlexEnd, safety: AlignmentSafety::Unsafe };
264    /// Items are centered around the middle of the axis.
265    pub const CENTER: Self = Self { keyword: AlignContentKeyword::Center, safety: AlignmentSafety::Unsafe };
266    /// Items are stretched to fill the container.
267    pub const STRETCH: Self = Self { keyword: AlignContentKeyword::Stretch, safety: AlignmentSafety::Unsafe };
268    /// The first and last items are aligned flush with the edges of the container.
269    pub const SPACE_BETWEEN: Self =
270        Self { keyword: AlignContentKeyword::SpaceBetween, safety: AlignmentSafety::Unsafe };
271    /// The gap between the first and last items equals the gap between items.
272    pub const SPACE_EVENLY: Self = Self { keyword: AlignContentKeyword::SpaceEvenly, safety: AlignmentSafety::Unsafe };
273    /// The gap between the first and last items is half the gap between items.
274    pub const SPACE_AROUND: Self = Self { keyword: AlignContentKeyword::SpaceAround, safety: AlignmentSafety::Unsafe };
275    /// Like [`AlignContent::START`], but falls back to [`AlignContent::START`] when the
276    /// content overflows the alignment container, to avoid data loss.
277    pub const SAFE_START: Self = Self { keyword: AlignContentKeyword::Start, safety: AlignmentSafety::Safe };
278    /// Like [`AlignContent::END`], but falls back to [`AlignContent::START`] when the
279    /// content overflows the alignment container, to avoid data loss.
280    pub const SAFE_END: Self = Self { keyword: AlignContentKeyword::End, safety: AlignmentSafety::Safe };
281    /// Like [`AlignContent::FLEX_START`], but falls back to [`AlignContent::START`] when
282    /// the content overflows the alignment container, to avoid data loss.
283    pub const SAFE_FLEX_START: Self = Self { keyword: AlignContentKeyword::FlexStart, safety: AlignmentSafety::Safe };
284    /// Like [`AlignContent::FLEX_END`], but falls back to [`AlignContent::START`] when the
285    /// content overflows the alignment container, to avoid data loss.
286    pub const SAFE_FLEX_END: Self = Self { keyword: AlignContentKeyword::FlexEnd, safety: AlignmentSafety::Safe };
287    /// Like [`AlignContent::CENTER`], but falls back to [`AlignContent::START`] when the
288    /// content overflows the alignment container, to avoid data loss.
289    pub const SAFE_CENTER: Self = Self { keyword: AlignContentKeyword::Center, safety: AlignmentSafety::Safe };
290
291    /// Returns `true` iff this carries the `safe` overflow-position modifier.
292    #[inline]
293    pub const fn is_safe(self) -> bool {
294        matches!(self.safety, AlignmentSafety::Safe)
295    }
296
297    /// Returns the underlying position keyword, discarding the safety modifier.
298    #[inline]
299    pub const fn keyword(self) -> AlignContentKeyword {
300        self.keyword
301    }
302}
303
304#[cfg(feature = "parse")]
305impl FromCss for AlignContent {
306    fn from_css<'i>(input: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
307        let first = input.expect_ident()?.clone();
308        cssparser::match_ignore_ascii_case! { &*first,
309            "safe" => {
310                let pos = input.expect_ident()?.clone();
311                cssparser::match_ignore_ascii_case! { &*pos,
312                    "start" => Ok(Self::SAFE_START),
313                    "end" => Ok(Self::SAFE_END),
314                    "flex-start" => Ok(Self::SAFE_FLEX_START),
315                    "flex-end" => Ok(Self::SAFE_FLEX_END),
316                    "center" => Ok(Self::SAFE_CENTER),
317                    _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
318                }
319            },
320            "unsafe" => {
321                let pos = input.expect_ident()?.clone();
322                cssparser::match_ignore_ascii_case! { &*pos,
323                    "start" => Ok(Self::START),
324                    "end" => Ok(Self::END),
325                    "flex-start" => Ok(Self::FLEX_START),
326                    "flex-end" => Ok(Self::FLEX_END),
327                    "center" => Ok(Self::CENTER),
328                    _ => Err(input.new_unexpected_token_error(Token::Ident(pos))),
329                }
330            },
331            "start" => Ok(Self::START),
332            "end" => Ok(Self::END),
333            "flex-start" => Ok(Self::FLEX_START),
334            "flex-end" => Ok(Self::FLEX_END),
335            "center" => Ok(Self::CENTER),
336            "stretch" => Ok(Self::STRETCH),
337            "space-between" => Ok(Self::SPACE_BETWEEN),
338            "space-evenly" => Ok(Self::SPACE_EVENLY),
339            "space-around" => Ok(Self::SPACE_AROUND),
340            _ => Err(input.new_unexpected_token_error(Token::Ident(first))),
341        }
342    }
343}
344
345#[cfg(feature = "parse")]
346crate::util::parse::from_str_from_css!(AlignContent);
347
348/// Sets the distribution of space between and around content items.
349/// For Flexbox it controls alignment in the main axis.
350/// For Grid it controls alignment in the inline axis.
351///
352/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
353pub type JustifyContent = AlignContent;
354
355// ---------------------------------------------------------------------------
356// Serde — custom impls preserve the pre-struct wire format (single tag string
357// per public spelling) so consumers reading data serialized before the refactor
358// continue to deserialize correctly.
359// ---------------------------------------------------------------------------
360
361/// Canonical tag-string set accepted by [`AlignItems`] serde deserialization, used in
362/// `unknown_variant` errors. Mirrors the spellings produced by `Serialize`.
363#[cfg(feature = "serde")]
364const ALIGN_ITEMS_NAMES: &[&str] = &[
365    "Start",
366    "End",
367    "FlexStart",
368    "FlexEnd",
369    "Center",
370    "Baseline",
371    "Stretch",
372    "SafeStart",
373    "SafeEnd",
374    "SafeFlexStart",
375    "SafeFlexEnd",
376    "SafeCenter",
377];
378
379#[cfg(feature = "serde")]
380impl serde::Serialize for AlignItems {
381    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
382        let name = match (self.keyword, self.safety) {
383            (AlignItemsKeyword::Start, AlignmentSafety::Unsafe) => "Start",
384            (AlignItemsKeyword::End, AlignmentSafety::Unsafe) => "End",
385            (AlignItemsKeyword::FlexStart, AlignmentSafety::Unsafe) => "FlexStart",
386            (AlignItemsKeyword::FlexEnd, AlignmentSafety::Unsafe) => "FlexEnd",
387            (AlignItemsKeyword::Center, AlignmentSafety::Unsafe) => "Center",
388            (AlignItemsKeyword::Baseline, _) => "Baseline",
389            (AlignItemsKeyword::Stretch, _) => "Stretch",
390            (AlignItemsKeyword::Start, AlignmentSafety::Safe) => "SafeStart",
391            (AlignItemsKeyword::End, AlignmentSafety::Safe) => "SafeEnd",
392            (AlignItemsKeyword::FlexStart, AlignmentSafety::Safe) => "SafeFlexStart",
393            (AlignItemsKeyword::FlexEnd, AlignmentSafety::Safe) => "SafeFlexEnd",
394            (AlignItemsKeyword::Center, AlignmentSafety::Safe) => "SafeCenter",
395        };
396        serializer.serialize_str(name)
397    }
398}
399
400#[cfg(feature = "serde")]
401impl<'de> serde::Deserialize<'de> for AlignItems {
402    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
403        struct AlignItemsVisitor;
404        impl<'de> serde::de::Visitor<'de> for AlignItemsVisitor {
405            type Value = AlignItems;
406            fn expecting(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
407                fmt.write_str("an AlignItems variant tag string")
408            }
409            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
410                Ok(match v {
411                    "Start" => AlignItems::START,
412                    "End" => AlignItems::END,
413                    "FlexStart" => AlignItems::FLEX_START,
414                    "FlexEnd" => AlignItems::FLEX_END,
415                    "Center" => AlignItems::CENTER,
416                    "Baseline" => AlignItems::BASELINE,
417                    "Stretch" => AlignItems::STRETCH,
418                    "SafeStart" => AlignItems::SAFE_START,
419                    "SafeEnd" => AlignItems::SAFE_END,
420                    "SafeFlexStart" => AlignItems::SAFE_FLEX_START,
421                    "SafeFlexEnd" => AlignItems::SAFE_FLEX_END,
422                    "SafeCenter" => AlignItems::SAFE_CENTER,
423                    other => return Err(E::unknown_variant(other, ALIGN_ITEMS_NAMES)),
424                })
425            }
426        }
427        deserializer.deserialize_str(AlignItemsVisitor)
428    }
429}
430
431/// Canonical tag-string set accepted by [`AlignContent`] serde deserialization, used in
432/// `unknown_variant` errors. Mirrors the spellings produced by `Serialize`.
433#[cfg(feature = "serde")]
434const ALIGN_CONTENT_NAMES: &[&str] = &[
435    "Start",
436    "End",
437    "FlexStart",
438    "FlexEnd",
439    "Center",
440    "Stretch",
441    "SpaceBetween",
442    "SpaceEvenly",
443    "SpaceAround",
444    "SafeStart",
445    "SafeEnd",
446    "SafeFlexStart",
447    "SafeFlexEnd",
448    "SafeCenter",
449];
450
451#[cfg(feature = "serde")]
452impl serde::Serialize for AlignContent {
453    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
454        let name = match (self.keyword, self.safety) {
455            (AlignContentKeyword::Start, AlignmentSafety::Unsafe) => "Start",
456            (AlignContentKeyword::End, AlignmentSafety::Unsafe) => "End",
457            (AlignContentKeyword::FlexStart, AlignmentSafety::Unsafe) => "FlexStart",
458            (AlignContentKeyword::FlexEnd, AlignmentSafety::Unsafe) => "FlexEnd",
459            (AlignContentKeyword::Center, AlignmentSafety::Unsafe) => "Center",
460            (AlignContentKeyword::Stretch, _) => "Stretch",
461            (AlignContentKeyword::SpaceBetween, _) => "SpaceBetween",
462            (AlignContentKeyword::SpaceEvenly, _) => "SpaceEvenly",
463            (AlignContentKeyword::SpaceAround, _) => "SpaceAround",
464            (AlignContentKeyword::Start, AlignmentSafety::Safe) => "SafeStart",
465            (AlignContentKeyword::End, AlignmentSafety::Safe) => "SafeEnd",
466            (AlignContentKeyword::FlexStart, AlignmentSafety::Safe) => "SafeFlexStart",
467            (AlignContentKeyword::FlexEnd, AlignmentSafety::Safe) => "SafeFlexEnd",
468            (AlignContentKeyword::Center, AlignmentSafety::Safe) => "SafeCenter",
469        };
470        serializer.serialize_str(name)
471    }
472}
473
474#[cfg(feature = "serde")]
475impl<'de> serde::Deserialize<'de> for AlignContent {
476    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
477        struct AlignContentVisitor;
478        impl<'de> serde::de::Visitor<'de> for AlignContentVisitor {
479            type Value = AlignContent;
480            fn expecting(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
481                fmt.write_str("an AlignContent variant tag string")
482            }
483            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
484                Ok(match v {
485                    "Start" => AlignContent::START,
486                    "End" => AlignContent::END,
487                    "FlexStart" => AlignContent::FLEX_START,
488                    "FlexEnd" => AlignContent::FLEX_END,
489                    "Center" => AlignContent::CENTER,
490                    "Stretch" => AlignContent::STRETCH,
491                    "SpaceBetween" => AlignContent::SPACE_BETWEEN,
492                    "SpaceEvenly" => AlignContent::SPACE_EVENLY,
493                    "SpaceAround" => AlignContent::SPACE_AROUND,
494                    "SafeStart" => AlignContent::SAFE_START,
495                    "SafeEnd" => AlignContent::SAFE_END,
496                    "SafeFlexStart" => AlignContent::SAFE_FLEX_START,
497                    "SafeFlexEnd" => AlignContent::SAFE_FLEX_END,
498                    "SafeCenter" => AlignContent::SAFE_CENTER,
499                    other => return Err(E::unknown_variant(other, ALIGN_CONTENT_NAMES)),
500                })
501            }
502        }
503        deserializer.deserialize_str(AlignContentVisitor)
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use core::mem::size_of;
511
512    // Size budget — struct = 1B keyword + 1B safety, no niche packing.
513    // Pre-refactor each was a single-byte enum; spec §V13 caps regression at +2B.
514    #[test]
515    fn align_types_within_size_budget() {
516        assert!(size_of::<AlignItems>() <= 2, "AlignItems grew to {}", size_of::<AlignItems>());
517        assert!(size_of::<AlignContent>() <= 2, "AlignContent grew to {}", size_of::<AlignContent>());
518        assert!(size_of::<Option<AlignItems>>() <= 3);
519        assert!(size_of::<Option<AlignContent>>() <= 3);
520    }
521
522    #[test]
523    fn align_items_is_safe() {
524        assert!(AlignItems::SAFE_START.is_safe());
525        assert!(AlignItems::SAFE_END.is_safe());
526        assert!(AlignItems::SAFE_FLEX_START.is_safe());
527        assert!(AlignItems::SAFE_FLEX_END.is_safe());
528        assert!(AlignItems::SAFE_CENTER.is_safe());
529        assert!(!AlignItems::START.is_safe());
530        assert!(!AlignItems::END.is_safe());
531        assert!(!AlignItems::FLEX_START.is_safe());
532        assert!(!AlignItems::FLEX_END.is_safe());
533        assert!(!AlignItems::CENTER.is_safe());
534        assert!(!AlignItems::BASELINE.is_safe());
535        assert!(!AlignItems::STRETCH.is_safe());
536    }
537
538    #[test]
539    fn align_items_keyword_strips_safe() {
540        assert_eq!(AlignItems::SAFE_START.keyword(), AlignItemsKeyword::Start);
541        assert_eq!(AlignItems::SAFE_END.keyword(), AlignItemsKeyword::End);
542        assert_eq!(AlignItems::SAFE_FLEX_START.keyword(), AlignItemsKeyword::FlexStart);
543        assert_eq!(AlignItems::SAFE_FLEX_END.keyword(), AlignItemsKeyword::FlexEnd);
544        assert_eq!(AlignItems::SAFE_CENTER.keyword(), AlignItemsKeyword::Center);
545    }
546
547    #[test]
548    fn align_items_keyword_passthrough() {
549        assert_eq!(AlignItems::START.keyword(), AlignItemsKeyword::Start);
550        assert_eq!(AlignItems::STRETCH.keyword(), AlignItemsKeyword::Stretch);
551        assert_eq!(AlignItems::BASELINE.keyword(), AlignItemsKeyword::Baseline);
552        assert_eq!(AlignItems::FLEX_START.keyword(), AlignItemsKeyword::FlexStart);
553    }
554
555    #[test]
556    fn align_content_is_safe() {
557        assert!(AlignContent::SAFE_START.is_safe());
558        assert!(AlignContent::SAFE_CENTER.is_safe());
559        assert!(!AlignContent::SPACE_BETWEEN.is_safe());
560        assert!(!AlignContent::STRETCH.is_safe());
561    }
562
563    #[test]
564    fn align_content_keyword_strips_safe() {
565        assert_eq!(AlignContent::SAFE_START.keyword(), AlignContentKeyword::Start);
566        assert_eq!(AlignContent::SAFE_FLEX_END.keyword(), AlignContentKeyword::FlexEnd);
567        assert_eq!(AlignContent::SAFE_CENTER.keyword(), AlignContentKeyword::Center);
568        assert_eq!(AlignContent::SPACE_BETWEEN.keyword(), AlignContentKeyword::SpaceBetween);
569    }
570
571    #[test]
572    fn align_content_keyword_reversed_swaps_start_end() {
573        assert_eq!(AlignContentKeyword::Start.reversed(), AlignContentKeyword::End);
574        assert_eq!(AlignContentKeyword::End.reversed(), AlignContentKeyword::Start);
575        assert_eq!(AlignContentKeyword::FlexStart.reversed(), AlignContentKeyword::FlexEnd);
576        assert_eq!(AlignContentKeyword::FlexEnd.reversed(), AlignContentKeyword::FlexStart);
577        // Stretch reverses to End — preserves pre-refactor behaviour.
578        assert_eq!(AlignContentKeyword::Stretch.reversed(), AlignContentKeyword::End);
579        assert_eq!(AlignContentKeyword::Center.reversed(), AlignContentKeyword::Center);
580        assert_eq!(AlignContentKeyword::SpaceBetween.reversed(), AlignContentKeyword::SpaceBetween);
581        assert_eq!(AlignContentKeyword::SpaceEvenly.reversed(), AlignContentKeyword::SpaceEvenly);
582        assert_eq!(AlignContentKeyword::SpaceAround.reversed(), AlignContentKeyword::SpaceAround);
583    }
584
585    #[cfg(feature = "parse")]
586    #[test]
587    fn parse_align_items_plain() {
588        assert_eq!("start".parse::<AlignItems>().unwrap(), AlignItems::START);
589        assert_eq!("end".parse::<AlignItems>().unwrap(), AlignItems::END);
590        assert_eq!("flex-start".parse::<AlignItems>().unwrap(), AlignItems::FLEX_START);
591        assert_eq!("flex-end".parse::<AlignItems>().unwrap(), AlignItems::FLEX_END);
592        assert_eq!("center".parse::<AlignItems>().unwrap(), AlignItems::CENTER);
593        assert_eq!("baseline".parse::<AlignItems>().unwrap(), AlignItems::BASELINE);
594        assert_eq!("stretch".parse::<AlignItems>().unwrap(), AlignItems::STRETCH);
595    }
596
597    #[cfg(feature = "parse")]
598    #[test]
599    fn parse_align_items_safe() {
600        assert_eq!("safe start".parse::<AlignItems>().unwrap(), AlignItems::SAFE_START);
601        assert_eq!("safe end".parse::<AlignItems>().unwrap(), AlignItems::SAFE_END);
602        assert_eq!("safe flex-start".parse::<AlignItems>().unwrap(), AlignItems::SAFE_FLEX_START);
603        assert_eq!("safe flex-end".parse::<AlignItems>().unwrap(), AlignItems::SAFE_FLEX_END);
604        assert_eq!("safe center".parse::<AlignItems>().unwrap(), AlignItems::SAFE_CENTER);
605    }
606
607    #[cfg(feature = "parse")]
608    #[test]
609    fn parse_align_items_safe_case_insensitive() {
610        assert_eq!("SAFE Start".parse::<AlignItems>().unwrap(), AlignItems::SAFE_START);
611        assert_eq!("Safe FLEX-end".parse::<AlignItems>().unwrap(), AlignItems::SAFE_FLEX_END);
612    }
613
614    #[cfg(feature = "parse")]
615    #[test]
616    fn parse_align_items_unsafe_drops_modifier() {
617        assert_eq!("unsafe start".parse::<AlignItems>().unwrap(), AlignItems::START);
618        assert_eq!("unsafe end".parse::<AlignItems>().unwrap(), AlignItems::END);
619        assert_eq!("unsafe center".parse::<AlignItems>().unwrap(), AlignItems::CENTER);
620    }
621
622    #[cfg(feature = "parse")]
623    #[test]
624    fn parse_align_items_rejects_invalid_safe_combos() {
625        assert!("safe stretch".parse::<AlignItems>().is_err());
626        assert!("safe baseline".parse::<AlignItems>().is_err());
627        assert!("safe space-between".parse::<AlignItems>().is_err());
628        assert!("safe".parse::<AlignItems>().is_err());
629        assert!("safe garbage".parse::<AlignItems>().is_err());
630        assert!("unsafe stretch".parse::<AlignItems>().is_err());
631        assert!("unsafe baseline".parse::<AlignItems>().is_err());
632    }
633
634    #[cfg(feature = "parse")]
635    #[test]
636    fn parse_align_content_plain() {
637        assert_eq!("start".parse::<AlignContent>().unwrap(), AlignContent::START);
638        assert_eq!("space-between".parse::<AlignContent>().unwrap(), AlignContent::SPACE_BETWEEN);
639        assert_eq!("space-evenly".parse::<AlignContent>().unwrap(), AlignContent::SPACE_EVENLY);
640        assert_eq!("space-around".parse::<AlignContent>().unwrap(), AlignContent::SPACE_AROUND);
641        assert_eq!("stretch".parse::<AlignContent>().unwrap(), AlignContent::STRETCH);
642    }
643
644    #[cfg(feature = "parse")]
645    #[test]
646    fn parse_align_content_safe() {
647        assert_eq!("safe start".parse::<AlignContent>().unwrap(), AlignContent::SAFE_START);
648        assert_eq!("safe end".parse::<AlignContent>().unwrap(), AlignContent::SAFE_END);
649        assert_eq!("safe flex-start".parse::<AlignContent>().unwrap(), AlignContent::SAFE_FLEX_START);
650        assert_eq!("safe flex-end".parse::<AlignContent>().unwrap(), AlignContent::SAFE_FLEX_END);
651        assert_eq!("safe center".parse::<AlignContent>().unwrap(), AlignContent::SAFE_CENTER);
652    }
653
654    #[cfg(feature = "parse")]
655    #[test]
656    fn parse_align_content_unsafe_drops_modifier() {
657        assert_eq!("unsafe start".parse::<AlignContent>().unwrap(), AlignContent::START);
658        assert_eq!("unsafe flex-end".parse::<AlignContent>().unwrap(), AlignContent::FLEX_END);
659    }
660
661    #[cfg(feature = "parse")]
662    #[test]
663    fn parse_align_content_rejects_invalid_safe_combos() {
664        assert!("safe stretch".parse::<AlignContent>().is_err());
665        assert!("safe space-between".parse::<AlignContent>().is_err());
666        assert!("safe space-evenly".parse::<AlignContent>().is_err());
667        assert!("safe space-around".parse::<AlignContent>().is_err());
668        assert!("safe".parse::<AlignContent>().is_err());
669        assert!("unsafe stretch".parse::<AlignContent>().is_err());
670        assert!("unsafe space-between".parse::<AlignContent>().is_err());
671    }
672
673    #[cfg(feature = "serde")]
674    #[test]
675    fn serde_align_items_round_trip() {
676        let cases = [
677            (AlignItems::START, "\"Start\""),
678            (AlignItems::END, "\"End\""),
679            (AlignItems::FLEX_START, "\"FlexStart\""),
680            (AlignItems::FLEX_END, "\"FlexEnd\""),
681            (AlignItems::CENTER, "\"Center\""),
682            (AlignItems::BASELINE, "\"Baseline\""),
683            (AlignItems::STRETCH, "\"Stretch\""),
684            (AlignItems::SAFE_START, "\"SafeStart\""),
685            (AlignItems::SAFE_END, "\"SafeEnd\""),
686            (AlignItems::SAFE_FLEX_START, "\"SafeFlexStart\""),
687            (AlignItems::SAFE_FLEX_END, "\"SafeFlexEnd\""),
688            (AlignItems::SAFE_CENTER, "\"SafeCenter\""),
689        ];
690        for (value, expected) in cases {
691            let serialized = serde_json::to_string(&value).unwrap();
692            assert_eq!(serialized, expected, "serialize {:?}", value);
693            let deserialized: AlignItems = serde_json::from_str(expected).unwrap();
694            assert_eq!(deserialized, value, "round-trip {:?}", value);
695        }
696        assert!(serde_json::from_str::<AlignItems>("\"NotAVariant\"").is_err());
697    }
698
699    #[cfg(feature = "serde")]
700    #[test]
701    fn serde_align_content_round_trip() {
702        let cases = [
703            (AlignContent::START, "\"Start\""),
704            (AlignContent::END, "\"End\""),
705            (AlignContent::FLEX_START, "\"FlexStart\""),
706            (AlignContent::FLEX_END, "\"FlexEnd\""),
707            (AlignContent::CENTER, "\"Center\""),
708            (AlignContent::STRETCH, "\"Stretch\""),
709            (AlignContent::SPACE_BETWEEN, "\"SpaceBetween\""),
710            (AlignContent::SPACE_EVENLY, "\"SpaceEvenly\""),
711            (AlignContent::SPACE_AROUND, "\"SpaceAround\""),
712            (AlignContent::SAFE_START, "\"SafeStart\""),
713            (AlignContent::SAFE_END, "\"SafeEnd\""),
714            (AlignContent::SAFE_FLEX_START, "\"SafeFlexStart\""),
715            (AlignContent::SAFE_FLEX_END, "\"SafeFlexEnd\""),
716            (AlignContent::SAFE_CENTER, "\"SafeCenter\""),
717        ];
718        for (value, expected) in cases {
719            let serialized = serde_json::to_string(&value).unwrap();
720            assert_eq!(serialized, expected, "serialize {:?}", value);
721            let deserialized: AlignContent = serde_json::from_str(expected).unwrap();
722            assert_eq!(deserialized, value, "round-trip {:?}", value);
723        }
724        assert!(serde_json::from_str::<AlignContent>("\"NotAVariant\"").is_err());
725    }
726}