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