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::{CssString, FitContent, Length, LengthPercentage, MaxContent, Percentage};
14use crate::to_css::{write_number, ToCss};
15
16// ---------- Size (width / height / min-/max-) ----------
17
18/// Value for `width`, `height`, `min-width`, `min-height`,
19/// `max-width`, `max-height`.
20#[derive(Clone, Debug, PartialEq)]
21pub enum Size {
22    /// `auto` — let the layout algorithm choose.
23    Auto,
24    /// An explicit length or percentage.
25    LengthPercentage(LengthPercentage),
26    /// `max-content` — the maximum intrinsic content size.
27    MaxContent,
28    /// `min-content` — the minimum intrinsic content size.
29    MinContent,
30    /// `fit-content` (or `fit-content(<limit>)`).
31    FitContent(FitContent),
32    /// `none` — only valid for `max-*` properties.
33    None,
34}
35
36impl ToCss for Size {
37    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
38        match self {
39            Size::Auto => dest.write_str("auto"),
40            Size::LengthPercentage(lp) => lp.to_css(dest),
41            Size::MaxContent => dest.write_str("max-content"),
42            Size::MinContent => dest.write_str("min-content"),
43            Size::FitContent(fc) => fc.to_css(dest),
44            Size::None => dest.write_str("none"),
45        }
46    }
47}
48
49impl From<Length> for Size {
50    fn from(l: Length) -> Self {
51        Self::LengthPercentage(l.into())
52    }
53}
54
55impl From<Percentage> for Size {
56    fn from(p: Percentage) -> Self {
57        Self::LengthPercentage(p.into())
58    }
59}
60
61impl From<LengthPercentage> for Size {
62    fn from(lp: LengthPercentage) -> Self {
63        Self::LengthPercentage(lp)
64    }
65}
66
67impl From<MaxContent> for Size {
68    fn from(_: MaxContent) -> Self {
69        Self::MaxContent
70    }
71}
72
73impl From<FitContent> for Size {
74    fn from(fc: FitContent) -> Self {
75        Self::FitContent(fc)
76    }
77}
78
79// ---------- FlexBasis ----------
80
81/// Value for `flex-basis`.
82#[derive(Clone, Debug, PartialEq)]
83pub enum FlexBasis {
84    /// `auto` — basis comes from the item's `width`/`height`.
85    Auto,
86    /// `content` — basis is the content size.
87    Content,
88    /// An explicit length or percentage.
89    LengthPercentage(LengthPercentage),
90}
91
92impl ToCss for FlexBasis {
93    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
94        match self {
95            FlexBasis::Auto => dest.write_str("auto"),
96            FlexBasis::Content => dest.write_str("content"),
97            FlexBasis::LengthPercentage(lp) => lp.to_css(dest),
98        }
99    }
100}
101
102impl From<Length> for FlexBasis {
103    fn from(l: Length) -> Self {
104        Self::LengthPercentage(l.into())
105    }
106}
107
108impl From<Percentage> for FlexBasis {
109    fn from(p: Percentage) -> Self {
110        Self::LengthPercentage(p.into())
111    }
112}
113
114impl From<LengthPercentage> for FlexBasis {
115    fn from(lp: LengthPercentage) -> Self {
116        Self::LengthPercentage(lp)
117    }
118}
119
120// ---------- LineHeight ----------
121
122/// Value for `line-height`.
123#[derive(Clone, Debug, PartialEq)]
124pub enum LineHeight {
125    /// `normal` — engine-chosen line height.
126    Normal,
127    /// Unit-less multiplier of the element's `font-size`.
128    Number(f32),
129    /// Explicit length or percentage.
130    LengthPercentage(LengthPercentage),
131}
132
133impl ToCss for LineHeight {
134    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
135        match self {
136            LineHeight::Normal => dest.write_str("normal"),
137            LineHeight::Number(n) => write_number(dest, *n),
138            LineHeight::LengthPercentage(lp) => lp.to_css(dest),
139        }
140    }
141}
142
143impl From<Length> for LineHeight {
144    fn from(l: Length) -> Self {
145        Self::LengthPercentage(l.into())
146    }
147}
148
149impl From<Percentage> for LineHeight {
150    fn from(p: Percentage) -> Self {
151        Self::LengthPercentage(p.into())
152    }
153}
154
155impl From<LengthPercentage> for LineHeight {
156    fn from(lp: LengthPercentage) -> Self {
157        Self::LengthPercentage(lp)
158    }
159}
160
161impl From<f32> for LineHeight {
162    fn from(v: f32) -> Self {
163        Self::Number(v)
164    }
165}
166
167// ---------- ImageRef (background-image, etc.) ----------
168
169/// A reference to an image resource. Lynx accepts `url("...")`,
170/// `linear-gradient(...)`, and `radial-gradient(...)`. `conic-gradient`
171/// is supported on background-image but represented via [`crate::Gradient`].
172#[derive(Clone, Debug, PartialEq)]
173pub enum ImageRef {
174    /// `none` — no image.
175    None,
176    /// `url("<path>")`.
177    Url(CssString),
178    /// One of the `<gradient>` functions.
179    Gradient(crate::Gradient),
180}
181
182impl ToCss for ImageRef {
183    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
184        match self {
185            ImageRef::None => dest.write_str("none"),
186            ImageRef::Url(s) => {
187                dest.write_str("url(")?;
188                s.to_css(dest)?;
189                dest.write_char(')')
190            }
191            ImageRef::Gradient(g) => g.to_css(dest),
192        }
193    }
194}
195
196impl From<crate::Gradient> for ImageRef {
197    fn from(g: crate::Gradient) -> Self {
198        Self::Gradient(g)
199    }
200}
201
202// ---------- BorderRadius (4 corners + optional elliptical y) ----------
203
204/// Value for the `border-radius` shorthand. Stores per-corner
205/// radii, optionally with an elliptical second axis.
206#[derive(Clone, Debug, PartialEq)]
207pub struct BorderRadius {
208    /// Horizontal radii: top-left, top-right, bottom-right, bottom-left.
209    pub horizontal: [LengthPercentage; 4],
210    /// Optional vertical radii for an elliptical corner.
211    pub vertical: Option<[LengthPercentage; 4]>,
212}
213
214impl BorderRadius {
215    /// All four corners share the same radius.
216    pub fn all(v: impl Into<LengthPercentage>) -> Self {
217        let v = v.into();
218        Self {
219            horizontal: [v.clone(), v.clone(), v.clone(), v],
220            vertical: None,
221        }
222    }
223
224    /// Specify each corner explicitly (top-left, top-right, bottom-right, bottom-left).
225    pub fn corners(
226        tl: impl Into<LengthPercentage>,
227        tr: impl Into<LengthPercentage>,
228        br: impl Into<LengthPercentage>,
229        bl: impl Into<LengthPercentage>,
230    ) -> Self {
231        Self {
232            horizontal: [tl.into(), tr.into(), br.into(), bl.into()],
233            vertical: None,
234        }
235    }
236
237    /// Elliptical radius: horizontal and vertical components.
238    pub fn elliptical(horizontal: [LengthPercentage; 4], vertical: [LengthPercentage; 4]) -> Self {
239        Self {
240            horizontal,
241            vertical: Some(vertical),
242        }
243    }
244}
245
246impl ToCss for BorderRadius {
247    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
248        write_four(dest, &self.horizontal)?;
249        if let Some(v) = &self.vertical {
250            dest.write_str(" / ")?;
251            write_four(dest, v)?;
252        }
253        Ok(())
254    }
255}
256
257fn write_four(dest: &mut dyn fmt::Write, v: &[LengthPercentage; 4]) -> fmt::Result {
258    for (i, item) in v.iter().enumerate() {
259        if i > 0 {
260            dest.write_char(' ')?;
261        }
262        item.to_css(dest)?;
263    }
264    Ok(())
265}
266
267// ---------- GridLine, GridTemplate ----------
268
269/// Value for `grid-row-start`, `grid-row-end`, `grid-column-start`,
270/// `grid-column-end`. Lynx accepts numeric line references and
271/// `span <integer>`.
272#[derive(Copy, Clone, Debug, PartialEq)]
273pub enum GridLine {
274    /// `auto` — let the layout algorithm decide.
275    Auto,
276    /// Numeric line reference; negative values count from the end.
277    Number(i32),
278    /// `span <integer>` — span N tracks from the opposite edge.
279    Span(u32),
280}
281
282impl ToCss for GridLine {
283    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
284        match self {
285            GridLine::Auto => dest.write_str("auto"),
286            GridLine::Number(n) => write!(dest, "{n}"),
287            GridLine::Span(n) => write!(dest, "span {n}"),
288        }
289    }
290}
291
292/// Value for `grid-template-rows` / `grid-template-columns`. Lynx
293/// accepts a sequence of track-sizing values; this struct stores
294/// them as already-serialized track strings since the grammar is
295/// rich enough that a typed model is impractical.
296#[derive(Clone, Debug, PartialEq, Eq, Hash)]
297pub struct GridTemplate(pub String);
298
299impl GridTemplate {
300    /// Build from a list of track-sizing tokens. Each token is
301    /// joined with a space.
302    pub fn tracks(tracks: impl IntoIterator<Item = impl Into<String>>) -> Self {
303        let mut out = String::new();
304        for (i, t) in tracks.into_iter().enumerate() {
305            if i > 0 {
306                out.push(' ');
307            }
308            out.push_str(&t.into());
309        }
310        Self(out)
311    }
312}
313
314impl ToCss for GridTemplate {
315    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
316        dest.write_str(&self.0)
317    }
318}
319
320// ---------- Repeated (animation-name list etc.) ----------
321
322/// Comma-separated list of values, used for properties like
323/// `animation-name`, `transition-property`, `background-image`.
324#[derive(Clone, Debug, PartialEq)]
325pub struct Repeated<T>(pub Vec<T>);
326
327impl<T> Repeated<T> {
328    /// Wrap a `Vec<T>`.
329    pub fn new(v: impl IntoIterator<Item = T>) -> Self {
330        Self(v.into_iter().collect())
331    }
332}
333
334impl<T: ToCss> ToCss for Repeated<T> {
335    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
336        for (i, item) in self.0.iter().enumerate() {
337            if i > 0 {
338                dest.write_str(", ")?;
339            }
340            item.to_css(dest)?;
341        }
342        Ok(())
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::data_type::{ColorStop, Gradient, Length, NamedColor};
350    use crate::ext::*;
351
352    #[test]
353    fn size_keywords() {
354        assert_eq!(Size::Auto.to_css_string(), "auto");
355        assert_eq!(Size::MaxContent.to_css_string(), "max-content");
356        assert_eq!(Size::MinContent.to_css_string(), "min-content");
357        assert_eq!(Size::None.to_css_string(), "none");
358    }
359
360    #[test]
361    fn size_from_lengths_and_percentages() {
362        let from_len: Size = px(8).into();
363        let from_pct: Size = 50.percent().into();
364        let from_lp: Size = LengthPercentage::Length(Length::Px(4.0)).into();
365        let from_mc: Size = MaxContent.into();
366        let from_fc: Size = FitContent::keyword().into();
367        assert_eq!(from_len.to_css_string(), "8px");
368        assert_eq!(from_pct.to_css_string(), "50%");
369        assert_eq!(from_lp.to_css_string(), "4px");
370        assert_eq!(from_mc.to_css_string(), "max-content");
371        assert_eq!(from_fc.to_css_string(), "fit-content");
372    }
373
374    #[test]
375    fn size_fit_content_with_limit() {
376        let s = Size::FitContent(FitContent::with_limit(px(200)));
377        assert_eq!(s.to_css_string(), "fit-content(200px)");
378    }
379
380    #[test]
381    fn flex_basis_variants() {
382        assert_eq!(FlexBasis::Auto.to_css_string(), "auto");
383        assert_eq!(FlexBasis::Content.to_css_string(), "content");
384        let from_len: FlexBasis = px(120).into();
385        let from_pct: FlexBasis = 25.percent().into();
386        let from_lp: FlexBasis = LengthPercentage::Length(Length::Px(8.0)).into();
387        assert_eq!(from_len.to_css_string(), "120px");
388        assert_eq!(from_pct.to_css_string(), "25%");
389        assert_eq!(from_lp.to_css_string(), "8px");
390    }
391
392    #[test]
393    fn line_height_variants() {
394        assert_eq!(LineHeight::Normal.to_css_string(), "normal");
395        let n: LineHeight = 1.5_f32.into();
396        let from_len: LineHeight = px(20).into();
397        let from_pct: LineHeight = 150.percent().into();
398        let from_lp: LineHeight = LengthPercentage::Length(Length::Px(10.0)).into();
399        assert_eq!(n.to_css_string(), "1.5");
400        assert_eq!(from_len.to_css_string(), "20px");
401        assert_eq!(from_pct.to_css_string(), "150%");
402        assert_eq!(from_lp.to_css_string(), "10px");
403    }
404
405    #[test]
406    fn image_ref_variants() {
407        assert_eq!(ImageRef::None.to_css_string(), "none");
408        assert_eq!(
409            ImageRef::Url(CssString::new("a.png")).to_css_string(),
410            "url(\"a.png\")"
411        );
412        let g = Gradient::linear_to_bottom([ColorStop::new(NamedColor::Red.into())]);
413        let r: ImageRef = g.into();
414        assert_eq!(r.to_css_string(), "linear-gradient(to bottom, red)");
415    }
416
417    #[test]
418    fn border_radius_uniform() {
419        let r = BorderRadius::all(px(8));
420        assert_eq!(r.to_css_string(), "8px 8px 8px 8px");
421    }
422
423    #[test]
424    fn border_radius_corners() {
425        let r = BorderRadius::corners(px(2), px(4), px(6), px(8));
426        assert_eq!(r.to_css_string(), "2px 4px 6px 8px");
427    }
428
429    #[test]
430    fn border_radius_elliptical() {
431        let h = [px(2).into(), px(4).into(), px(6).into(), px(8).into()];
432        let v = [px(20).into(), px(40).into(), px(60).into(), px(80).into()];
433        let r = BorderRadius::elliptical(h, v);
434        assert_eq!(r.to_css_string(), "2px 4px 6px 8px / 20px 40px 60px 80px");
435    }
436
437    #[test]
438    fn grid_line_variants() {
439        assert_eq!(GridLine::Auto.to_css_string(), "auto");
440        assert_eq!(GridLine::Number(1).to_css_string(), "1");
441        assert_eq!(GridLine::Number(-1).to_css_string(), "-1");
442        assert_eq!(GridLine::Span(2).to_css_string(), "span 2");
443    }
444
445    #[test]
446    fn grid_template_joins_tracks() {
447        let t = GridTemplate::tracks(["1fr", "auto", "2fr"]);
448        assert_eq!(t.to_css_string(), "1fr auto 2fr");
449    }
450
451    #[test]
452    fn repeated_serializes_with_commas() {
453        let r = Repeated::new([Length::Px(8.0), Length::Px(16.0)]);
454        assert_eq!(r.to_css_string(), "8px, 16px");
455    }
456}