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}