tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 5544Rust 88Markdown 11TOML 91926 Definiciones

⎇ 4a56ad2392dd569cc975530b5ae8cf0536f7f3b2 src/style/alignment.rs Rust · 727 líneas · 34.1 KB Historia Quién tocó qué crudo

Implement `safe` and `unsafe` overflow-position alignment keywords (#952) d1ff7e339 · Ritesh ·

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}