1use serde::de::{self, Deserializer, MapAccess, Visitor};
17use serde::ser::{SerializeStruct, Serializer};
18use serde::{Deserialize, Serialize};
19use std::fmt::{self, Write as _};
20
21pub(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
36fn tokens(text: &str) -> impl Iterator<Item = &str> {
38 text.split(' ').filter(|token| !token.is_empty())
39}
40
41fn write_length(out: &mut impl fmt::Write, value: f32, unit: &str) -> fmt::Result {
43 write!(out, "{value}{unit}")
44}
45
46#[derive(Copy, Clone, Debug, PartialEq)]
48pub enum LayoutLength {
49 Fraction(f32),
51 Px(f32),
54 Percent(f32),
57}
58
59impl Default for LayoutLength {
60 fn default() -> Self {
61 Self::Fraction(0.0)
62 }
63}
64
65impl LayoutLength {
66 #[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 #[must_use]
76 pub const fn is_percent(self) -> bool {
77 matches!(self, Self::Percent(_))
78 }
79
80 #[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 #[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#[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#[derive(Copy, Clone, Debug, PartialEq)]
136pub enum LayoutSize {
137 Fraction(f32),
139 Px(f32),
141}
142
143impl Default for LayoutSize {
144 fn default() -> Self {
145 Self::Fraction(0.0)
146 }
147}
148
149impl LayoutSize {
150 #[must_use]
152 pub const fn value(self) -> f32 {
153 match self {
154 Self::Fraction(value) | Self::Px(value) => value,
155 }
156 }
157
158 #[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 #[must_use]
170 pub fn fraction(self, shorter_side: f32) -> f32 {
171 self.without_px(shorter_side).value()
172 }
173
174 #[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 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
229struct 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#[derive(Copy, Clone, Debug, Default, PartialEq)]
308pub struct Insets {
309 pub top: LayoutSize,
311 pub right: LayoutSize,
313 pub bottom: LayoutSize,
315 pub left: LayoutSize,
317}
318
319impl Insets {
320 #[must_use]
322 pub const fn sides(self) -> [LayoutSize; 4] {
323 [self.top, self.right, self.bottom, self.left]
324 }
325
326 #[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 #[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
357fn 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#[derive(Copy, Clone, Debug, Default, PartialEq)]
442pub struct LayoutOffset {
443 pub x: LayoutSize,
445 pub y: LayoutSize,
447}
448
449impl LayoutOffset {
450 #[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#[derive(Copy, Clone, Debug, PartialEq)]
519pub enum TrackBreadth {
520 Length(LayoutLength),
522 Fr(f32),
524 Auto,
526 MinContent,
528 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#[derive(Copy, Clone, Debug, PartialEq)]
579pub enum TrackSize {
580 Breadth(TrackBreadth),
583 MinMax(TrackBreadth, TrackBreadth),
585 FitContent(LayoutLength),
587}
588
589impl TrackSize {
590 #[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#[derive(Copy, Clone, Debug, Eq, PartialEq)]
649pub enum RepeatCount {
650 Count(u16),
652 AutoFill,
654 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#[derive(Clone, Debug, PartialEq)]
670pub enum TrackComponent {
671 Single(TrackSize),
673 Repeat(RepeatCount, Vec<TrackSize>),
675}
676
677#[derive(Clone, Debug, Default, PartialEq)]
681pub struct TrackList(pub Vec<TrackComponent>);
682
683impl TrackList {
684 #[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 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 pub fn lengths(&self) -> impl Iterator<Item = LayoutLength> + '_ {
710 self.sizes().flat_map(TrackSize::lengths)
711 }
712
713 pub fn shares(&self) -> impl Iterator<Item = f32> + '_ {
715 self.sizes().filter_map(TrackSize::fr)
716 }
717
718 #[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
779fn 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
798struct 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 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 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 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 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#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
946pub enum GridEdge {
947 #[default]
949 Auto,
950 Line(i16),
952 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#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
1013pub struct GridPlacement {
1014 pub start: GridEdge,
1016 pub end: GridEdge,
1018}
1019
1020impl GridPlacement {
1021 #[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 #[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 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;