Skip to main content

whisker_css/shorthand/
background.rs

1//! `background` shorthand — multi-layer plus a trailing color.
2
3use core::fmt;
4
5use crate::css::Css;
6use crate::data_type::Color;
7use crate::data_type_ext::Position;
8use crate::keyword::{
9    BackgroundAttachment, BackgroundClip, BackgroundOrigin, BackgroundRepeat, BackgroundSize,
10};
11use crate::to_css::ToCss;
12use crate::value::ImageRef;
13
14/// One background layer. Only `image` is required; other fields
15/// default to omitted in the serialized form.
16#[derive(Clone, Debug, PartialEq)]
17pub struct BackgroundLayer {
18    /// Image (`url(...)` / `<gradient>` / `none`).
19    pub image: ImageRef,
20    /// Position.
21    pub position: Option<Position>,
22    /// Size — emitted as `/ <size>` after `position`.
23    pub size: Option<BackgroundSize>,
24    /// Repeat.
25    pub repeat: Option<BackgroundRepeat>,
26    /// Attachment.
27    pub attachment: Option<BackgroundAttachment>,
28    /// Origin.
29    pub origin: Option<BackgroundOrigin>,
30    /// Clip.
31    pub clip: Option<BackgroundClip>,
32}
33
34impl BackgroundLayer {
35    /// Start with an image (or [`ImageRef::None`]).
36    pub fn new(image: impl Into<ImageRef>) -> Self {
37        Self {
38            image: image.into(),
39            position: None,
40            size: None,
41            repeat: None,
42            attachment: None,
43            origin: None,
44            clip: None,
45        }
46    }
47
48    /// Set the layer position.
49    pub fn position(mut self, p: Position) -> Self {
50        self.position = Some(p);
51        self
52    }
53
54    /// Set the layer size.
55    pub fn size(mut self, sz: BackgroundSize) -> Self {
56        self.size = Some(sz);
57        self
58    }
59
60    /// Set the repeat behavior.
61    pub fn repeat(mut self, r: BackgroundRepeat) -> Self {
62        self.repeat = Some(r);
63        self
64    }
65
66    /// Set the attachment.
67    pub fn attachment(mut self, a: BackgroundAttachment) -> Self {
68        self.attachment = Some(a);
69        self
70    }
71
72    /// Set the origin.
73    pub fn origin(mut self, o: BackgroundOrigin) -> Self {
74        self.origin = Some(o);
75        self
76    }
77
78    /// Set the clip.
79    pub fn clip(mut self, c: BackgroundClip) -> Self {
80        self.clip = Some(c);
81        self
82    }
83}
84
85impl ToCss for BackgroundLayer {
86    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
87        self.image.to_css(dest)?;
88        if let Some(p) = &self.position {
89            dest.write_char(' ')?;
90            p.to_css(dest)?;
91            if let Some(sz) = &self.size {
92                dest.write_str(" / ")?;
93                sz.to_css(dest)?;
94            }
95        } else if let Some(sz) = &self.size {
96            // `background-size` without an explicit `<position>` —
97            // CSS requires at least an implicit `0% 0%`; we emit
98            // `0 0` to keep the shorthand parseable.
99            dest.write_str(" 0 0 / ")?;
100            sz.to_css(dest)?;
101        }
102        if let Some(r) = &self.repeat {
103            dest.write_char(' ')?;
104            r.to_css(dest)?;
105        }
106        if let Some(a) = &self.attachment {
107            dest.write_char(' ')?;
108            a.to_css(dest)?;
109        }
110        if let Some(o) = &self.origin {
111            dest.write_char(' ')?;
112            o.to_css(dest)?;
113        }
114        if let Some(c) = &self.clip {
115            dest.write_char(' ')?;
116            c.to_css(dest)?;
117        }
118        Ok(())
119    }
120}
121
122/// `background` shorthand value — N image layers + one trailing
123/// `background-color`.
124///
125/// Lynx requires the color, if present, to come last (after all
126/// image layers). [`Background`] enforces that by storing it
127/// separately from the layer list.
128#[derive(Clone, Debug, Default, PartialEq)]
129pub struct Background {
130    /// Image layers, listed first-to-last.
131    pub layers: Vec<BackgroundLayer>,
132    /// Trailing color.
133    pub color: Option<Color>,
134}
135
136impl Background {
137    /// An empty background.
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// Append one layer.
143    pub fn layer(mut self, l: BackgroundLayer) -> Self {
144        self.layers.push(l);
145        self
146    }
147
148    /// Set the trailing color.
149    pub fn color(mut self, c: Color) -> Self {
150        self.color = Some(c);
151        self
152    }
153}
154
155impl ToCss for Background {
156    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
157        let mut wrote = false;
158        for layer in &self.layers {
159            if wrote {
160                dest.write_str(", ")?;
161            }
162            layer.to_css(dest)?;
163            wrote = true;
164        }
165        if let Some(c) = &self.color {
166            if wrote {
167                dest.write_char(' ')?;
168            }
169            c.to_css(dest)?;
170        }
171        Ok(())
172    }
173}
174
175impl Css {
176    /// Sets the `background` shorthand.
177    /// <https://lynxjs.org/api/css/properties/background>
178    pub fn background(self, b: Background) -> Self {
179        self.push("background", b)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use crate::data_type::{Color, ColorStop, CssString, Gradient, NamedColor};
186    use crate::data_type_ext::{Position, PositionKeyword};
187    use crate::keyword::*;
188    use crate::value::ImageRef;
189    use crate::Css;
190
191    use super::*;
192
193    #[test]
194    fn background_color_only() {
195        let s = Css::new().background(Background::new().color(Color::Named(NamedColor::Red)));
196        assert_eq!(s.to_string(), "background: red;");
197    }
198
199    #[test]
200    fn background_image_url_with_repeat() {
201        let layer = BackgroundLayer::new(ImageRef::Url(CssString::new("a.png")))
202            .repeat(BackgroundRepeat::NoRepeat);
203        let s = Css::new().background(Background::new().layer(layer));
204        assert_eq!(s.to_string(), "background: url(\"a.png\") no-repeat;");
205    }
206
207    #[test]
208    fn background_gradient_with_color_trailing() {
209        let layer = BackgroundLayer::new(Gradient::linear_to_bottom([
210            ColorStop::new(NamedColor::Red.into()),
211            ColorStop::new(NamedColor::Blue.into()),
212        ]));
213        let s = Css::new().background(
214            Background::new()
215                .layer(layer)
216                .color(Color::Named(NamedColor::White)),
217        );
218        assert_eq!(
219            s.to_string(),
220            "background: linear-gradient(to bottom, red, blue) white;"
221        );
222    }
223
224    #[test]
225    fn background_multiple_layers() {
226        let l1 = BackgroundLayer::new(ImageRef::Url(CssString::new("top.png")))
227            .repeat(BackgroundRepeat::NoRepeat);
228        let l2 = BackgroundLayer::new(ImageRef::Url(CssString::new("base.png")));
229        let s = Css::new().background(Background::new().layer(l1).layer(l2));
230        assert_eq!(
231            s.to_string(),
232            "background: url(\"top.png\") no-repeat, url(\"base.png\");"
233        );
234    }
235
236    #[test]
237    fn background_layer_size_with_position() {
238        let layer = BackgroundLayer::new(ImageRef::Url(CssString::new("a.png")))
239            .position(Position::Keyword(PositionKeyword::Center))
240            .size(BackgroundSize::Cover);
241        let s = Css::new().background(Background::new().layer(layer));
242        assert_eq!(s.to_string(), "background: url(\"a.png\") center / cover;");
243    }
244
245    #[test]
246    fn background_layer_size_without_position_inserts_zero() {
247        let layer = BackgroundLayer::new(ImageRef::Url(CssString::new("a.png")))
248            .size(BackgroundSize::Cover);
249        let s = Css::new().background(Background::new().layer(layer));
250        assert_eq!(s.to_string(), "background: url(\"a.png\") 0 0 / cover;");
251    }
252
253    #[test]
254    fn background_layer_origin_clip_attachment() {
255        let layer = BackgroundLayer::new(ImageRef::None)
256            .attachment(BackgroundAttachment::Fixed)
257            .origin(BackgroundOrigin::ContentBox)
258            .clip(BackgroundClip::PaddingBox);
259        let s = Css::new().background(Background::new().layer(layer));
260        assert_eq!(
261            s.to_string(),
262            "background: none fixed content-box padding-box;"
263        );
264    }
265}