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