Skip to main content

sequoia_style/
box_style.rs

1use sequoia_core::{Color, Rect, RenderList, Shadow, Vec2};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct ShadowSpec {
5    pub color: Color,
6
7    pub blur: f32,
8
9    pub spread: f32,
10
11    pub offset: Vec2,
12}
13
14impl ShadowSpec {
15    pub fn new(color: Color, blur: f32) -> Self {
16        Self {
17            color,
18            blur,
19            spread: 0.0,
20            offset: Vec2::ZERO,
21        }
22    }
23
24    pub fn spread(mut self, spread: f32) -> Self {
25        self.spread = spread;
26        self
27    }
28
29    pub fn offset(mut self, x: f32, y: f32) -> Self {
30        self.offset = Vec2::new(x, y);
31        self
32    }
33
34    pub fn is_visible(self) -> bool {
35        self.color.a > 0.0 && self.blur > 0.0 && self.blur.is_finite()
36    }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct BorderSide {
41    pub width: f32,
42
43    pub color: Color,
44}
45
46impl Default for BorderSide {
47    fn default() -> Self {
48        Self {
49            width: 0.0,
50            color: Color::TRANSPARENT,
51        }
52    }
53}
54
55impl BorderSide {
56    pub fn new(width: f32, color: Color) -> Self {
57        Self {
58            width: width.max(0.0),
59            color,
60        }
61    }
62
63    pub fn is_visible(self) -> bool {
64        self.width > 0.0 && self.color.a > 0.0
65    }
66}
67
68#[derive(Clone, Copy, Debug, PartialEq)]
69#[non_exhaustive]
70pub struct BoxStyle {
71    pub background: Color,
72
73    pub border_top: BorderSide,
74
75    pub border_right: BorderSide,
76
77    pub border_bottom: BorderSide,
78
79    pub border_left: BorderSide,
80
81    pub radius: f32,
82
83    pub shadow: Option<ShadowSpec>,
84
85    pub inset_shadow: Option<ShadowSpec>,
86}
87
88impl Default for BoxStyle {
89    fn default() -> Self {
90        Self {
91            background: Color::TRANSPARENT,
92            border_top: BorderSide::default(),
93            border_right: BorderSide::default(),
94            border_bottom: BorderSide::default(),
95            border_left: BorderSide::default(),
96            radius: 0.0,
97            shadow: None,
98            inset_shadow: None,
99        }
100    }
101}
102
103impl BoxStyle {
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    pub fn border_edges(self) -> sequoia_core::Edges {
109        sequoia_core::Edges::only(
110            self.border_left.width.max(0.0),
111            self.border_top.width.max(0.0),
112            self.border_right.width.max(0.0),
113            self.border_bottom.width.max(0.0),
114        )
115    }
116
117    pub fn background(mut self, color: Color) -> Self {
118        self.background = color;
119        self
120    }
121
122    pub fn border(mut self, width: f32, color: Color) -> Self {
123        let side = BorderSide::new(width, color);
124        self.border_top = side;
125        self.border_right = side;
126        self.border_bottom = side;
127        self.border_left = side;
128        self
129    }
130
131    pub fn border_left(mut self, width: f32, color: Color) -> Self {
132        self.border_left = BorderSide::new(width, color);
133        self
134    }
135
136    pub fn border_right(mut self, width: f32, color: Color) -> Self {
137        self.border_right = BorderSide::new(width, color);
138        self
139    }
140
141    pub fn border_top(mut self, width: f32, color: Color) -> Self {
142        self.border_top = BorderSide::new(width, color);
143        self
144    }
145
146    pub fn border_bottom(mut self, width: f32, color: Color) -> Self {
147        self.border_bottom = BorderSide::new(width, color);
148        self
149    }
150
151    pub fn border_x(mut self, width: f32, color: Color) -> Self {
152        let side = BorderSide::new(width, color);
153        self.border_left = side;
154        self.border_right = side;
155        self
156    }
157
158    pub fn border_y(mut self, width: f32, color: Color) -> Self {
159        let side = BorderSide::new(width, color);
160        self.border_top = side;
161        self.border_bottom = side;
162        self
163    }
164
165    pub fn radius(mut self, radius: f32) -> Self {
166        self.radius = radius.max(0.0);
167        self
168    }
169
170    pub fn shadow(mut self, spec: ShadowSpec) -> Self {
171        self.shadow = Some(spec);
172        self
173    }
174
175    pub fn inset_shadow(mut self, spec: ShadowSpec) -> Self {
176        self.inset_shadow = Some(spec);
177        self
178    }
179
180    pub fn is_empty(self) -> bool {
181        self.background.a <= 0.0
182            && !self.border_top.is_visible()
183            && !self.border_right.is_visible()
184            && !self.border_bottom.is_visible()
185            && !self.border_left.is_visible()
186            && !self.shadow.is_some_and(ShadowSpec::is_visible)
187            && !self.inset_shadow.is_some_and(ShadowSpec::is_visible)
188    }
189
190    fn paint_shadow(&self, list: &mut RenderList, rect: Rect, spec: ShadowSpec, inset: bool) {
191        if !spec.is_visible() {
192            return;
193        }
194
195        let radius = if inset {
196            (self.radius - spec.spread).max(0.0)
197        } else {
198            (self.radius + spec.spread).max(0.0)
199        };
200        list.push_shadow(Shadow {
201            rect,
202            color: spec.color,
203            radius,
204            blur: spec.blur,
205            spread: spec.spread,
206            offset: spec.offset,
207            inset,
208        });
209    }
210
211    pub fn uniform_border(self) -> Option<(f32, Color)> {
212        let t = self.border_top;
213        if !t.is_visible() {
214            return None;
215        }
216        for s in [self.border_right, self.border_bottom, self.border_left] {
217            if !s.is_visible() || s.width != t.width || s.color != t.color {
218                return None;
219            }
220        }
221        Some((t.width, t.color))
222    }
223
224    pub fn paint(self, list: &mut RenderList, rect: Rect) {
225        if self.is_empty() {
226            return;
227        }
228        if let Some(spec) = self.shadow {
229            self.paint_shadow(list, rect, spec, false);
230        }
231        if let Some((w, c)) = self.uniform_border() {
232            list.push_bordered_quad(rect, self.background, self.radius, w, c);
233            if let Some(spec) = self.inset_shadow {
234                self.paint_shadow(list, rect, spec, true);
235            }
236            return;
237        }
238        if self.background.a > 0.0 {
239            list.push_quad(rect, self.background, self.radius);
240        }
241        let x = rect.left();
242        let y = rect.top();
243        let w = rect.size.width;
244        let h = rect.size.height;
245
246        if self.border_left.is_visible() {
247            let lw = self.border_left.width.min(w.max(0.0));
248            list.push_quad(Rect::from_xywh(x, y, lw, h), self.border_left.color, 0.0);
249        }
250        if self.border_right.is_visible() {
251            let rw = self.border_right.width.min(w.max(0.0));
252            list.push_quad(
253                Rect::from_xywh(x + w - rw, y, rw, h),
254                self.border_right.color,
255                0.0,
256            );
257        }
258
259        let lw = if self.border_left.is_visible() {
260            self.border_left.width.min(w.max(0.0))
261        } else {
262            0.0
263        };
264        let rw = if self.border_right.is_visible() {
265            self.border_right.width.min(w.max(0.0))
266        } else {
267            0.0
268        };
269        let inner_w = (w - lw - rw).max(0.0);
270        if self.border_top.is_visible() {
271            let th = self.border_top.width;
272            list.push_quad(
273                Rect::from_xywh(x + lw, y, inner_w, th),
274                self.border_top.color,
275                0.0,
276            );
277        }
278        if self.border_bottom.is_visible() {
279            let bh = self.border_bottom.width;
280            list.push_quad(
281                Rect::from_xywh(x + lw, y + h - bh, inner_w, bh),
282                self.border_bottom.color,
283                0.0,
284            );
285        }
286        if let Some(spec) = self.inset_shadow {
287            self.paint_shadow(list, rect, spec, true);
288        }
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use sequoia_core::Vec2;
296    use sequoia_core::{Rect, RenderItem};
297
298    #[test]
299    fn shadow_is_painted_under_and_inset_over_the_decoration() {
300        let r = Rect::from_xywh(0.0, 0.0, 100.0, 50.0);
301        let mut list = RenderList::new();
302        BoxStyle::new()
303            .background(Color::WHITE)
304            .radius(8.0)
305            .shadow(ShadowSpec::new(Color::BLACK, 12.0).offset(0.0, 4.0))
306            .inset_shadow(ShadowSpec::new(Color::BLACK, 6.0))
307            .paint(&mut list, r);
308        let items = list.items();
309
310        let RenderItem::Shadow(outer) = items[0] else {
311            panic!("outer shadow first")
312        };
313        assert!(!outer.inset);
314        assert_eq!(outer.rect, r, "caster shape is the box");
315        assert_eq!(outer.radius, 8.0, "radius from the box");
316        assert_eq!(outer.blur, 12.0);
317        assert_eq!(outer.offset, Vec2::new(0.0, 4.0));
318        assert!(matches!(items[1], RenderItem::Quad(_)));
319        let RenderItem::Shadow(inner) = items[2] else {
320            panic!("inner shadow last")
321        };
322        assert!(inner.inset);
323        assert_eq!(inner.blur, 6.0);
324    }
325
326    #[test]
327    fn shadow_alone_is_not_empty() {
328        let style = BoxStyle::new().shadow(ShadowSpec::new(Color::BLACK, 4.0));
329        assert!(!style.is_empty(), "shadow alone is not an empty style");
330        let mut list = RenderList::new();
331        style.paint(&mut list, Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
332        assert_eq!(list.shadows().count(), 1);
333
334        assert!(
335            BoxStyle::new()
336                .shadow(ShadowSpec::new(Color::TRANSPARENT, 4.0))
337                .is_empty()
338        );
339        assert!(
340            BoxStyle::new()
341                .inset_shadow(ShadowSpec::new(Color::BLACK, 0.0))
342                .is_empty()
343        );
344    }
345
346    #[test]
347    fn empty_paints_nothing() {
348        let mut list = RenderList::new();
349        BoxStyle::new().paint(&mut list, Rect::from_xywh(0.0, 0.0, 100.0, 50.0));
350        assert!(list.is_empty());
351    }
352
353    #[test]
354    fn background_only_is_single_quad() {
355        let mut list = RenderList::new();
356        BoxStyle::new()
357            .background(Color::RED)
358            .radius(6.0)
359            .paint(&mut list, Rect::from_xywh(0.0, 0.0, 100.0, 50.0));
360        let qs: Vec<_> = list.quads().collect();
361        assert_eq!(qs.len(), 1);
362        assert_eq!(qs[0].color, Color::RED);
363        assert_eq!(qs[0].radius, 6.0);
364        assert!(!qs[0].has_border());
365    }
366
367    #[test]
368    fn uniform_border_is_single_bordered_quad() {
369        let mut list = RenderList::new();
370        BoxStyle::new()
371            .background(Color::BLUE)
372            .border(2.0, Color::WHITE)
373            .paint(&mut list, Rect::from_xywh(0.0, 0.0, 100.0, 60.0));
374        let qs: Vec<_> = list.quads().collect();
375        assert_eq!(qs.len(), 1, "background + frame is one quad");
376        assert_eq!(qs[0].border_width, 2.0);
377        assert_eq!(qs[0].border_color, Color::WHITE);
378    }
379
380    #[test]
381    fn left_border_is_background_plus_stripe() {
382        let mut list = RenderList::new();
383        BoxStyle::new()
384            .border_left(8.5, Color::hex(0xCACACA))
385            .paint(&mut list, Rect::from_xywh(0.0, 0.0, 100.0, 40.0));
386        let qs: Vec<_> = list.quads().collect();
387        assert_eq!(qs.len(), 1, "no background, bar only");
388        assert_eq!(qs[0].rect, Rect::from_xywh(0.0, 0.0, 8.5, 40.0));
389        assert_eq!(qs[0].color, Color::hex(0xCACACA));
390    }
391
392    #[test]
393    fn background_plus_left_border_is_two_quads() {
394        let mut list = RenderList::new();
395        BoxStyle::new()
396            .background(Color::WHITE)
397            .border_left(8.0, Color::RED)
398            .paint(&mut list, Rect::from_xywh(10.0, 5.0, 100.0, 40.0));
399        let qs: Vec<_> = list.quads().collect();
400        assert_eq!(qs.len(), 2);
401        assert_eq!(qs[0].rect, Rect::from_xywh(10.0, 5.0, 100.0, 40.0));
402        assert_eq!(qs[1].rect, Rect::from_xywh(10.0, 5.0, 8.0, 40.0));
403    }
404
405    #[test]
406    fn top_bottom_fit_between_sides() {
407        let mut list = RenderList::new();
408        BoxStyle::new()
409            .border_left(10.0, Color::RED)
410            .border_right(10.0, Color::GREEN)
411            .border_top(5.0, Color::BLUE)
412            .paint(&mut list, Rect::from_xywh(0.0, 0.0, 100.0, 50.0));
413        let qs: Vec<_> = list.quads().collect();
414        assert_eq!(qs.len(), 3);
415
416        assert_eq!(qs[2].rect, Rect::from_xywh(10.0, 0.0, 80.0, 5.0));
417    }
418
419    #[test]
420    fn uniform_detection() {
421        let u = BoxStyle::new().border(2.0, Color::WHITE);
422        assert_eq!(u.uniform_border(), Some((2.0, Color::WHITE)));
423        let mixed = BoxStyle::new()
424            .border_left(2.0, Color::WHITE)
425            .border_right(3.0, Color::WHITE);
426        assert_eq!(mixed.uniform_border(), None);
427        let tinted = BoxStyle::new()
428            .border_left(2.0, Color::WHITE)
429            .border_right(2.0, Color::RED);
430        assert_eq!(tinted.uniform_border(), None);
431    }
432}