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}