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