Skip to main content

alpha2_model/
layout_values.rs

1//! The CSS value forms a layout reads: lengths, the four-side shorthand,
2//! grid track lists and grid lines.
3//!
4//! A JSON number is a length, a fraction of the picture's shorter side. A
5//! string is CSS: `"12px"` is pixels of the project canvas, `"50%"` a
6//! percent of the parent's content box where a percent is allowed. Inside
7//! these strings keywords use CSS spelling (`min-content`, `auto-fill`);
8//! everywhere else a layout's keywords are `snake_case`. Whitespace is any run
9//! of ASCII spaces. A string that does not parse is refused by the parser,
10//! naming the token and, where one is likely, the spelling meant.
11//!
12//! Each form serializes canonically (a number, `"<n>px"`, `"<n>%"`, the
13//! sides object, a track list as CSS), and a serialized value parses to the
14//! same value.
15
16use serde::de::{self, Deserializer, MapAccess, Visitor};
17use serde::ser::{SerializeStruct, Serializer};
18use serde::{Deserialize, Serialize};
19use std::fmt::{self, Write as _};
20
21/// A CSS number without an exponent: `-?(\d+\.?\d*|\.\d+)`, finite. Read as
22/// a JSON number is, through 64 bits, so `"0.1px"` holds the `f32` that
23/// `0.1` does.
24pub(crate) fn parse_number(text: &str) -> Option<f32> {
25    let digits = text.strip_prefix('-').unwrap_or(text);
26    let (whole, fraction) = digits.split_once('.').unwrap_or((digits, ""));
27    let all_digits = |part: &str| part.bytes().all(|byte| byte.is_ascii_digit());
28    if !(all_digits(whole) && all_digits(fraction)) || (whole.is_empty() && fraction.is_empty()) {
29        return None;
30    }
31    #[allow(clippy::cast_possible_truncation)]
32    let value = text.parse::<f64>().ok()? as f32;
33    value.is_finite().then_some(value)
34}
35
36/// The tokens of a CSS value: runs of characters between ASCII spaces.
37fn tokens(text: &str) -> impl Iterator<Item = &str> {
38    text.split(' ').filter(|token| !token.is_empty())
39}
40
41/// A length's text, as it is written back.
42fn write_length(out: &mut impl fmt::Write, value: f32, unit: &str) -> fmt::Result {
43    write!(out, "{value}{unit}")
44}
45
46/// A layout length that may be a percent: a size or an inset side.
47#[derive(Copy, Clone, Debug, PartialEq)]
48pub enum LayoutLength {
49    /// A fraction of the layout picture's shorter side, written as a number.
50    Fraction(f32),
51    /// Pixels of the project canvas, written `"<n>px"`. A plan turns it into
52    /// a fraction of the canvas's shorter side.
53    Px(f32),
54    /// Percent (0 to 100 is 0% to 100%) of the parent's content box along
55    /// the field's axis, written `"<n>%"`.
56    Percent(f32),
57}
58
59impl Default for LayoutLength {
60    fn default() -> Self {
61        Self::Fraction(0.0)
62    }
63}
64
65impl LayoutLength {
66    /// The number, whatever its unit.
67    #[must_use]
68    pub const fn value(self) -> f32 {
69        match self {
70            Self::Fraction(value) | Self::Px(value) | Self::Percent(value) => value,
71        }
72    }
73
74    /// Whether it is a percent.
75    #[must_use]
76    pub const fn is_percent(self) -> bool {
77        matches!(self, Self::Percent(_))
78    }
79
80    /// The same length with pixels turned into a fraction of a picture
81    /// `shorter_side` pixels on its shorter side.
82    #[must_use]
83    pub fn without_px(self, shorter_side: f32) -> Self {
84        match self {
85            Self::Px(value) => Self::Fraction(px_fraction(value, shorter_side)),
86            other => other,
87        }
88    }
89
90    /// The same kind of length with `value`.
91    #[must_use]
92    pub const fn with_value(self, value: f32) -> Self {
93        match self {
94            Self::Fraction(_) => Self::Fraction(value),
95            Self::Px(_) => Self::Px(value),
96            Self::Percent(_) => Self::Percent(value),
97        }
98    }
99
100    fn parse(text: &str) -> Result<Self, String> {
101        let size = |value: LayoutSize| match value {
102            LayoutSize::Fraction(value) => Self::Fraction(value),
103            LayoutSize::Px(value) => Self::Px(value),
104        };
105        match text.trim_matches(' ').strip_suffix('%') {
106            Some(number) => parse_number(number)
107                .map(Self::Percent)
108                .ok_or_else(|| invalid_length(text, true)),
109            None => LayoutSize::parse_token(text.trim_matches(' '), true)
110                .map(size)
111                .map_err(|_| invalid_length(text, true)),
112        }
113    }
114}
115
116/// `n` canvas pixels as a fraction of a shorter side of `shorter_side`
117/// pixels.
118#[allow(clippy::cast_possible_truncation)]
119fn px_fraction(value: f32, shorter_side: f32) -> f32 {
120    (f64::from(value) / f64::from(shorter_side)) as f32
121}
122
123impl fmt::Display for LayoutLength {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match *self {
126            Self::Fraction(value) => write!(formatter, "{value}"),
127            Self::Px(value) => write_length(formatter, value, "px"),
128            Self::Percent(value) => write_length(formatter, value, "%"),
129        }
130    }
131}
132
133/// A layout length that may not be a percent: a gap, a radius, a border
134/// width, a font size, a padding or margin side, a translation.
135#[derive(Copy, Clone, Debug, PartialEq)]
136pub enum LayoutSize {
137    /// A fraction of the layout picture's shorter side, written as a number.
138    Fraction(f32),
139    /// Pixels of the project canvas, written `"<n>px"`.
140    Px(f32),
141}
142
143impl Default for LayoutSize {
144    fn default() -> Self {
145        Self::Fraction(0.0)
146    }
147}
148
149impl LayoutSize {
150    /// The number, whatever its unit.
151    #[must_use]
152    pub const fn value(self) -> f32 {
153        match self {
154            Self::Fraction(value) | Self::Px(value) => value,
155        }
156    }
157
158    /// The same size with pixels turned into a fraction of a picture
159    /// `shorter_side` pixels on its shorter side.
160    #[must_use]
161    pub fn without_px(self, shorter_side: f32) -> Self {
162        match self {
163            Self::Px(value) => Self::Fraction(px_fraction(value, shorter_side)),
164            other @ Self::Fraction(_) => other,
165        }
166    }
167
168    /// Its fraction of a picture `shorter_side` pixels on its shorter side.
169    #[must_use]
170    pub fn fraction(self, shorter_side: f32) -> f32 {
171        self.without_px(shorter_side).value()
172    }
173
174    /// The same kind of size with `value`.
175    #[must_use]
176    pub const fn with_value(self, value: f32) -> Self {
177        match self {
178            Self::Fraction(_) => Self::Fraction(value),
179            Self::Px(_) => Self::Px(value),
180        }
181    }
182
183    /// One token: a number or `<n>px`. `percent` says whether the field takes
184    /// a percent, for the message.
185    fn parse_token(token: &str, percent: bool) -> Result<Self, String> {
186        if let Some(number) = token.strip_suffix("px") {
187            return parse_number(number)
188                .map(Self::Px)
189                .ok_or_else(|| invalid_length(token, percent));
190        }
191        if !percent && token.ends_with('%') && parse_number(&token[..token.len() - 1]).is_some() {
192            return Err(format!("percent is not allowed here: \"{token}\""));
193        }
194        parse_number(token)
195            .map(Self::Fraction)
196            .ok_or_else(|| invalid_length(token, percent))
197    }
198
199    fn parse(text: &str) -> Result<Self, String> {
200        Self::parse_token(text.trim_matches(' '), false)
201    }
202}
203
204impl fmt::Display for LayoutSize {
205    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206        match *self {
207            Self::Fraction(value) => write!(formatter, "{value}"),
208            Self::Px(value) => write_length(formatter, value, "px"),
209        }
210    }
211}
212
213fn invalid_length(token: &str, percent: bool) -> String {
214    let hint = if token.contains(' ') {
215        format!(" (write {})", token.replace(' ', ""))
216    } else if token.ends_with("em") {
217        " (em and rem are not supported: write px or a number)".to_owned()
218    } else {
219        String::new()
220    };
221    let forms = if percent {
222        "a number, \"<n>px\", or \"<n>%\""
223    } else {
224        "a number or \"<n>px\""
225    };
226    format!("invalid length \"{token}\"{hint}: write {forms}")
227}
228
229/// A visitor for a value written as a number or a string.
230struct NumberOrString<T> {
231    expecting: &'static str,
232    number: fn(f32) -> T,
233    string: fn(&str) -> Result<T, String>,
234}
235
236impl<T> Visitor<'_> for NumberOrString<T> {
237    type Value = T;
238
239    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240        formatter.write_str(self.expecting)
241    }
242
243    #[allow(clippy::cast_possible_truncation)]
244    fn visit_f64<E: de::Error>(self, value: f64) -> Result<T, E> {
245        Ok((self.number)(value as f32))
246    }
247
248    #[allow(clippy::cast_precision_loss)]
249    fn visit_u64<E: de::Error>(self, value: u64) -> Result<T, E> {
250        Ok((self.number)(value as f32))
251    }
252
253    #[allow(clippy::cast_precision_loss)]
254    fn visit_i64<E: de::Error>(self, value: i64) -> Result<T, E> {
255        Ok((self.number)(value as f32))
256    }
257
258    fn visit_str<E: de::Error>(self, value: &str) -> Result<T, E> {
259        (self.string)(value).map_err(E::custom)
260    }
261}
262
263impl<'de> Deserialize<'de> for LayoutLength {
264    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
265        deserializer.deserialize_any(NumberOrString {
266            expecting: "a length: a number, \"<n>px\", or \"<n>%\"",
267            number: Self::Fraction,
268            string: Self::parse,
269        })
270    }
271}
272
273impl Serialize for LayoutLength {
274    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
275        match self {
276            Self::Fraction(value) => serializer.serialize_f32(*value),
277            other => serializer.collect_str(other),
278        }
279    }
280}
281
282impl<'de> Deserialize<'de> for LayoutSize {
283    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
284        deserializer.deserialize_any(NumberOrString {
285            expecting: "a length: a number or \"<n>px\"",
286            number: Self::Fraction,
287            string: Self::parse,
288        })
289    }
290}
291
292impl Serialize for LayoutSize {
293    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
294        match self {
295            Self::Fraction(value) => serializer.serialize_f32(*value),
296            other @ Self::Px(_) => serializer.collect_str(other),
297        }
298    }
299}
300
301/// A length on each side of a box (a padding or a margin). Missing sides
302/// are 0.
303///
304/// Written as one length for every side, as the CSS shorthand (`"8px 16px"`:
305/// one to four lengths, top first, clockwise, the missing ones repeating the
306/// opposite side), or as the sides object.
307#[derive(Copy, Clone, Debug, Default, PartialEq)]
308pub struct Insets {
309    /// Top side.
310    pub top: LayoutSize,
311    /// Right side.
312    pub right: LayoutSize,
313    /// Bottom side.
314    pub bottom: LayoutSize,
315    /// Left side.
316    pub left: LayoutSize,
317}
318
319impl Insets {
320    /// The sides, top first, clockwise.
321    #[must_use]
322    pub const fn sides(self) -> [LayoutSize; 4] {
323        [self.top, self.right, self.bottom, self.left]
324    }
325
326    /// Insets from sides, top first, clockwise.
327    #[must_use]
328    pub const fn from_sides([top, right, bottom, left]: [LayoutSize; 4]) -> Self {
329        Self {
330            top,
331            right,
332            bottom,
333            left,
334        }
335    }
336
337    /// The same insets with pixels turned into fractions.
338    #[must_use]
339    pub fn without_px(self, shorter_side: f32) -> Self {
340        Self::from_sides(self.sides().map(|side| side.without_px(shorter_side)))
341    }
342
343    fn parse(text: &str) -> Result<Self, String> {
344        let sides = tokens(text)
345            .map(|token| LayoutSize::parse_token(token, false))
346            .collect::<Result<Vec<_>, _>>()?;
347        let [top, right, bottom, left] = shorthand(&sides)?;
348        Ok(Self {
349            top,
350            right,
351            bottom,
352            left,
353        })
354    }
355}
356
357/// The four sides of a CSS shorthand of one to four values.
358fn shorthand<T: Copy>(values: &[T]) -> Result<[T; 4], String> {
359    match *values {
360        [all] => Ok([all; 4]),
361        [vertical, horizontal] => Ok([vertical, horizontal, vertical, horizontal]),
362        [top, horizontal, bottom] => Ok([top, horizontal, bottom, horizontal]),
363        [top, right, bottom, left] => Ok([top, right, bottom, left]),
364        _ => Err(format!("expected 1 to 4 lengths, got {}", values.len())),
365    }
366}
367
368#[derive(Deserialize)]
369#[serde(deny_unknown_fields)]
370struct InsetsObject {
371    #[serde(default)]
372    top: LayoutSize,
373    #[serde(default)]
374    right: LayoutSize,
375    #[serde(default)]
376    bottom: LayoutSize,
377    #[serde(default)]
378    left: LayoutSize,
379}
380
381struct InsetsVisitor;
382
383impl<'de> Visitor<'de> for InsetsVisitor {
384    type Value = Insets;
385
386    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
387        formatter.write_str("insets: a length, one to four lengths, or the sides object")
388    }
389
390    #[allow(clippy::cast_possible_truncation)]
391    fn visit_f64<E: de::Error>(self, value: f64) -> Result<Insets, E> {
392        Ok(Insets::from_sides([LayoutSize::Fraction(value as f32); 4]))
393    }
394
395    #[allow(clippy::cast_precision_loss)]
396    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Insets, E> {
397        Ok(Insets::from_sides([LayoutSize::Fraction(value as f32); 4]))
398    }
399
400    #[allow(clippy::cast_precision_loss)]
401    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Insets, E> {
402        Ok(Insets::from_sides([LayoutSize::Fraction(value as f32); 4]))
403    }
404
405    fn visit_str<E: de::Error>(self, value: &str) -> Result<Insets, E> {
406        Insets::parse(value).map_err(E::custom)
407    }
408
409    fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Insets, A::Error> {
410        let sides = InsetsObject::deserialize(de::value::MapAccessDeserializer::new(map))?;
411        Ok(Insets {
412            top: sides.top,
413            right: sides.right,
414            bottom: sides.bottom,
415            left: sides.left,
416        })
417    }
418}
419
420impl<'de> Deserialize<'de> for Insets {
421    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
422        deserializer.deserialize_any(InsetsVisitor)
423    }
424}
425
426impl Serialize for Insets {
427    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
428        let mut sides = serializer.serialize_struct("Insets", 4)?;
429        sides.serialize_field("top", &self.top)?;
430        sides.serialize_field("right", &self.right)?;
431        sides.serialize_field("bottom", &self.bottom)?;
432        sides.serialize_field("left", &self.left)?;
433        sides.end()
434    }
435}
436
437/// A box's offset after layout: `x` right, `y` down.
438///
439/// Written as the object, or as the CSS `translate` value: one or two
440/// lengths, `y` 0 when missing.
441#[derive(Copy, Clone, Debug, Default, PartialEq)]
442pub struct LayoutOffset {
443    /// Rightward.
444    pub x: LayoutSize,
445    /// Downward.
446    pub y: LayoutSize,
447}
448
449impl LayoutOffset {
450    /// The same offset with pixels turned into fractions.
451    #[must_use]
452    pub fn without_px(self, shorter_side: f32) -> Self {
453        Self {
454            x: self.x.without_px(shorter_side),
455            y: self.y.without_px(shorter_side),
456        }
457    }
458
459    fn parse(text: &str) -> Result<Self, String> {
460        let values = tokens(text)
461            .map(|token| LayoutSize::parse_token(token, false))
462            .collect::<Result<Vec<_>, _>>()?;
463        match *values {
464            [x] => Ok(Self {
465                x,
466                y: LayoutSize::default(),
467            }),
468            [x, y] => Ok(Self { x, y }),
469            _ => Err(format!("expected 1 or 2 lengths, got {}", values.len())),
470        }
471    }
472}
473
474#[derive(Deserialize)]
475#[serde(deny_unknown_fields)]
476struct OffsetObject {
477    x: LayoutSize,
478    y: LayoutSize,
479}
480
481struct OffsetVisitor;
482
483impl<'de> Visitor<'de> for OffsetVisitor {
484    type Value = LayoutOffset;
485
486    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
487        formatter.write_str("an offset: one or two lengths, or {x, y}")
488    }
489
490    fn visit_str<E: de::Error>(self, value: &str) -> Result<LayoutOffset, E> {
491        LayoutOffset::parse(value).map_err(E::custom)
492    }
493
494    fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<LayoutOffset, A::Error> {
495        let OffsetObject { x, y } =
496            OffsetObject::deserialize(de::value::MapAccessDeserializer::new(map))?;
497        Ok(LayoutOffset { x, y })
498    }
499}
500
501impl<'de> Deserialize<'de> for LayoutOffset {
502    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
503        deserializer.deserialize_any(OffsetVisitor)
504    }
505}
506
507impl Serialize for LayoutOffset {
508    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
509        let mut offset = serializer.serialize_struct("LayoutOffset", 2)?;
510        offset.serialize_field("x", &self.x)?;
511        offset.serialize_field("y", &self.y)?;
512        offset.end()
513    }
514}
515
516/// How large a grid track may be at least or at most: one side of
517/// `minmax()`.
518#[derive(Copy, Clone, Debug, PartialEq)]
519pub enum TrackBreadth {
520    /// A length or a percent of the grid's content box.
521    Length(LayoutLength),
522    /// A share of the free space, `<n>fr`; only as the largest size.
523    Fr(f32),
524    /// `auto`.
525    Auto,
526    /// `min-content`.
527    MinContent,
528    /// `max-content`.
529    MaxContent,
530}
531
532impl TrackBreadth {
533    const fn is_fixed(self) -> bool {
534        matches!(self, Self::Length(_))
535    }
536
537    fn without_px(self, shorter_side: f32) -> Self {
538        match self {
539            Self::Length(length) => Self::Length(length.without_px(shorter_side)),
540            other => other,
541        }
542    }
543
544    fn parse(token: &str, max: bool) -> Result<Self, String> {
545        match token {
546            "auto" => Ok(Self::Auto),
547            "min-content" => Ok(Self::MinContent),
548            "max-content" => Ok(Self::MaxContent),
549            _ => {
550                if let Some(share) = token.strip_suffix("fr").and_then(parse_number) {
551                    return if max {
552                        Ok(Self::Fr(share))
553                    } else {
554                        Err(format!("a minimum size cannot be fr: \"{token}\""))
555                    };
556                }
557                LayoutLength::parse(token)
558                    .map(Self::Length)
559                    .map_err(|_| unknown_track(token))
560            }
561        }
562    }
563}
564
565impl fmt::Display for TrackBreadth {
566    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
567        match self {
568            Self::Length(length) => write!(formatter, "{length}"),
569            Self::Fr(share) => write_length(formatter, *share, "fr"),
570            Self::Auto => formatter.write_str("auto"),
571            Self::MinContent => formatter.write_str("min-content"),
572            Self::MaxContent => formatter.write_str("max-content"),
573        }
574    }
575}
576
577/// The size of one grid track.
578#[derive(Copy, Clone, Debug, PartialEq)]
579pub enum TrackSize {
580    /// One breadth, as CSS writes it: a length is fixed, `<n>fr` is
581    /// `minmax(auto, <n>fr)`, a keyword sizes by content.
582    Breadth(TrackBreadth),
583    /// `minmax(min, max)`.
584    MinMax(TrackBreadth, TrackBreadth),
585    /// `fit-content(limit)`.
586    FitContent(LayoutLength),
587}
588
589impl TrackSize {
590    /// Whether it is a CSS fixed size, as `auto-fill` and `auto-fit` need:
591    /// a length, or `minmax()` with a length on one side and no flexible
592    /// or content minimum.
593    #[must_use]
594    pub const fn is_fixed(self) -> bool {
595        match self {
596            Self::Breadth(breadth) => breadth.is_fixed(),
597            Self::MinMax(min, max) => {
598                min.is_fixed() || (max.is_fixed() && !matches!(min, TrackBreadth::Fr(_)))
599            }
600            Self::FitContent(_) => false,
601        }
602    }
603
604    fn without_px(self, shorter_side: f32) -> Self {
605        match self {
606            Self::Breadth(breadth) => Self::Breadth(breadth.without_px(shorter_side)),
607            Self::MinMax(min, max) => {
608                Self::MinMax(min.without_px(shorter_side), max.without_px(shorter_side))
609            }
610            Self::FitContent(limit) => Self::FitContent(limit.without_px(shorter_side)),
611        }
612    }
613
614    fn lengths(self) -> impl Iterator<Item = LayoutLength> {
615        let breadth = |breadth: TrackBreadth| match breadth {
616            TrackBreadth::Length(length) => Some(length),
617            _ => None,
618        };
619        let (first, second) = match self {
620            Self::Breadth(single) => (breadth(single), None),
621            Self::MinMax(min, max) => (breadth(min), breadth(max)),
622            Self::FitContent(limit) => (Some(limit), None),
623        };
624        first.into_iter().chain(second)
625    }
626
627    fn fr(self) -> Option<f32> {
628        match self {
629            Self::Breadth(TrackBreadth::Fr(share)) | Self::MinMax(_, TrackBreadth::Fr(share)) => {
630                Some(share)
631            }
632            _ => None,
633        }
634    }
635}
636
637impl fmt::Display for TrackSize {
638    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
639        match self {
640            Self::Breadth(breadth) => write!(formatter, "{breadth}"),
641            Self::MinMax(min, max) => write!(formatter, "minmax({min}, {max})"),
642            Self::FitContent(limit) => write!(formatter, "fit-content({limit})"),
643        }
644    }
645}
646
647/// How often `repeat()` repeats its tracks.
648#[derive(Copy, Clone, Debug, Eq, PartialEq)]
649pub enum RepeatCount {
650    /// That many times.
651    Count(u16),
652    /// `auto-fill`: as many as fit.
653    AutoFill,
654    /// `auto-fit`: as many as fit, empty ones collapsed.
655    AutoFit,
656}
657
658impl fmt::Display for RepeatCount {
659    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
660        match self {
661            Self::Count(count) => write!(formatter, "{count}"),
662            Self::AutoFill => formatter.write_str("auto-fill"),
663            Self::AutoFit => formatter.write_str("auto-fit"),
664        }
665    }
666}
667
668/// One entry of a track list: a track, or `repeat()` of some.
669#[derive(Clone, Debug, PartialEq)]
670pub enum TrackComponent {
671    /// One track.
672    Single(TrackSize),
673    /// `repeat(count, tracks)`.
674    Repeat(RepeatCount, Vec<TrackSize>),
675}
676
677/// A grid track list, as `grid_template_columns` writes it in CSS:
678/// `"1fr 2fr 120px"`, `"repeat(3, minmax(100px, 1fr))"`,
679/// `"repeat(auto-fill, 160px)"`. Named lines are not supported.
680#[derive(Clone, Debug, Default, PartialEq)]
681pub struct TrackList(pub Vec<TrackComponent>);
682
683impl TrackList {
684    /// How many explicit tracks it has with each count repeated (an
685    /// automatic repeat counts once).
686    #[must_use]
687    pub fn track_count(&self) -> usize {
688        self.0
689            .iter()
690            .map(|component| match component {
691                TrackComponent::Single(_) => 1,
692                TrackComponent::Repeat(RepeatCount::Count(count), tracks) => {
693                    usize::from(*count) * tracks.len()
694                }
695                TrackComponent::Repeat(_, tracks) => tracks.len(),
696            })
697            .sum()
698    }
699
700    /// Every track size, once each, repeats included once.
701    pub fn sizes(&self) -> impl Iterator<Item = TrackSize> + '_ {
702        self.0.iter().flat_map(|component| match component {
703            TrackComponent::Single(size) => std::slice::from_ref(size).iter().copied(),
704            TrackComponent::Repeat(_, tracks) => tracks.iter().copied(),
705        })
706    }
707
708    /// Every length in it, for range checks.
709    pub fn lengths(&self) -> impl Iterator<Item = LayoutLength> + '_ {
710        self.sizes().flat_map(TrackSize::lengths)
711    }
712
713    /// Every `fr` share in it.
714    pub fn shares(&self) -> impl Iterator<Item = f32> + '_ {
715        self.sizes().filter_map(TrackSize::fr)
716    }
717
718    /// The same list with pixels turned into fractions.
719    #[must_use]
720    pub fn without_px(&self, shorter_side: f32) -> Self {
721        Self(
722            self.0
723                .iter()
724                .map(|component| match component {
725                    TrackComponent::Single(size) => {
726                        TrackComponent::Single(size.without_px(shorter_side))
727                    }
728                    TrackComponent::Repeat(count, tracks) => TrackComponent::Repeat(
729                        *count,
730                        tracks
731                            .iter()
732                            .map(|size| size.without_px(shorter_side))
733                            .collect(),
734                    ),
735                })
736                .collect(),
737        )
738    }
739
740    fn parse(text: &str) -> Result<Self, String> {
741        let mut parser = TrackParser { text, at: 0 };
742        let mut components = Vec::new();
743        while parser.skip_spaces() {
744            if parser.rest().starts_with("repeat(") {
745                parser.at += "repeat(".len();
746                components.push(parser.repeat()?);
747            } else {
748                components.push(TrackComponent::Single(parser.track()?));
749            }
750        }
751        if components.is_empty() {
752            return Err("expected at least one track".to_owned());
753        }
754        Ok(Self(components))
755    }
756}
757
758impl fmt::Display for TrackList {
759    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
760        for (index, component) in self.0.iter().enumerate() {
761            if index > 0 {
762                formatter.write_char(' ')?;
763            }
764            match component {
765                TrackComponent::Single(size) => write!(formatter, "{size}")?,
766                TrackComponent::Repeat(count, tracks) => {
767                    write!(formatter, "repeat({count},")?;
768                    for size in tracks {
769                        write!(formatter, " {size}")?;
770                    }
771                    formatter.write_char(')')?;
772                }
773            }
774        }
775        Ok(())
776    }
777}
778
779/// The message for a track that is not one, with the spelling likely meant.
780fn unknown_track(token: &str) -> String {
781    let hint = match token {
782        "min_content" => Some("min-content".to_owned()),
783        "max_content" => Some("max-content".to_owned()),
784        "auto_fill" => Some("auto-fill".to_owned()),
785        "auto_fit" => Some("auto-fit".to_owned()),
786        _ if token.starts_with('[') => {
787            return format!("named grid lines are not supported: \"{token}\"");
788        }
789        _ if token.contains(' ') => Some(token.replace(' ', "")),
790        _ => None,
791    };
792    match hint {
793        Some(hint) => format!("unknown track \"{token}\" (write {hint})"),
794        None => format!("unknown track \"{token}\""),
795    }
796}
797
798/// Reads a track list left to right.
799struct TrackParser<'a> {
800    text: &'a str,
801    at: usize,
802}
803
804impl<'a> TrackParser<'a> {
805    fn rest(&self) -> &'a str {
806        &self.text[self.at..]
807    }
808
809    /// Skips spaces; whether anything is left.
810    fn skip_spaces(&mut self) -> bool {
811        while self.rest().starts_with(' ') {
812            self.at += 1;
813        }
814        !self.rest().is_empty()
815    }
816
817    /// The next token: up to a space, a comma or a parenthesis.
818    fn token(&mut self) -> &'a str {
819        let rest = &self.text[self.at..];
820        let end = rest.find([' ', ',', '(', ')']).unwrap_or(rest.len());
821        self.at += end;
822        &rest[..end]
823    }
824
825    /// `expected` after optional spaces.
826    fn expect(&mut self, expected: char) -> Result<(), String> {
827        self.skip_spaces();
828        if self.rest().starts_with(expected) {
829            self.at += 1;
830            Ok(())
831        } else {
832            Err(format!("expected \"{expected}\" in \"{}\"", self.text))
833        }
834    }
835
836    fn track(&mut self) -> Result<TrackSize, String> {
837        let start = self.at;
838        let token = self.token();
839        match token {
840            "minmax" | "fit-content" if self.rest().starts_with('(') => {
841                self.at += 1;
842                if token == "fit-content" {
843                    let limit = self.breadth(false)?;
844                    self.expect(')')?;
845                    return match limit {
846                        TrackBreadth::Length(limit) => Ok(TrackSize::FitContent(limit)),
847                        _ => Err(format!(
848                            "fit-content takes a length: \"{}\"",
849                            &self.text[start..self.at]
850                        )),
851                    };
852                }
853                let min = self.breadth(false)?;
854                self.expect(',')?;
855                let max = self.breadth(true)?;
856                self.expect(')')?;
857                Ok(TrackSize::MinMax(min, max))
858            }
859            "" => Err(format!("expected a track in \"{}\"", self.text)),
860            "fit_content" | "min_max" => Err(format!(
861                "unknown track \"{token}\" (write {})",
862                token.replace('_', "-").replace("min-max", "minmax")
863            )),
864            _ => {
865                // A unit standing apart from its number: "1 fr".
866                let after = self.at;
867                if parse_number(token).is_some() && self.skip_spaces() {
868                    let mark = self.at;
869                    let unit = self.token();
870                    if matches!(unit, "fr" | "px" | "%") {
871                        return Err(unknown_track(&self.text[start..self.at]));
872                    }
873                    self.at = mark;
874                }
875                self.at = after;
876                TrackBreadth::parse(token, true).map(TrackSize::Breadth)
877            }
878        }
879    }
880
881    fn breadth(&mut self, max: bool) -> Result<TrackBreadth, String> {
882        self.skip_spaces();
883        let token = self.token();
884        if token.is_empty() {
885            return Err(format!("expected a track size in \"{}\"", self.text));
886        }
887        TrackBreadth::parse(token, max)
888    }
889
890    fn repeat(&mut self) -> Result<TrackComponent, String> {
891        self.skip_spaces();
892        let token = self.token();
893        let count = match token {
894            "auto-fill" => RepeatCount::AutoFill,
895            "auto-fit" => RepeatCount::AutoFit,
896            "auto_fill" | "auto_fit" => {
897                return Err(format!(
898                    "unknown repeat count \"{token}\" (write {})",
899                    token.replace('_', "-")
900                ));
901            }
902            _ => token
903                .parse::<u16>()
904                .ok()
905                .filter(|_| token.bytes().all(|byte| byte.is_ascii_digit()))
906                .map(RepeatCount::Count)
907                .ok_or_else(|| format!("unknown repeat count \"{token}\""))?,
908        };
909        self.expect(',')?;
910        let mut tracks = Vec::new();
911        loop {
912            if !self.skip_spaces() {
913                return Err(format!("expected \")\" in \"{}\"", self.text));
914            }
915            if self.rest().starts_with(')') {
916                self.at += 1;
917                break;
918            }
919            if self.rest().starts_with("repeat(") {
920                return Err("repeat() cannot hold repeat()".to_owned());
921            }
922            tracks.push(self.track()?);
923        }
924        if tracks.is_empty() {
925            return Err("repeat() needs at least one track".to_owned());
926        }
927        Ok(TrackComponent::Repeat(count, tracks))
928    }
929}
930
931impl<'de> Deserialize<'de> for TrackList {
932    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
933        let text = String::deserialize(deserializer)?;
934        Self::parse(&text).map_err(de::Error::custom)
935    }
936}
937
938impl Serialize for TrackList {
939    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
940        serializer.collect_str(self)
941    }
942}
943
944/// One edge of a grid item's placement.
945#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
946pub enum GridEdge {
947    /// Placed automatically.
948    #[default]
949    Auto,
950    /// At a line: 1 is the first, -1 the last explicit line.
951    Line(i16),
952    /// So many tracks from the other edge.
953    Span(u16),
954}
955
956impl GridEdge {
957    fn parse(text: &str) -> Result<Self, String> {
958        let words = tokens(text).collect::<Vec<_>>();
959        let integer = |word: &str| {
960            let digits = word.strip_prefix('-').unwrap_or(word);
961            (!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
962                .then(|| word.parse::<i16>().ok())
963                .flatten()
964        };
965        match *words {
966            ["auto"] => Ok(Self::Auto),
967            ["span", count] => integer(count)
968                .and_then(|count| u16::try_from(count).ok())
969                .map(Self::Span)
970                .ok_or_else(|| invalid_line(text)),
971            [line] => integer(line)
972                .map(Self::Line)
973                .ok_or_else(|| invalid_line(text)),
974            _ => Err(invalid_line(text)),
975        }
976    }
977}
978
979fn invalid_line(text: &str) -> String {
980    let text = text.trim_matches(' ');
981    let named = text
982        .split([' ', '/'])
983        .filter(|word| !word.is_empty())
984        .any(|word| {
985            word != "span"
986                && word != "auto"
987                && word
988                    .bytes()
989                    .next()
990                    .is_some_and(|byte| byte.is_ascii_alphabetic())
991        });
992    if named {
993        format!("named grid lines are not supported: \"{text}\"")
994    } else {
995        format!("invalid grid line \"{text}\": write a line number, \"span <n>\", or \"auto\"")
996    }
997}
998
999impl fmt::Display for GridEdge {
1000    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1001        match self {
1002            Self::Auto => formatter.write_str("auto"),
1003            Self::Line(line) => write!(formatter, "{line}"),
1004            Self::Span(count) => write!(formatter, "span {count}"),
1005        }
1006    }
1007}
1008
1009/// Where a grid item lies on one axis, as `grid_column` writes it in CSS:
1010/// `2`, `"1 / 3"`, `"span 2"`, `"2 / span 3"`, `"auto"`. A missing end is
1011/// `auto`.
1012#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
1013pub struct GridPlacement {
1014    /// The start edge.
1015    pub start: GridEdge,
1016    /// The end edge.
1017    pub end: GridEdge,
1018}
1019
1020impl GridPlacement {
1021    /// Every edge of it.
1022    #[must_use]
1023    pub const fn edges(self) -> [GridEdge; 2] {
1024        [self.start, self.end]
1025    }
1026
1027    fn parse(text: &str) -> Result<Self, String> {
1028        let (start, end) = match text.split_once('/') {
1029            Some((start, end)) => (start, Some(end)),
1030            None => (text, None),
1031        };
1032        if end.is_some_and(|end| end.contains('/')) {
1033            return Err(invalid_line(text));
1034        }
1035        let edge = |text| GridEdge::parse(text).map_err(|_| invalid_line(text));
1036        Ok(Self {
1037            start: edge(start)?,
1038            end: end.map_or(Ok(GridEdge::Auto), edge)?,
1039        })
1040    }
1041}
1042
1043impl fmt::Display for GridPlacement {
1044    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1045        match self.end {
1046            GridEdge::Auto => write!(formatter, "{}", self.start),
1047            end => write!(formatter, "{} / {end}", self.start),
1048        }
1049    }
1050}
1051
1052struct PlacementVisitor;
1053
1054impl Visitor<'_> for PlacementVisitor {
1055    type Value = GridPlacement;
1056
1057    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1058        formatter.write_str("a grid line: a line number, \"1 / 3\", \"span 2\", or \"auto\"")
1059    }
1060
1061    fn visit_u64<E: de::Error>(self, value: u64) -> Result<GridPlacement, E> {
1062        i16::try_from(value)
1063            .map(|line| GridPlacement {
1064                start: GridEdge::Line(line),
1065                end: GridEdge::Auto,
1066            })
1067            .map_err(|_| E::custom(invalid_line(&value.to_string())))
1068    }
1069
1070    fn visit_i64<E: de::Error>(self, value: i64) -> Result<GridPlacement, E> {
1071        i16::try_from(value)
1072            .map(|line| GridPlacement {
1073                start: GridEdge::Line(line),
1074                end: GridEdge::Auto,
1075            })
1076            .map_err(|_| E::custom(invalid_line(&value.to_string())))
1077    }
1078
1079    /// A JSON number written with a fraction is a line when it is whole, as
1080    /// a JavaScript number is.
1081    #[allow(clippy::cast_possible_truncation)]
1082    fn visit_f64<E: de::Error>(self, value: f64) -> Result<GridPlacement, E> {
1083        if value.fract() == 0.0 && (f64::from(i16::MIN)..=f64::from(i16::MAX)).contains(&value) {
1084            return Ok(GridPlacement {
1085                start: GridEdge::Line(value as i16),
1086                end: GridEdge::Auto,
1087            });
1088        }
1089        Err(E::custom(invalid_line(&value.to_string())))
1090    }
1091
1092    fn visit_str<E: de::Error>(self, value: &str) -> Result<GridPlacement, E> {
1093        GridPlacement::parse(value).map_err(E::custom)
1094    }
1095}
1096
1097impl<'de> Deserialize<'de> for GridPlacement {
1098    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1099        deserializer.deserialize_any(PlacementVisitor)
1100    }
1101}
1102
1103impl Serialize for GridPlacement {
1104    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1105        match (self.start, self.end) {
1106            (GridEdge::Line(line), GridEdge::Auto) => serializer.serialize_i16(line),
1107            _ => serializer.collect_str(self),
1108        }
1109    }
1110}
1111
1112#[cfg(feature = "schema")]
1113mod schema {
1114    //! The schema of each form: what JSON it takes. A string's grammar is
1115    //! the parser's; the patterns say it where a regular expression can.
1116
1117    use super::{GridPlacement, Insets, LayoutLength, LayoutOffset, LayoutSize, TrackList};
1118    use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
1119    use std::borrow::Cow;
1120
1121    const NUMBER: &str = r"-?(\d+\.?\d*|\.\d+)";
1122
1123    fn size_pattern() -> String {
1124        format!("^ *{NUMBER}(px)? *$")
1125    }
1126
1127    fn length_pattern() -> String {
1128        format!("^ *{NUMBER}(px|%)? *$")
1129    }
1130
1131    fn sides_pattern() -> String {
1132        format!("^ *{NUMBER}(px)?( +{NUMBER}(px)?){{0,3}} *$")
1133    }
1134
1135    impl JsonSchema for LayoutLength {
1136        fn schema_name() -> Cow<'static, str> {
1137            "LayoutLength".into()
1138        }
1139
1140        fn json_schema(_: &mut SchemaGenerator) -> Schema {
1141            json_schema!({
1142                "description": "A length: a number is a fraction of the layout picture's shorter side, \"<n>px\" pixels of the project canvas, \"<n>%\" a percent of the parent's content box along the field's axis.",
1143                "anyOf": [
1144                    { "type": "number", "format": "float" },
1145                    { "type": "string", "pattern": length_pattern() },
1146                ],
1147            })
1148        }
1149    }
1150
1151    impl JsonSchema for LayoutSize {
1152        fn schema_name() -> Cow<'static, str> {
1153            "LayoutSize".into()
1154        }
1155
1156        fn json_schema(_: &mut SchemaGenerator) -> Schema {
1157            json_schema!({
1158                "description": "A length that may not be a percent: a number is a fraction of the layout picture's shorter side, \"<n>px\" pixels of the project canvas.",
1159                "anyOf": [
1160                    { "type": "number", "format": "float" },
1161                    { "type": "string", "pattern": size_pattern() },
1162                ],
1163            })
1164        }
1165    }
1166
1167    impl JsonSchema for Insets {
1168        fn schema_name() -> Cow<'static, str> {
1169            "Insets".into()
1170        }
1171
1172        fn json_schema(generator: &mut SchemaGenerator) -> Schema {
1173            let side = generator.subschema_for::<LayoutSize>();
1174            json_schema!({
1175                "description": "A length on each side of a box: one length for every side, the CSS shorthand of one to four lengths (\"8px 16px\": top first, clockwise), or the sides object, missing sides 0.",
1176                "anyOf": [
1177                    { "type": "number", "format": "float" },
1178                    { "type": "string", "pattern": sides_pattern() },
1179                    {
1180                        "type": "object",
1181                        "properties": {
1182                            "top": side,
1183                            "right": side,
1184                            "bottom": side,
1185                            "left": side,
1186                        },
1187                        "additionalProperties": false,
1188                    },
1189                ],
1190            })
1191        }
1192    }
1193
1194    impl JsonSchema for LayoutOffset {
1195        fn schema_name() -> Cow<'static, str> {
1196            "LayoutOffset".into()
1197        }
1198
1199        fn json_schema(generator: &mut SchemaGenerator) -> Schema {
1200            let side = generator.subschema_for::<LayoutSize>();
1201            json_schema!({
1202                "description": "An offset, x right and y down: one or two lengths as CSS translate writes them (y 0 when missing), or {x, y}.",
1203                "anyOf": [
1204                    {
1205                        "type": "string",
1206                        "pattern": format!("^ *{NUMBER}(px)?( +{NUMBER}(px)?)? *$"),
1207                    },
1208                    {
1209                        "type": "object",
1210                        "properties": { "x": side, "y": side },
1211                        "required": ["x", "y"],
1212                        "additionalProperties": false,
1213                    },
1214                ],
1215            })
1216        }
1217    }
1218
1219    impl JsonSchema for TrackList {
1220        fn schema_name() -> Cow<'static, str> {
1221            "TrackList".into()
1222        }
1223
1224        fn json_schema(_: &mut SchemaGenerator) -> Schema {
1225            json_schema!({
1226                "description": "A grid track list as CSS writes it: \"1fr 2fr 120px\", \"repeat(3, minmax(100px, 1fr))\", \"repeat(auto-fill, 160px)\", \"auto min-content max-content\", \"fit-content(200px)\". A bare number is a fraction of the picture's shorter side. Named lines are not supported.",
1227                "type": "string",
1228            })
1229        }
1230    }
1231
1232    impl JsonSchema for GridPlacement {
1233        fn schema_name() -> Cow<'static, str> {
1234            "GridPlacement".into()
1235        }
1236
1237        fn json_schema(_: &mut SchemaGenerator) -> Schema {
1238            json_schema!({
1239                "description": "Where a grid item lies on one axis, as CSS writes it: 2, \"1 / 3\", \"span 2\", \"2 / span 3\", \"auto\". Lines count from 1, or from -1 at the end.",
1240                "anyOf": [
1241                    { "type": "integer", "minimum": -32768, "maximum": 32767 },
1242                    { "type": "string" },
1243                ],
1244            })
1245        }
1246    }
1247}
1248
1249#[cfg(test)]
1250#[path = "layout_values_tests.rs"]
1251mod tests;