Skip to main content

whisker_css/
value.rs

1//! Property-input composite value types.
2//!
3//! Some CSS properties accept a value Lynx does not document as a
4//! standalone data type — e.g. `width` accepts a `<length-percentage>`,
5//! `auto`, `max-content`, or a `fit-content()` function. Modeling
6//! that mixture cleanly requires a Rust enum that gathers the
7//! allowed forms in one place. Those enums live here so each
8//! property method on [`Css`](crate::Css) can declare a precise
9//! argument type.
10
11use core::fmt;
12
13use crate::data_type::{
14    Color, CssString, FitContent, Length, LengthPercentage, MaxContent, Number, Percentage,
15};
16use crate::to_css::{ToCss, write_number};
17
18/// A keyword or signed distance for `vertical-align`.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub enum VerticalAlignment {
21    /// Inline alignment keyword.
22    Keyword(crate::VerticalAlign),
23    /// Signed baseline shift; positive values raise the content.
24    Offset(Length),
25}
26
27impl From<crate::VerticalAlign> for VerticalAlignment {
28    fn from(value: crate::VerticalAlign) -> Self {
29        Self::Keyword(value)
30    }
31}
32impl From<Length> for VerticalAlignment {
33    fn from(value: Length) -> Self {
34        Self::Offset(value)
35    }
36}
37impl ToCss for VerticalAlignment {
38    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
39        match self {
40            Self::Keyword(value) => value.to_css(dest),
41            Self::Offset(value) => value.to_css(dest),
42        }
43    }
44}
45
46// ---------- BackdropFilter ----------
47
48/// Supported value of `backdrop-filter`.
49///
50/// Whisker deliberately exposes only the app-oriented blur subset rather than
51/// the complete CSS filter-function list.
52#[derive(Clone, Debug, PartialEq)]
53pub enum BackdropFilter {
54    /// `none` — do not alter pixels behind the element.
55    None,
56    /// `blur(<length>)` — blur pixels already painted behind the element.
57    Blur(crate::ValueOrVariable<Length>),
58}
59
60impl BackdropFilter {
61    /// Creates `blur(<radius>)`.
62    pub fn blur(radius: impl Into<crate::ValueOrVariable<Length>>) -> Self {
63        Self::Blur(radius.into())
64    }
65}
66
67impl ToCss for BackdropFilter {
68    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
69        match self {
70            Self::None => dest.write_str("none"),
71            Self::Blur(radius) => {
72                dest.write_str("blur(")?;
73                radius.to_css(dest)?;
74                dest.write_char(')')
75            }
76        }
77    }
78}
79
80// ---------- Box shadow ----------
81
82/// One structured `box-shadow` layer.
83#[derive(Clone, Debug, PartialEq)]
84pub struct BoxShadow {
85    /// Horizontal offset.
86    pub offset_x: crate::ValueOrVariable<Length>,
87    /// Vertical offset.
88    pub offset_y: crate::ValueOrVariable<Length>,
89    /// Non-negative blur radius.
90    pub blur_radius: crate::ValueOrVariable<Length>,
91    /// Signed spread radius.
92    pub spread_radius: crate::ValueOrVariable<Length>,
93    /// Shadow color.
94    pub color: crate::ValueOrVariable<Color>,
95    /// Whether the shadow is painted inside the box.
96    pub inset: bool,
97}
98
99impl BoxShadow {
100    /// Creates an outer shadow.
101    pub fn outer(
102        offset_x: impl Into<crate::ValueOrVariable<Length>>,
103        offset_y: impl Into<crate::ValueOrVariable<Length>>,
104        blur_radius: impl Into<crate::ValueOrVariable<Length>>,
105        spread_radius: impl Into<crate::ValueOrVariable<Length>>,
106        color: impl Into<crate::ValueOrVariable<Color>>,
107    ) -> Self {
108        Self {
109            offset_x: offset_x.into(),
110            offset_y: offset_y.into(),
111            blur_radius: blur_radius.into(),
112            spread_radius: spread_radius.into(),
113            color: color.into(),
114            inset: false,
115        }
116    }
117
118    /// Creates an inset shadow.
119    pub fn inset(
120        offset_x: impl Into<crate::ValueOrVariable<Length>>,
121        offset_y: impl Into<crate::ValueOrVariable<Length>>,
122        blur_radius: impl Into<crate::ValueOrVariable<Length>>,
123        spread_radius: impl Into<crate::ValueOrVariable<Length>>,
124        color: impl Into<crate::ValueOrVariable<Color>>,
125    ) -> Self {
126        Self {
127            inset: true,
128            ..Self::outer(offset_x, offset_y, blur_radius, spread_radius, color)
129        }
130    }
131}
132
133impl ToCss for BoxShadow {
134    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
135        if self.inset {
136            dest.write_str("inset ")?;
137        }
138        self.offset_x.to_css(dest)?;
139        dest.write_char(' ')?;
140        self.offset_y.to_css(dest)?;
141        dest.write_char(' ')?;
142        self.blur_radius.to_css(dest)?;
143        dest.write_char(' ')?;
144        self.spread_radius.to_css(dest)?;
145        dest.write_char(' ')?;
146        self.color.to_css(dest)
147    }
148}
149
150// ---------- Clip path ----------
151
152/// Reference box used by a structured clip path.
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
154pub enum ClipBox {
155    /// Border box.
156    #[default]
157    BorderBox,
158    /// Padding box.
159    PaddingBox,
160    /// Content box.
161    ContentBox,
162    /// Object bounding box for vector content.
163    FillBox,
164    /// Stroke bounding box for vector content.
165    StrokeBox,
166    /// Nearest vector viewport box.
167    ViewBox,
168}
169
170impl ToCss for ClipBox {
171    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
172        dest.write_str(match self {
173            Self::BorderBox => "border-box",
174            Self::PaddingBox => "padding-box",
175            Self::ContentBox => "content-box",
176            Self::FillBox => "fill-box",
177            Self::StrokeBox => "stroke-box",
178            Self::ViewBox => "view-box",
179        })
180    }
181}
182
183/// Fill rule used by a clip path.
184#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
185pub enum ClipFillRule {
186    /// Non-zero winding rule.
187    #[default]
188    NonZero,
189    /// Even-odd winding rule.
190    EvenOdd,
191}
192
193/// One point in a structured clip path.
194#[derive(Clone, Debug, PartialEq)]
195pub struct ClipPoint {
196    /// Horizontal coordinate.
197    pub x: LengthPercentage,
198    /// Vertical coordinate.
199    pub y: LengthPercentage,
200}
201
202impl ClipPoint {
203    /// Creates a point.
204    pub fn new(x: impl Into<LengthPercentage>, y: impl Into<LengthPercentage>) -> Self {
205        Self {
206            x: x.into(),
207            y: y.into(),
208        }
209    }
210}
211
212/// One command in a structured clip path.
213#[derive(Clone, Debug, PartialEq)]
214pub enum ClipPathCommand {
215    /// Start a subpath.
216    MoveTo(ClipPoint),
217    /// Add a line.
218    LineTo(ClipPoint),
219    /// Add a quadratic Bezier segment.
220    QuadraticTo {
221        /// Quadratic control point.
222        control: ClipPoint,
223        /// Segment end point.
224        end: ClipPoint,
225    },
226    /// Add a cubic Bezier segment.
227    CubicTo {
228        /// First cubic control point.
229        control_1: ClipPoint,
230        /// Second cubic control point.
231        control_2: ClipPoint,
232        /// Segment end point.
233        end: ClipPoint,
234    },
235    /// Close the current subpath.
236    Close,
237}
238
239/// Typed `clip-path` value.
240#[derive(Clone, Debug, PartialEq)]
241pub enum ClipPath {
242    /// Disable clipping.
243    None,
244    /// Inset rectangle.
245    Inset {
246        /// Coordinate box used to resolve percentages.
247        reference_box: ClipBox,
248        /// Top, right, bottom, and left inset offsets.
249        offsets: [LengthPercentage; 4],
250        /// Optional corner radii.
251        radii: Option<BorderRadius>,
252    },
253    /// Circle.
254    Circle {
255        /// Coordinate box used to resolve percentages.
256        reference_box: ClipBox,
257        /// Circle radius.
258        radius: LengthPercentage,
259        /// Horizontal center coordinate.
260        center_x: LengthPercentage,
261        /// Vertical center coordinate.
262        center_y: LengthPercentage,
263    },
264    /// Ellipse.
265    Ellipse {
266        /// Coordinate box used to resolve percentages.
267        reference_box: ClipBox,
268        /// Horizontal radius.
269        radius_x: LengthPercentage,
270        /// Vertical radius.
271        radius_y: LengthPercentage,
272        /// Horizontal center coordinate.
273        center_x: LengthPercentage,
274        /// Vertical center coordinate.
275        center_y: LengthPercentage,
276    },
277    /// Structured path.
278    Path {
279        /// Coordinate box used to resolve percentages.
280        reference_box: ClipBox,
281        /// Fill rule applied to the path.
282        fill_rule: ClipFillRule,
283        /// Ordered path commands.
284        commands: Vec<ClipPathCommand>,
285    },
286}
287
288impl ClipPath {
289    /// Creates a centered circle against the border box.
290    pub fn circle(radius: impl Into<LengthPercentage>) -> Self {
291        Self::Circle {
292            reference_box: ClipBox::BorderBox,
293            radius: radius.into(),
294            center_x: Percentage::new(50.0).into(),
295            center_y: Percentage::new(50.0).into(),
296        }
297    }
298
299    /// Creates a centered ellipse against the border box.
300    pub fn ellipse(
301        radius_x: impl Into<LengthPercentage>,
302        radius_y: impl Into<LengthPercentage>,
303    ) -> Self {
304        Self::Ellipse {
305            reference_box: ClipBox::BorderBox,
306            radius_x: radius_x.into(),
307            radius_y: radius_y.into(),
308            center_x: Percentage::new(50.0).into(),
309            center_y: Percentage::new(50.0).into(),
310        }
311    }
312
313    /// Creates an inset rectangle against the border box.
314    pub fn inset(offsets: [LengthPercentage; 4]) -> Self {
315        Self::Inset {
316            reference_box: ClipBox::BorderBox,
317            offsets,
318            radii: None,
319        }
320    }
321
322    /// Replaces the reference box of this shape.
323    pub fn with_reference_box(mut self, reference_box: ClipBox) -> Self {
324        match &mut self {
325            Self::None => {}
326            Self::Inset {
327                reference_box: box_value,
328                ..
329            }
330            | Self::Circle {
331                reference_box: box_value,
332                ..
333            }
334            | Self::Ellipse {
335                reference_box: box_value,
336                ..
337            }
338            | Self::Path {
339                reference_box: box_value,
340                ..
341            } => *box_value = reference_box,
342        }
343        self
344    }
345}
346
347impl ToCss for ClipPath {
348    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
349        fn point(dest: &mut dyn fmt::Write, point: &ClipPoint) -> fmt::Result {
350            point.x.to_css(dest)?;
351            dest.write_char(' ')?;
352            point.y.to_css(dest)
353        }
354
355        let reference_box = match self {
356            Self::None => return dest.write_str("none"),
357            Self::Inset {
358                reference_box,
359                offsets,
360                radii,
361            } => {
362                dest.write_str("inset(")?;
363                write_four(dest, offsets)?;
364                if let Some(radii) = radii {
365                    dest.write_str(" round ")?;
366                    radii.to_css(dest)?;
367                }
368                dest.write_char(')')?;
369                reference_box
370            }
371            Self::Circle {
372                reference_box,
373                radius,
374                center_x,
375                center_y,
376            } => {
377                dest.write_str("circle(")?;
378                radius.to_css(dest)?;
379                dest.write_str(" at ")?;
380                center_x.to_css(dest)?;
381                dest.write_char(' ')?;
382                center_y.to_css(dest)?;
383                dest.write_char(')')?;
384                reference_box
385            }
386            Self::Ellipse {
387                reference_box,
388                radius_x,
389                radius_y,
390                center_x,
391                center_y,
392            } => {
393                dest.write_str("ellipse(")?;
394                radius_x.to_css(dest)?;
395                dest.write_char(' ')?;
396                radius_y.to_css(dest)?;
397                dest.write_str(" at ")?;
398                center_x.to_css(dest)?;
399                dest.write_char(' ')?;
400                center_y.to_css(dest)?;
401                dest.write_char(')')?;
402                reference_box
403            }
404            Self::Path {
405                reference_box,
406                fill_rule,
407                commands,
408            } => {
409                dest.write_str("path(")?;
410                if matches!(fill_rule, ClipFillRule::EvenOdd) {
411                    dest.write_str("evenodd, ")?;
412                }
413                for (index, command) in commands.iter().enumerate() {
414                    if index > 0 {
415                        dest.write_char(' ')?;
416                    }
417                    match command {
418                        ClipPathCommand::MoveTo(value) => {
419                            dest.write_str("M ")?;
420                            point(dest, value)?;
421                        }
422                        ClipPathCommand::LineTo(value) => {
423                            dest.write_str("L ")?;
424                            point(dest, value)?;
425                        }
426                        ClipPathCommand::QuadraticTo { control, end } => {
427                            dest.write_str("Q ")?;
428                            point(dest, control)?;
429                            dest.write_char(' ')?;
430                            point(dest, end)?;
431                        }
432                        ClipPathCommand::CubicTo {
433                            control_1,
434                            control_2,
435                            end,
436                        } => {
437                            dest.write_str("C ")?;
438                            point(dest, control_1)?;
439                            dest.write_char(' ')?;
440                            point(dest, control_2)?;
441                            dest.write_char(' ')?;
442                            point(dest, end)?;
443                        }
444                        ClipPathCommand::Close => dest.write_char('Z')?,
445                    }
446                }
447                dest.write_char(')')?;
448                reference_box
449            }
450        };
451        dest.write_char(' ')?;
452        reference_box.to_css(dest)
453    }
454}
455
456// ---------- Motion path ----------
457
458/// One absolute point in an `offset-path: path()` value.
459#[derive(Clone, Copy, Debug, PartialEq)]
460pub struct MotionPathPoint {
461    /// Horizontal logical-pixel coordinate.
462    pub x: f32,
463    /// Vertical logical-pixel coordinate.
464    pub y: f32,
465}
466
467impl MotionPathPoint {
468    /// Creates an absolute motion-path point.
469    pub const fn new(x: f32, y: f32) -> Self {
470        Self { x, y }
471    }
472}
473
474/// One command in an absolute SVG `offset-path: path()` value.
475#[derive(Clone, Copy, Debug, PartialEq)]
476pub enum MotionPathCommand {
477    /// Start a new subpath.
478    MoveTo(MotionPathPoint),
479    /// Add a straight segment.
480    LineTo(MotionPathPoint),
481    /// Add a quadratic Bezier segment.
482    QuadraticTo {
483        /// Curve control point.
484        control: MotionPathPoint,
485        /// Segment endpoint.
486        to: MotionPathPoint,
487    },
488    /// Add a cubic Bezier segment.
489    CubicTo {
490        /// First curve control point.
491        control1: MotionPathPoint,
492        /// Second curve control point.
493        control2: MotionPathPoint,
494        /// Segment endpoint.
495        to: MotionPathPoint,
496    },
497    /// Add an absolute SVG elliptical arc segment.
498    ArcTo {
499        /// Horizontal ellipse radius.
500        radius_x: f32,
501        /// Vertical ellipse radius.
502        radius_y: f32,
503        /// Clockwise rotation of the ellipse x axis, in degrees.
504        x_axis_rotation: f32,
505        /// Select the arc spanning at least 180 degrees.
506        large_arc: bool,
507        /// Sweep through increasing angles.
508        sweep: bool,
509        /// Segment endpoint.
510        to: MotionPathPoint,
511    },
512    /// Close the current subpath.
513    Close,
514}
515
516/// A typed `inset()` motion path.
517#[derive(Clone, Debug, PartialEq)]
518pub struct InsetPath {
519    /// Top, right, bottom, and left offsets from the border box.
520    pub offsets: [LengthPercentage; 4],
521    /// Optional per-corner radii in CSS border-radius order.
522    pub radii: Option<BorderRadius>,
523}
524
525/// Supported `offset-path` value.
526#[derive(Clone, Debug, PartialEq)]
527pub enum OffsetPath {
528    /// Disable motion-path positioning.
529    None,
530    /// Follow an absolute SVG path.
531    Path(Vec<MotionPathCommand>),
532    /// Follow a circle resolved against the node border box.
533    Circle {
534        /// Radius.
535        radius: LengthPercentage,
536        /// Horizontal center position.
537        center_x: LengthPercentage,
538        /// Vertical center position.
539        center_y: LengthPercentage,
540    },
541    /// Follow an ellipse resolved against the node border box.
542    Ellipse {
543        /// Horizontal radius.
544        radius_x: LengthPercentage,
545        /// Vertical radius.
546        radius_y: LengthPercentage,
547        /// Horizontal center position.
548        center_x: LengthPercentage,
549        /// Vertical center position.
550        center_y: LengthPercentage,
551    },
552    /// Follow a possibly-rounded rectangle inset from the node border box.
553    Inset(Box<InsetPath>),
554}
555
556impl OffsetPath {
557    /// Creates a `path()` from absolute SVG commands.
558    pub fn path(commands: impl Into<Vec<MotionPathCommand>>) -> Self {
559        Self::Path(commands.into())
560    }
561
562    /// Creates a centered `circle()` motion path.
563    pub fn circle(radius: impl Into<LengthPercentage>) -> Self {
564        Self::circle_at(radius, Percentage::new(50.0), Percentage::new(50.0))
565    }
566
567    /// Creates a positioned `circle()` motion path.
568    pub fn circle_at(
569        radius: impl Into<LengthPercentage>,
570        center_x: impl Into<LengthPercentage>,
571        center_y: impl Into<LengthPercentage>,
572    ) -> Self {
573        Self::Circle {
574            radius: radius.into(),
575            center_x: center_x.into(),
576            center_y: center_y.into(),
577        }
578    }
579
580    /// Creates a centered `ellipse()` motion path.
581    pub fn ellipse(
582        radius_x: impl Into<LengthPercentage>,
583        radius_y: impl Into<LengthPercentage>,
584    ) -> Self {
585        Self::ellipse_at(
586            radius_x,
587            radius_y,
588            Percentage::new(50.0),
589            Percentage::new(50.0),
590        )
591    }
592
593    /// Creates a positioned `ellipse()` motion path.
594    pub fn ellipse_at(
595        radius_x: impl Into<LengthPercentage>,
596        radius_y: impl Into<LengthPercentage>,
597        center_x: impl Into<LengthPercentage>,
598        center_y: impl Into<LengthPercentage>,
599    ) -> Self {
600        Self::Ellipse {
601            radius_x: radius_x.into(),
602            radius_y: radius_y.into(),
603            center_x: center_x.into(),
604            center_y: center_y.into(),
605        }
606    }
607
608    /// Creates a rectangular `inset()` motion path.
609    pub fn inset(
610        top: impl Into<LengthPercentage>,
611        right: impl Into<LengthPercentage>,
612        bottom: impl Into<LengthPercentage>,
613        left: impl Into<LengthPercentage>,
614    ) -> Self {
615        Self::Inset(Box::new(InsetPath {
616            offsets: [top.into(), right.into(), bottom.into(), left.into()],
617            radii: None,
618        }))
619    }
620
621    /// Creates a rounded `inset()` motion path.
622    pub fn inset_round(
623        top: impl Into<LengthPercentage>,
624        right: impl Into<LengthPercentage>,
625        bottom: impl Into<LengthPercentage>,
626        left: impl Into<LengthPercentage>,
627        radii: BorderRadius,
628    ) -> Self {
629        Self::Inset(Box::new(InsetPath {
630            offsets: [top.into(), right.into(), bottom.into(), left.into()],
631            radii: Some(radii),
632        }))
633    }
634}
635
636impl ToCss for OffsetPath {
637    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
638        match self {
639            Self::None => dest.write_str("none"),
640            Self::Path(commands) => {
641                dest.write_str("path(\"")?;
642                for (index, command) in commands.iter().enumerate() {
643                    if index > 0 {
644                        dest.write_char(' ')?;
645                    }
646                    match command {
647                        MotionPathCommand::MoveTo(point) => {
648                            dest.write_str("M ")?;
649                            write_number(dest, point.x)?;
650                            dest.write_char(' ')?;
651                            write_number(dest, point.y)?;
652                        }
653                        MotionPathCommand::LineTo(point) => {
654                            dest.write_str("L ")?;
655                            write_number(dest, point.x)?;
656                            dest.write_char(' ')?;
657                            write_number(dest, point.y)?;
658                        }
659                        MotionPathCommand::QuadraticTo { control, to } => {
660                            dest.write_str("Q ")?;
661                            write_number(dest, control.x)?;
662                            dest.write_char(' ')?;
663                            write_number(dest, control.y)?;
664                            dest.write_char(' ')?;
665                            write_number(dest, to.x)?;
666                            dest.write_char(' ')?;
667                            write_number(dest, to.y)?;
668                        }
669                        MotionPathCommand::CubicTo {
670                            control1,
671                            control2,
672                            to,
673                        } => {
674                            dest.write_str("C ")?;
675                            write_number(dest, control1.x)?;
676                            dest.write_char(' ')?;
677                            write_number(dest, control1.y)?;
678                            dest.write_char(' ')?;
679                            write_number(dest, control2.x)?;
680                            dest.write_char(' ')?;
681                            write_number(dest, control2.y)?;
682                            dest.write_char(' ')?;
683                            write_number(dest, to.x)?;
684                            dest.write_char(' ')?;
685                            write_number(dest, to.y)?;
686                        }
687                        MotionPathCommand::ArcTo {
688                            radius_x,
689                            radius_y,
690                            x_axis_rotation,
691                            large_arc,
692                            sweep,
693                            to,
694                        } => {
695                            dest.write_str("A ")?;
696                            write_number(dest, *radius_x)?;
697                            dest.write_char(' ')?;
698                            write_number(dest, *radius_y)?;
699                            dest.write_char(' ')?;
700                            write_number(dest, *x_axis_rotation)?;
701                            dest.write_char(' ')?;
702                            dest.write_char(if *large_arc { '1' } else { '0' })?;
703                            dest.write_char(' ')?;
704                            dest.write_char(if *sweep { '1' } else { '0' })?;
705                            dest.write_char(' ')?;
706                            write_number(dest, to.x)?;
707                            dest.write_char(' ')?;
708                            write_number(dest, to.y)?;
709                        }
710                        MotionPathCommand::Close => dest.write_char('Z')?,
711                    }
712                }
713                dest.write_str("\")")
714            }
715            Self::Circle {
716                radius,
717                center_x,
718                center_y,
719            } => {
720                dest.write_str("circle(")?;
721                radius.to_css(dest)?;
722                dest.write_str(" at ")?;
723                center_x.to_css(dest)?;
724                dest.write_char(' ')?;
725                center_y.to_css(dest)?;
726                dest.write_char(')')
727            }
728            Self::Ellipse {
729                radius_x,
730                radius_y,
731                center_x,
732                center_y,
733            } => {
734                dest.write_str("ellipse(")?;
735                radius_x.to_css(dest)?;
736                dest.write_char(' ')?;
737                radius_y.to_css(dest)?;
738                dest.write_str(" at ")?;
739                center_x.to_css(dest)?;
740                dest.write_char(' ')?;
741                center_y.to_css(dest)?;
742                dest.write_char(')')
743            }
744            Self::Inset(value) => {
745                dest.write_str("inset(")?;
746                write_four(dest, &value.offsets)?;
747                if let Some(radii) = &value.radii {
748                    dest.write_str(" round ")?;
749                    radii.to_css(dest)?;
750                }
751                dest.write_char(')')
752            }
753        }
754    }
755}
756
757/// Supported `offset-distance` value.
758#[derive(Clone, Copy, Debug, PartialEq)]
759pub enum OffsetDistance {
760    /// Unitless normalized progress in the `0..=1` range.
761    Number(Number),
762    /// Percentage progress in the `0%..=100%` range.
763    Percentage(Percentage),
764}
765
766impl From<Number> for OffsetDistance {
767    fn from(value: Number) -> Self {
768        Self::Number(value)
769    }
770}
771
772impl From<Percentage> for OffsetDistance {
773    fn from(value: Percentage) -> Self {
774        Self::Percentage(value)
775    }
776}
777
778impl ToCss for OffsetDistance {
779    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
780        match self {
781            Self::Number(value) => value.to_css(dest),
782            Self::Percentage(value) => value.to_css(dest),
783        }
784    }
785}
786
787/// Supported `offset-rotate` value.
788#[derive(Clone, Copy, Debug, PartialEq)]
789pub enum OffsetRotate {
790    /// Follow the path tangent.
791    Auto,
792    /// Use a fixed clockwise angle.
793    Angle(crate::Angle),
794}
795
796impl ToCss for OffsetRotate {
797    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
798        match self {
799            Self::Auto => dest.write_str("auto"),
800            Self::Angle(angle) => angle.to_css(dest),
801        }
802    }
803}
804
805// ---------- Size (width / height / min-/max-) ----------
806
807/// Value for `width`, `height`, `min-width`, `min-height`,
808/// `max-width`, `max-height`.
809#[derive(Clone, Debug, PartialEq)]
810pub enum Size {
811    /// `auto` — let the layout algorithm choose.
812    Auto,
813    /// An explicit length or percentage.
814    LengthPercentage(LengthPercentage),
815    /// `max-content` — the maximum intrinsic content size.
816    MaxContent,
817    /// `min-content` — the minimum intrinsic content size.
818    MinContent,
819    /// `fit-content` (or `fit-content(<limit>)`).
820    FitContent(FitContent),
821    /// `none` — only valid for `max-*` properties.
822    None,
823}
824
825impl ToCss for Size {
826    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
827        match self {
828            Size::Auto => dest.write_str("auto"),
829            Size::LengthPercentage(lp) => lp.to_css(dest),
830            Size::MaxContent => dest.write_str("max-content"),
831            Size::MinContent => dest.write_str("min-content"),
832            Size::FitContent(fc) => fc.to_css(dest),
833            Size::None => dest.write_str("none"),
834        }
835    }
836}
837
838impl From<Length> for Size {
839    fn from(l: Length) -> Self {
840        Self::LengthPercentage(l.into())
841    }
842}
843
844impl From<Percentage> for Size {
845    fn from(p: Percentage) -> Self {
846        Self::LengthPercentage(p.into())
847    }
848}
849
850impl From<LengthPercentage> for Size {
851    fn from(lp: LengthPercentage) -> Self {
852        Self::LengthPercentage(lp)
853    }
854}
855
856impl From<MaxContent> for Size {
857    fn from(_: MaxContent) -> Self {
858        Self::MaxContent
859    }
860}
861
862impl From<FitContent> for Size {
863    fn from(fc: FitContent) -> Self {
864        Self::FitContent(fc)
865    }
866}
867
868// ---------- FlexBasis ----------
869
870/// Value for `flex-basis`.
871#[derive(Clone, Debug, PartialEq)]
872pub enum FlexBasis {
873    /// `auto` — basis comes from the item's `width`/`height`.
874    Auto,
875    /// `content` — basis is the content size.
876    Content,
877    /// An explicit length or percentage.
878    LengthPercentage(LengthPercentage),
879}
880
881impl ToCss for FlexBasis {
882    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
883        match self {
884            FlexBasis::Auto => dest.write_str("auto"),
885            FlexBasis::Content => dest.write_str("content"),
886            FlexBasis::LengthPercentage(lp) => lp.to_css(dest),
887        }
888    }
889}
890
891impl From<Length> for FlexBasis {
892    fn from(l: Length) -> Self {
893        Self::LengthPercentage(l.into())
894    }
895}
896
897impl From<Percentage> for FlexBasis {
898    fn from(p: Percentage) -> Self {
899        Self::LengthPercentage(p.into())
900    }
901}
902
903impl From<LengthPercentage> for FlexBasis {
904    fn from(lp: LengthPercentage) -> Self {
905        Self::LengthPercentage(lp)
906    }
907}
908
909// ---------- LineHeight ----------
910
911/// Value for `line-height`.
912#[derive(Clone, Debug, PartialEq)]
913pub enum LineHeight {
914    /// `normal` — engine-chosen line height.
915    Normal,
916    /// Unit-less multiplier of the element's `font-size`.
917    Number(f32),
918    /// Explicit length or percentage.
919    LengthPercentage(LengthPercentage),
920}
921
922impl ToCss for LineHeight {
923    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
924        match self {
925            LineHeight::Normal => dest.write_str("normal"),
926            LineHeight::Number(n) => write_number(dest, *n),
927            LineHeight::LengthPercentage(lp) => lp.to_css(dest),
928        }
929    }
930}
931
932impl From<Length> for LineHeight {
933    fn from(l: Length) -> Self {
934        Self::LengthPercentage(l.into())
935    }
936}
937
938impl From<Percentage> for LineHeight {
939    fn from(p: Percentage) -> Self {
940        Self::LengthPercentage(p.into())
941    }
942}
943
944impl From<LengthPercentage> for LineHeight {
945    fn from(lp: LengthPercentage) -> Self {
946        Self::LengthPercentage(lp)
947    }
948}
949
950impl From<f32> for LineHeight {
951    fn from(v: f32) -> Self {
952        Self::Number(v)
953    }
954}
955
956// ---------- ImageRef (background-image, etc.) ----------
957
958/// A reference to an image resource. Lynx accepts `url("...")`,
959/// `linear-gradient(...)`, and `radial-gradient(...)`. `conic-gradient`
960/// is supported on background-image but represented via [`crate::Gradient`].
961#[derive(Clone, Debug, PartialEq)]
962pub enum ImageRef {
963    /// `none` — no image.
964    None,
965    /// `url("<path>")`.
966    Url(CssString),
967    /// One of the `<gradient>` functions.
968    Gradient(crate::Gradient),
969}
970
971impl ToCss for ImageRef {
972    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
973        match self {
974            ImageRef::None => dest.write_str("none"),
975            ImageRef::Url(s) => {
976                dest.write_str("url(")?;
977                s.to_css(dest)?;
978                dest.write_char(')')
979            }
980            ImageRef::Gradient(g) => g.to_css(dest),
981        }
982    }
983}
984
985impl From<crate::Gradient> for ImageRef {
986    fn from(g: crate::Gradient) -> Self {
987        Self::Gradient(g)
988    }
989}
990
991// ---------- BorderRadius (4 corners + optional elliptical y) ----------
992
993/// Value for the `border-radius` shorthand. Stores per-corner
994/// radii, optionally with an elliptical second axis.
995#[derive(Clone, Debug, PartialEq)]
996pub struct BorderRadius {
997    /// Horizontal radii: top-left, top-right, bottom-right, bottom-left.
998    pub horizontal: [LengthPercentage; 4],
999    /// Optional vertical radii for an elliptical corner.
1000    pub vertical: Option<[LengthPercentage; 4]>,
1001}
1002
1003impl BorderRadius {
1004    /// All four corners share the same radius.
1005    pub fn all(v: impl Into<LengthPercentage>) -> Self {
1006        let v = v.into();
1007        Self {
1008            horizontal: [v.clone(), v.clone(), v.clone(), v],
1009            vertical: None,
1010        }
1011    }
1012
1013    /// Specify each corner explicitly (top-left, top-right, bottom-right, bottom-left).
1014    pub fn corners(
1015        tl: impl Into<LengthPercentage>,
1016        tr: impl Into<LengthPercentage>,
1017        br: impl Into<LengthPercentage>,
1018        bl: impl Into<LengthPercentage>,
1019    ) -> Self {
1020        Self {
1021            horizontal: [tl.into(), tr.into(), br.into(), bl.into()],
1022            vertical: None,
1023        }
1024    }
1025
1026    /// Elliptical radius: horizontal and vertical components.
1027    pub fn elliptical(horizontal: [LengthPercentage; 4], vertical: [LengthPercentage; 4]) -> Self {
1028        Self {
1029            horizontal,
1030            vertical: Some(vertical),
1031        }
1032    }
1033}
1034
1035impl ToCss for BorderRadius {
1036    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1037        write_four(dest, &self.horizontal)?;
1038        if let Some(v) = &self.vertical {
1039            dest.write_str(" / ")?;
1040            write_four(dest, v)?;
1041        }
1042        Ok(())
1043    }
1044}
1045
1046fn write_four(dest: &mut dyn fmt::Write, v: &[LengthPercentage; 4]) -> fmt::Result {
1047    for (i, item) in v.iter().enumerate() {
1048        if i > 0 {
1049            dest.write_char(' ')?;
1050        }
1051        item.to_css(dest)?;
1052    }
1053    Ok(())
1054}
1055
1056// ---------- CSS Grid ----------
1057
1058/// Value for `grid-row-start`, `grid-row-end`, `grid-column-start`,
1059/// `grid-column-end`. Lynx accepts numeric line references and
1060/// `span <integer>`.
1061#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1062pub enum GridLine {
1063    /// `auto` — let the layout algorithm decide.
1064    Auto,
1065    /// Numeric line reference; negative values count from the end.
1066    Number(i16),
1067    /// `span <integer>` — span N tracks from the opposite edge.
1068    Span(u16),
1069    /// A named line, optionally selecting the nth occurrence.
1070    Named(String, i16),
1071    /// Span to a named line.
1072    NamedSpan(String, u16),
1073}
1074
1075impl ToCss for GridLine {
1076    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1077        match self {
1078            GridLine::Auto => dest.write_str("auto"),
1079            GridLine::Number(n) => write!(dest, "{n}"),
1080            GridLine::Span(n) => write!(dest, "span {n}"),
1081            GridLine::Named(name, occurrence) if *occurrence == 0 => dest.write_str(name),
1082            GridLine::Named(name, occurrence) => write!(dest, "{occurrence} {name}"),
1083            GridLine::NamedSpan(name, occurrence) if *occurrence == 0 => {
1084                write!(dest, "span {name}")
1085            }
1086            GridLine::NamedSpan(name, occurrence) => write!(dest, "span {occurrence} {name}"),
1087        }
1088    }
1089}
1090
1091/// Minimum sizing function accepted by `minmax()`.
1092#[derive(Clone, Debug, PartialEq)]
1093pub enum GridTrackMin {
1094    /// A fixed length or percentage.
1095    Fixed(LengthPercentage),
1096    /// The minimum intrinsic contribution.
1097    MinContent,
1098    /// The maximum intrinsic contribution.
1099    MaxContent,
1100    /// Automatic minimum sizing.
1101    Auto,
1102}
1103
1104/// Maximum sizing function accepted by `minmax()`.
1105#[derive(Clone, Debug, PartialEq)]
1106pub enum GridTrackMax {
1107    /// A fixed length or percentage.
1108    Fixed(LengthPercentage),
1109    /// The minimum intrinsic contribution.
1110    MinContent,
1111    /// The maximum intrinsic contribution.
1112    MaxContent,
1113    /// `fit-content(<limit>)`.
1114    FitContent(LengthPercentage),
1115    /// Automatic maximum sizing.
1116    Auto,
1117    /// A flexible share in `fr` units.
1118    Fraction(f32),
1119}
1120
1121/// One CSS Grid track sizing function.
1122#[derive(Clone, Debug, PartialEq)]
1123pub struct GridTrack {
1124    pub(crate) min: GridTrackMin,
1125    pub(crate) max: GridTrackMax,
1126}
1127
1128impl GridTrack {
1129    /// `auto`.
1130    pub const fn auto() -> Self {
1131        Self {
1132            min: GridTrackMin::Auto,
1133            max: GridTrackMax::Auto,
1134        }
1135    }
1136
1137    /// `min-content`.
1138    pub const fn min_content() -> Self {
1139        Self {
1140            min: GridTrackMin::MinContent,
1141            max: GridTrackMax::MinContent,
1142        }
1143    }
1144
1145    /// `max-content`.
1146    pub const fn max_content() -> Self {
1147        Self {
1148            min: GridTrackMin::MaxContent,
1149            max: GridTrackMax::MaxContent,
1150        }
1151    }
1152
1153    /// A fixed length or percentage.
1154    pub fn fixed(value: impl Into<LengthPercentage>) -> Self {
1155        let value = value.into();
1156        Self {
1157            min: GridTrackMin::Fixed(value.clone()),
1158            max: GridTrackMax::Fixed(value),
1159        }
1160    }
1161
1162    /// A flexible `fr` track.
1163    pub const fn fraction(value: f32) -> Self {
1164        Self {
1165            min: GridTrackMin::Auto,
1166            max: GridTrackMax::Fraction(value),
1167        }
1168    }
1169
1170    /// `fit-content(<limit>)`.
1171    pub fn fit_content(limit: impl Into<LengthPercentage>) -> Self {
1172        Self {
1173            min: GridTrackMin::Auto,
1174            max: GridTrackMax::FitContent(limit.into()),
1175        }
1176    }
1177
1178    /// `minmax(<min>, <max>)`.
1179    pub const fn minmax(min: GridTrackMin, max: GridTrackMax) -> Self {
1180        Self { min, max }
1181    }
1182}
1183
1184impl From<Length> for GridTrack {
1185    fn from(value: Length) -> Self {
1186        Self::fixed(value)
1187    }
1188}
1189
1190impl From<Percentage> for GridTrack {
1191    fn from(value: Percentage) -> Self {
1192        Self::fixed(value)
1193    }
1194}
1195
1196impl From<LengthPercentage> for GridTrack {
1197    fn from(value: LengthPercentage) -> Self {
1198        Self::fixed(value)
1199    }
1200}
1201
1202impl From<Length> for GridTrackMin {
1203    fn from(value: Length) -> Self {
1204        Self::Fixed(value.into())
1205    }
1206}
1207
1208impl From<Percentage> for GridTrackMin {
1209    fn from(value: Percentage) -> Self {
1210        Self::Fixed(value.into())
1211    }
1212}
1213
1214impl From<LengthPercentage> for GridTrackMin {
1215    fn from(value: LengthPercentage) -> Self {
1216        Self::Fixed(value)
1217    }
1218}
1219
1220impl From<Length> for GridTrackMax {
1221    fn from(value: Length) -> Self {
1222        Self::Fixed(value.into())
1223    }
1224}
1225
1226impl From<Percentage> for GridTrackMax {
1227    fn from(value: Percentage) -> Self {
1228        Self::Fixed(value.into())
1229    }
1230}
1231
1232impl From<LengthPercentage> for GridTrackMax {
1233    fn from(value: LengthPercentage) -> Self {
1234        Self::Fixed(value)
1235    }
1236}
1237
1238impl ToCss for GridTrack {
1239    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1240        match (&self.min, &self.max) {
1241            (GridTrackMin::Auto, GridTrackMax::Auto) => dest.write_str("auto"),
1242            (GridTrackMin::MinContent, GridTrackMax::MinContent) => dest.write_str("min-content"),
1243            (GridTrackMin::MaxContent, GridTrackMax::MaxContent) => dest.write_str("max-content"),
1244            (GridTrackMin::Fixed(min), GridTrackMax::Fixed(max)) if min == max => min.to_css(dest),
1245            (GridTrackMin::Auto, GridTrackMax::Fraction(value)) => {
1246                write_number(dest, *value)?;
1247                dest.write_str("fr")
1248            }
1249            (GridTrackMin::Auto, GridTrackMax::FitContent(limit)) => {
1250                dest.write_str("fit-content(")?;
1251                limit.to_css(dest)?;
1252                dest.write_char(')')
1253            }
1254            (min, max) => {
1255                dest.write_str("minmax(")?;
1256                min.to_css(dest)?;
1257                dest.write_str(", ")?;
1258                max.to_css(dest)?;
1259                dest.write_char(')')
1260            }
1261        }
1262    }
1263}
1264
1265impl ToCss for GridTrackMin {
1266    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1267        match self {
1268            Self::Fixed(value) => value.to_css(dest),
1269            Self::MinContent => dest.write_str("min-content"),
1270            Self::MaxContent => dest.write_str("max-content"),
1271            Self::Auto => dest.write_str("auto"),
1272        }
1273    }
1274}
1275
1276impl ToCss for GridTrackMax {
1277    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1278        match self {
1279            Self::Fixed(value) => value.to_css(dest),
1280            Self::MinContent => dest.write_str("min-content"),
1281            Self::MaxContent => dest.write_str("max-content"),
1282            Self::FitContent(limit) => {
1283                dest.write_str("fit-content(")?;
1284                limit.to_css(dest)?;
1285                dest.write_char(')')
1286            }
1287            Self::Auto => dest.write_str("auto"),
1288            Self::Fraction(value) => {
1289                write_number(dest, *value)?;
1290                dest.write_str("fr")
1291            }
1292        }
1293    }
1294}
1295
1296/// Count used by a Grid `repeat()` fragment.
1297#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
1298pub enum GridRepeatCount {
1299    /// Repeat a fixed number of times.
1300    Count(u16),
1301    /// Fill the available axis while retaining empty repeated tracks.
1302    AutoFill,
1303    /// Fill the available axis and collapse empty repeated tracks.
1304    AutoFit,
1305}
1306
1307/// One explicit track or repeated track fragment.
1308#[derive(Clone, Debug, PartialEq)]
1309pub enum GridTemplateComponent {
1310    /// One track.
1311    Track(GridTrack),
1312    /// A repeated fragment.
1313    Repeat {
1314        /// Fixed or automatic repetition count.
1315        count: GridRepeatCount,
1316        /// Tracks inside the repeated fragment.
1317        tracks: Vec<GridTrack>,
1318        /// Named lines before, between, and after repeated tracks.
1319        line_names: Vec<Vec<String>>,
1320    },
1321}
1322
1323impl From<GridTrack> for GridTemplateComponent {
1324    fn from(value: GridTrack) -> Self {
1325        Self::Track(value)
1326    }
1327}
1328
1329/// Value for `grid-template-rows` / `grid-template-columns`.
1330#[derive(Clone, Debug, PartialEq)]
1331pub struct GridTemplate {
1332    pub(crate) components: Vec<GridTemplateComponent>,
1333    pub(crate) line_names: Vec<Vec<String>>,
1334}
1335
1336impl GridTemplate {
1337    /// Build from a list of track-sizing tokens. Each token is
1338    /// joined with a space.
1339    pub fn tracks(tracks: impl IntoIterator<Item = impl Into<GridTrack>>) -> Self {
1340        let components: Vec<_> = tracks
1341            .into_iter()
1342            .map(|track| track.into().into())
1343            .collect();
1344        let line_names = vec![Vec::new(); components.len() + 1];
1345        Self {
1346            components,
1347            line_names,
1348        }
1349    }
1350
1351    /// Build a template from explicit track and `repeat()` components.
1352    pub fn components(components: impl IntoIterator<Item = GridTemplateComponent>) -> Self {
1353        let components: Vec<_> = components.into_iter().collect();
1354        let line_names = vec![Vec::new(); components.len() + 1];
1355        Self {
1356            components,
1357            line_names,
1358        }
1359    }
1360
1361    /// Build a template containing one `repeat()` fragment.
1362    pub fn repeat(
1363        count: GridRepeatCount,
1364        tracks: impl IntoIterator<Item = impl Into<GridTrack>>,
1365    ) -> Self {
1366        let tracks: Vec<_> = tracks.into_iter().map(Into::into).collect();
1367        let line_names = vec![Vec::new(); tracks.len() + 1];
1368        Self::components([GridTemplateComponent::Repeat {
1369            count,
1370            tracks,
1371            line_names,
1372        }])
1373    }
1374
1375    /// Attach names to the lines before, between, and after components.
1376    /// Invalid line-name counts are rejected during style resolution.
1377    pub fn line_names(
1378        mut self,
1379        line_names: impl IntoIterator<Item = impl IntoIterator<Item = impl Into<String>>>,
1380    ) -> Self {
1381        self.line_names = line_names
1382            .into_iter()
1383            .map(|names| names.into_iter().map(Into::into).collect())
1384            .collect();
1385        self
1386    }
1387}
1388
1389impl ToCss for GridTemplate {
1390    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1391        for (index, component) in self.components.iter().enumerate() {
1392            if index > 0 {
1393                dest.write_char(' ')?;
1394            }
1395            write_grid_line_names(dest, self.line_names.get(index))?;
1396            if !self.line_names.get(index).is_none_or(Vec::is_empty) {
1397                dest.write_char(' ')?;
1398            }
1399            component.to_css(dest)?;
1400        }
1401        if !self
1402            .line_names
1403            .get(self.components.len())
1404            .is_none_or(Vec::is_empty)
1405        {
1406            if !self.components.is_empty() {
1407                dest.write_char(' ')?;
1408            }
1409            write_grid_line_names(dest, self.line_names.get(self.components.len()))?;
1410        }
1411        Ok(())
1412    }
1413}
1414
1415impl ToCss for GridTemplateComponent {
1416    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1417        match self {
1418            Self::Track(track) => track.to_css(dest),
1419            Self::Repeat {
1420                count,
1421                tracks,
1422                line_names,
1423            } => {
1424                dest.write_str("repeat(")?;
1425                count.to_css(dest)?;
1426                dest.write_str(", ")?;
1427                for (index, track) in tracks.iter().enumerate() {
1428                    if index > 0 {
1429                        dest.write_char(' ')?;
1430                    }
1431                    write_grid_line_names(dest, line_names.get(index))?;
1432                    if !line_names.get(index).is_none_or(Vec::is_empty) {
1433                        dest.write_char(' ')?;
1434                    }
1435                    track.to_css(dest)?;
1436                }
1437                if !line_names.get(tracks.len()).is_none_or(Vec::is_empty) {
1438                    if !tracks.is_empty() {
1439                        dest.write_char(' ')?;
1440                    }
1441                    write_grid_line_names(dest, line_names.get(tracks.len()))?;
1442                }
1443                dest.write_char(')')
1444            }
1445        }
1446    }
1447}
1448
1449impl ToCss for GridRepeatCount {
1450    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1451        match self {
1452            Self::Count(value) => write!(dest, "{value}"),
1453            Self::AutoFill => dest.write_str("auto-fill"),
1454            Self::AutoFit => dest.write_str("auto-fit"),
1455        }
1456    }
1457}
1458
1459fn write_grid_line_names(dest: &mut dyn fmt::Write, names: Option<&Vec<String>>) -> fmt::Result {
1460    let Some(names) = names.filter(|names| !names.is_empty()) else {
1461        return Ok(());
1462    };
1463    dest.write_char('[')?;
1464    for (index, name) in names.iter().enumerate() {
1465        if index > 0 {
1466            dest.write_char(' ')?;
1467        }
1468        dest.write_str(name)?;
1469    }
1470    dest.write_char(']')
1471}
1472
1473/// One rectangular named region in `grid-template-areas`.
1474#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1475pub struct GridArea {
1476    pub(crate) name: String,
1477    pub(crate) row_start: u16,
1478    pub(crate) row_end: u16,
1479    pub(crate) column_start: u16,
1480    pub(crate) column_end: u16,
1481}
1482
1483impl GridArea {
1484    /// Defines a zero-based, end-exclusive rectangular area.
1485    pub fn new(
1486        name: impl Into<String>,
1487        row_start: u16,
1488        row_end: u16,
1489        column_start: u16,
1490        column_end: u16,
1491    ) -> Self {
1492        Self {
1493            name: name.into(),
1494            row_start,
1495            row_end,
1496            column_start,
1497            column_end,
1498        }
1499    }
1500}
1501
1502/// Rectangular named regions for `grid-template-areas`.
1503#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1504pub struct GridTemplateAreas {
1505    pub(crate) row_count: u16,
1506    pub(crate) column_count: u16,
1507    pub(crate) areas: Vec<GridArea>,
1508}
1509
1510impl GridTemplateAreas {
1511    /// Creates an empty named-area matrix with explicit dimensions.
1512    pub const fn new(row_count: u16, column_count: u16) -> Self {
1513        Self {
1514            row_count,
1515            column_count,
1516            areas: Vec::new(),
1517        }
1518    }
1519
1520    /// Adds one named rectangular area.
1521    pub fn area(mut self, area: GridArea) -> Self {
1522        self.areas.push(area);
1523        self
1524    }
1525}
1526
1527impl ToCss for GridTemplateAreas {
1528    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1529        for row in 0..self.row_count {
1530            if row > 0 {
1531                dest.write_char(' ')?;
1532            }
1533            dest.write_char('"')?;
1534            for column in 0..self.column_count {
1535                if column > 0 {
1536                    dest.write_char(' ')?;
1537                }
1538                let name = self
1539                    .areas
1540                    .iter()
1541                    .rev()
1542                    .find(|area| {
1543                        area.row_start <= row
1544                            && row < area.row_end
1545                            && area.column_start <= column
1546                            && column < area.column_end
1547                    })
1548                    .map_or(".", |area| area.name.as_str());
1549                dest.write_str(name)?;
1550            }
1551            dest.write_char('"')?;
1552        }
1553        Ok(())
1554    }
1555}
1556
1557// ---------- Repeated (animation-name list etc.) ----------
1558
1559/// Comma-separated list of values, used for properties like
1560/// `animation-name`, `transition-property`, `background-image`.
1561#[derive(Clone, Debug, PartialEq)]
1562pub struct Repeated<T>(pub Vec<T>);
1563
1564impl<T> Repeated<T> {
1565    /// Wrap a `Vec<T>`.
1566    pub fn new(v: impl IntoIterator<Item = T>) -> Self {
1567        Self(v.into_iter().collect())
1568    }
1569}
1570
1571impl<T: ToCss> ToCss for Repeated<T> {
1572    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
1573        for (i, item) in self.0.iter().enumerate() {
1574            if i > 0 {
1575                dest.write_str(", ")?;
1576            }
1577            item.to_css(dest)?;
1578        }
1579        Ok(())
1580    }
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585    use super::*;
1586    use crate::data_type::{ColorStop, Gradient, Length, NamedColor};
1587    use crate::ext::*;
1588
1589    #[test]
1590    fn size_keywords() {
1591        assert_eq!(Size::Auto.to_css_string(), "auto");
1592        assert_eq!(Size::MaxContent.to_css_string(), "max-content");
1593        assert_eq!(Size::MinContent.to_css_string(), "min-content");
1594        assert_eq!(Size::None.to_css_string(), "none");
1595    }
1596
1597    #[test]
1598    fn size_from_lengths_and_percentages() {
1599        let from_len: Size = px(8).into();
1600        let from_pct: Size = 50.percent().into();
1601        let from_lp: Size = LengthPercentage::Length(Length::Px(4.0)).into();
1602        let from_mc: Size = MaxContent.into();
1603        let from_fc: Size = FitContent::keyword().into();
1604        assert_eq!(from_len.to_css_string(), "8px");
1605        assert_eq!(from_pct.to_css_string(), "50%");
1606        assert_eq!(from_lp.to_css_string(), "4px");
1607        assert_eq!(from_mc.to_css_string(), "max-content");
1608        assert_eq!(from_fc.to_css_string(), "fit-content");
1609    }
1610
1611    #[test]
1612    fn size_fit_content_with_limit() {
1613        let s = Size::FitContent(FitContent::with_limit(px(200)));
1614        assert_eq!(s.to_css_string(), "fit-content(200px)");
1615    }
1616
1617    #[test]
1618    fn flex_basis_variants() {
1619        assert_eq!(FlexBasis::Auto.to_css_string(), "auto");
1620        assert_eq!(FlexBasis::Content.to_css_string(), "content");
1621        let from_len: FlexBasis = px(120).into();
1622        let from_pct: FlexBasis = 25.percent().into();
1623        let from_lp: FlexBasis = LengthPercentage::Length(Length::Px(8.0)).into();
1624        assert_eq!(from_len.to_css_string(), "120px");
1625        assert_eq!(from_pct.to_css_string(), "25%");
1626        assert_eq!(from_lp.to_css_string(), "8px");
1627    }
1628
1629    #[test]
1630    fn line_height_variants() {
1631        assert_eq!(LineHeight::Normal.to_css_string(), "normal");
1632        let n: LineHeight = 1.5_f32.into();
1633        let from_len: LineHeight = px(20).into();
1634        let from_pct: LineHeight = 150.percent().into();
1635        let from_lp: LineHeight = LengthPercentage::Length(Length::Px(10.0)).into();
1636        assert_eq!(n.to_css_string(), "1.5");
1637        assert_eq!(from_len.to_css_string(), "20px");
1638        assert_eq!(from_pct.to_css_string(), "150%");
1639        assert_eq!(from_lp.to_css_string(), "10px");
1640    }
1641
1642    #[test]
1643    fn image_ref_variants() {
1644        assert_eq!(ImageRef::None.to_css_string(), "none");
1645        assert_eq!(
1646            ImageRef::Url(CssString::new("a.png")).to_css_string(),
1647            "url(\"a.png\")"
1648        );
1649        let g = Gradient::linear_to_bottom([ColorStop::new(crate::Color::Named(NamedColor::Red))]);
1650        let r: ImageRef = g.into();
1651        assert_eq!(r.to_css_string(), "linear-gradient(to bottom, red)");
1652    }
1653
1654    #[test]
1655    fn border_radius_uniform() {
1656        let r = BorderRadius::all(px(8));
1657        assert_eq!(r.to_css_string(), "8px 8px 8px 8px");
1658    }
1659
1660    #[test]
1661    fn border_radius_corners() {
1662        let r = BorderRadius::corners(px(2), px(4), px(6), px(8));
1663        assert_eq!(r.to_css_string(), "2px 4px 6px 8px");
1664    }
1665
1666    #[test]
1667    fn border_radius_elliptical() {
1668        let h = [px(2).into(), px(4).into(), px(6).into(), px(8).into()];
1669        let v = [px(20).into(), px(40).into(), px(60).into(), px(80).into()];
1670        let r = BorderRadius::elliptical(h, v);
1671        assert_eq!(r.to_css_string(), "2px 4px 6px 8px / 20px 40px 60px 80px");
1672    }
1673
1674    #[test]
1675    fn grid_line_variants() {
1676        assert_eq!(GridLine::Auto.to_css_string(), "auto");
1677        assert_eq!(GridLine::Number(1).to_css_string(), "1");
1678        assert_eq!(GridLine::Number(-1).to_css_string(), "-1");
1679        assert_eq!(GridLine::Span(2).to_css_string(), "span 2");
1680        assert_eq!(
1681            GridLine::Named("content".into(), 0).to_css_string(),
1682            "content"
1683        );
1684        assert_eq!(
1685            GridLine::NamedSpan("content".into(), 2).to_css_string(),
1686            "span 2 content"
1687        );
1688    }
1689
1690    #[test]
1691    fn grid_template_joins_tracks() {
1692        let t = GridTemplate::tracks([
1693            GridTrack::fraction(1.0),
1694            GridTrack::auto(),
1695            GridTrack::fraction(2.0),
1696        ]);
1697        assert_eq!(t.to_css_string(), "1fr auto 2fr");
1698    }
1699
1700    #[test]
1701    fn repeated_serializes_with_commas() {
1702        let r = Repeated::new([Length::Px(8.0), Length::Px(16.0)]);
1703        assert_eq!(r.to_css_string(), "8px, 16px");
1704    }
1705}