1use crate::{Color, Insets, Rect, Scene, TextDirection};
13
14#[derive(Clone, Copy, Debug, Default, PartialEq)]
17pub struct BorderRadius {
18 pub top_left: f32,
20 pub top_right: f32,
22 pub bottom_right: f32,
24 pub bottom_left: f32,
26}
27
28#[derive(Clone, Copy, Debug, Default, PartialEq)]
47pub struct BorderRadiusDirectional {
48 pub top_start: f32,
50 pub top_end: f32,
52 pub bottom_end: f32,
54 pub bottom_start: f32,
56}
57
58impl BorderRadiusDirectional {
59 pub const ZERO: Self = Self::uniform(0.0);
61
62 pub const fn uniform(radius: f32) -> Self {
65 Self {
66 top_start: radius,
67 top_end: radius,
68 bottom_end: radius,
69 bottom_start: radius,
70 }
71 }
72
73 pub const fn start(radius: f32) -> Self {
76 Self {
77 top_start: radius,
78 top_end: 0.0,
79 bottom_end: 0.0,
80 bottom_start: radius,
81 }
82 }
83
84 pub const fn end(radius: f32) -> Self {
86 Self {
87 top_start: 0.0,
88 top_end: radius,
89 bottom_end: radius,
90 bottom_start: 0.0,
91 }
92 }
93
94 pub const fn horizontal(start: f32, end: f32) -> Self {
97 Self {
98 top_start: start,
99 top_end: end,
100 bottom_end: end,
101 bottom_start: start,
102 }
103 }
104
105 pub const fn vertical(top: f32, bottom: f32) -> Self {
109 Self {
110 top_start: top,
111 top_end: top,
112 bottom_end: bottom,
113 bottom_start: bottom,
114 }
115 }
116
117 pub const fn resolve(self, direction: TextDirection) -> BorderRadius {
120 match direction {
121 TextDirection::Ltr => BorderRadius {
122 top_left: self.top_start,
123 top_right: self.top_end,
124 bottom_right: self.bottom_end,
125 bottom_left: self.bottom_start,
126 },
127 TextDirection::Rtl => BorderRadius {
128 top_left: self.top_end,
129 top_right: self.top_start,
130 bottom_right: self.bottom_start,
131 bottom_left: self.bottom_end,
132 },
133 }
134 }
135}
136
137impl From<f32> for BorderRadiusDirectional {
138 fn from(radius: f32) -> Self {
139 Self::uniform(radius)
140 }
141}
142
143impl BorderRadius {
144 #[must_use]
150 pub fn lerp(self, other: BorderRadius, t: f32) -> BorderRadius {
151 let mix = |a: f32, b: f32| a + (b - a) * t;
152 BorderRadius {
153 top_left: mix(self.top_left, other.top_left),
154 top_right: mix(self.top_right, other.top_right),
155 bottom_right: mix(self.bottom_right, other.bottom_right),
156 bottom_left: mix(self.bottom_left, other.bottom_left),
157 }
158 }
159
160 pub const ZERO: Self = Self::uniform(0.0);
162
163 pub const fn uniform(radius: f32) -> Self {
165 Self {
166 top_left: radius,
167 top_right: radius,
168 bottom_right: radius,
169 bottom_left: radius,
170 }
171 }
172
173 pub const fn top(radius: f32) -> Self {
175 Self {
176 top_left: radius,
177 top_right: radius,
178 bottom_right: 0.0,
179 bottom_left: 0.0,
180 }
181 }
182
183 pub const fn bottom(radius: f32) -> Self {
185 Self {
186 top_left: 0.0,
187 top_right: 0.0,
188 bottom_right: radius,
189 bottom_left: radius,
190 }
191 }
192
193 pub const fn left(radius: f32) -> Self {
195 Self {
196 top_left: radius,
197 top_right: 0.0,
198 bottom_right: 0.0,
199 bottom_left: radius,
200 }
201 }
202
203 pub const fn right(radius: f32) -> Self {
210 Self {
211 top_left: 0.0,
212 top_right: radius,
213 bottom_right: radius,
214 bottom_left: 0.0,
215 }
216 }
217
218 pub fn clamped(self) -> Self {
220 Self {
221 top_left: self.top_left.max(0.0),
222 top_right: self.top_right.max(0.0),
223 bottom_right: self.bottom_right.max(0.0),
224 bottom_left: self.bottom_left.max(0.0),
225 }
226 }
227
228 pub fn inflate(self, by: f32) -> Self {
230 Self {
231 top_left: self.top_left + by,
232 top_right: self.top_right + by,
233 bottom_right: self.bottom_right + by,
234 bottom_left: self.bottom_left + by,
235 }
236 }
237
238 pub fn scale(self, factor: f32) -> Self {
240 Self {
241 top_left: self.top_left * factor,
242 top_right: self.top_right * factor,
243 bottom_right: self.bottom_right * factor,
244 bottom_left: self.bottom_left * factor,
245 }
246 }
247
248 pub fn to_array(self) -> [f32; 4] {
250 [
251 self.top_left,
252 self.top_right,
253 self.bottom_right,
254 self.bottom_left,
255 ]
256 }
257}
258
259impl From<f32> for BorderRadius {
260 fn from(radius: f32) -> Self {
261 Self::uniform(radius)
262 }
263}
264
265#[derive(Clone, Copy, Debug, PartialEq)]
267pub struct Border {
268 pub width: f32,
270 pub color: Color,
272}
273
274impl Border {
275 pub const fn new(width: f32, color: Color) -> Self {
277 Self { width, color }
278 }
279
280 pub fn is_visible(&self) -> bool {
282 self.width > 0.0 && self.color.a > 0.0
283 }
284}
285
286#[derive(Clone, Copy, Debug, PartialEq)]
290pub struct LinearGradient {
291 pub end: Color,
293 pub direction: [f32; 2],
295}
296
297impl LinearGradient {
298 pub const fn new(end: Color, direction: [f32; 2]) -> Self {
300 Self { end, direction }
301 }
302}
303
304#[derive(Clone, Copy, Debug, PartialEq)]
306pub struct BoxShadow {
307 pub color: Color,
309 pub offset: (f32, f32),
311 pub blur: f32,
313 pub spread: f32,
315}
316
317impl BoxShadow {
318 pub const fn new(dx: f32, dy: f32, blur: f32, color: Color) -> Self {
320 Self {
321 color,
322 offset: (dx, dy),
323 blur,
324 spread: 0.0,
325 }
326 }
327
328 pub const fn spread(mut self, spread: f32) -> Self {
330 self.spread = spread;
331 self
332 }
333
334 pub fn bounds(&self, rect: Rect) -> Rect {
336 let grow = self.blur + self.spread;
337 Rect::new(
338 rect.x + self.offset.0 - grow,
339 rect.y + self.offset.1 - grow,
340 rect.width + 2.0 * grow,
341 rect.height + 2.0 * grow,
342 )
343 }
344}
345
346#[derive(Clone, Copy, Debug, Default, PartialEq)]
353pub struct BoxDecoration {
354 pub color: Option<Color>,
356 pub gradient: Option<LinearGradient>,
358 pub border: Option<Border>,
360 pub radius: BorderRadius,
362 pub shadow: Option<BoxShadow>,
364}
365
366fn fade_between(a: Option<Color>, b: Option<Color>, t: f32) -> Option<Color> {
373 match (a, b) {
374 (Some(a), Some(b)) => Some(a.lerp(b, t)),
375 (Some(a), None) => Some(a.fade(1.0 - t)),
376 (None, Some(b)) => Some(b.fade(t)),
377 (None, None) => None,
378 }
379}
380
381impl BoxDecoration {
382 #[must_use]
406 pub fn lerp(self, other: BoxDecoration, t: f32) -> BoxDecoration {
407 if t.is_nan() || t <= 0.0 {
408 return self;
409 }
410 if t >= 1.0 {
411 return other;
412 }
413 let mix = |a: f32, b: f32| a + (b - a) * t;
414 let gradient = match (self.gradient, other.gradient) {
415 (Some(a), Some(b)) => Some(LinearGradient {
416 end: a.end.lerp(b.end, t),
417 direction: [
418 mix(a.direction[0], b.direction[0]),
419 mix(a.direction[1], b.direction[1]),
420 ],
421 }),
422 (Some(a), None) => {
424 fade_between(Some(a.end), other.color, t).map(|end| LinearGradient {
425 end,
426 direction: a.direction,
427 })
428 }
429 (None, Some(b)) => fade_between(self.color, Some(b.end), t).map(|end| LinearGradient {
430 end,
431 direction: b.direction,
432 }),
433 (None, None) => None,
434 };
435 let border = match (self.border, other.border) {
436 (Some(a), Some(b)) => Some(Border {
437 width: mix(a.width, b.width),
438 color: a.color.lerp(b.color, t),
439 }),
440 (Some(a), None) => Some(Border {
441 width: a.width * (1.0 - t),
442 ..a
443 }),
444 (None, Some(b)) => Some(Border {
445 width: b.width * t,
446 ..b
447 }),
448 (None, None) => None,
449 };
450 let grown = |s: BoxShadow, f: f32| BoxShadow {
454 color: s.color.fade(f),
455 offset: (s.offset.0 * f, s.offset.1 * f),
456 blur: s.blur * f,
457 spread: s.spread * f,
458 };
459 let shadow = match (self.shadow, other.shadow) {
460 (Some(a), Some(b)) => Some(BoxShadow {
461 color: a.color.lerp(b.color, t),
462 offset: (mix(a.offset.0, b.offset.0), mix(a.offset.1, b.offset.1)),
463 blur: mix(a.blur, b.blur),
464 spread: mix(a.spread, b.spread),
465 }),
466 (Some(a), None) => Some(grown(a, 1.0 - t)),
467 (None, Some(b)) => Some(grown(b, t)),
468 (None, None) => None,
469 };
470 BoxDecoration {
471 color: fade_between(self.color, other.color, t),
472 gradient,
473 border,
474 radius: self.radius.lerp(other.radius, t),
475 shadow,
476 }
477 }
478}
479
480impl BoxDecoration {
481 pub fn filled(color: Color) -> Self {
483 Self {
484 color: Some(color),
485 ..Default::default()
486 }
487 }
488
489 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
492 self.radius = radius.into();
493 self
494 }
495
496 pub fn border(mut self, border: Border) -> Self {
498 self.border = Some(border);
499 self
500 }
501
502 pub fn shadow(mut self, shadow: BoxShadow) -> Self {
504 self.shadow = Some(shadow);
505 self
506 }
507
508 pub fn gradient(mut self, gradient: LinearGradient) -> Self {
510 self.gradient = Some(gradient);
511 self
512 }
513
514 pub fn content_padding(&self) -> Insets {
517 match self.border {
518 Some(b) if b.is_visible() => Insets::uniform(b.width),
519 _ => Insets::ZERO,
520 }
521 }
522
523 pub fn paint_into(&self, scene: &mut Scene, rect: Rect, opacity: f32) {
527 if let Some(shadow) = self.shadow {
529 scene.shadow(
530 shadow.bounds(rect),
531 shadow.color.fade(opacity),
532 self.radius.inflate(shadow.blur + shadow.spread),
533 shadow.blur,
534 );
535 }
536
537 let (border_width, border_color) = match self.border {
539 Some(b) => (b.width, b.color.fade(opacity)),
540 None => (0.0, Color::TRANSPARENT),
541 };
542 let has_border = self.border.map(|b| b.is_visible()).unwrap_or(false);
543
544 match (self.color, self.gradient) {
545 (Some(color), Some(gradient)) => scene.gradient_rect(
546 rect,
547 color.fade(opacity),
548 gradient.end.fade(opacity),
549 gradient.direction,
550 self.radius,
551 border_width,
552 border_color,
553 ),
554 (Some(color), None) => scene.draw_rect(
555 rect,
556 color.fade(opacity),
557 self.radius,
558 border_width,
559 border_color,
560 ),
561 (None, _) if has_border => scene.draw_rect(
563 rect,
564 Color::TRANSPARENT,
565 self.radius,
566 border_width,
567 border_color,
568 ),
569 (None, _) => {}
571 }
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 #[test]
580 fn a_directional_radius_follows_the_reading_direction() {
581 let end = BorderRadiusDirectional::end(16.0);
582 assert_eq!(
583 end.resolve(TextDirection::Ltr),
584 BorderRadius::right(16.0),
585 "the line ends on the right in English"
586 );
587 assert_eq!(
588 end.resolve(TextDirection::Rtl),
589 BorderRadius::left(16.0),
590 "and on the left in Arabic"
591 );
592
593 let start = BorderRadiusDirectional::start(16.0);
594 assert_eq!(start.resolve(TextDirection::Ltr), BorderRadius::left(16.0));
595 assert_eq!(start.resolve(TextDirection::Rtl), BorderRadius::right(16.0));
596
597 let both = BorderRadiusDirectional::horizontal(4.0, 12.0);
599 assert_eq!(
600 both.resolve(TextDirection::Ltr),
601 BorderRadius {
602 top_left: 4.0,
603 top_right: 12.0,
604 bottom_right: 12.0,
605 bottom_left: 4.0,
606 }
607 );
608 assert_eq!(
609 both.resolve(TextDirection::Rtl),
610 BorderRadius {
611 top_left: 12.0,
612 top_right: 4.0,
613 bottom_right: 4.0,
614 bottom_left: 12.0,
615 }
616 );
617
618 for direction in [TextDirection::Ltr, TextDirection::Rtl] {
620 assert_eq!(
621 BorderRadiusDirectional::uniform(8.0).resolve(direction),
622 BorderRadius::uniform(8.0)
623 );
624 assert_eq!(
625 BorderRadiusDirectional::vertical(8.0, 0.0).resolve(direction),
626 BorderRadius::top(8.0)
627 );
628 }
629 }
630 use super::*;
631 use crate::Primitive;
632
633 fn rect() -> Rect {
634 Rect::new(10.0, 20.0, 100.0, 40.0)
635 }
636
637 #[test]
638 fn content_padding_reserves_the_border() {
639 let deco = BoxDecoration::filled(Color::WHITE).border(Border::new(2.0, Color::BLACK));
640 assert_eq!(deco.content_padding(), Insets::uniform(2.0));
641 let none = BoxDecoration::filled(Color::WHITE).border(Border::new(0.0, Color::BLACK));
643 assert_eq!(none.content_padding(), Insets::ZERO);
644 }
645
646 #[test]
647 fn empty_decoration_paints_nothing() {
648 let mut scene = Scene::new();
649 BoxDecoration::default().paint_into(&mut scene, rect(), 1.0);
650 assert!(scene.is_empty());
651 }
652
653 #[test]
654 fn fixed_paint_order_shadow_then_fill() {
655 let mut scene = Scene::new();
656 let deco = BoxDecoration::filled(Color::WHITE)
657 .radius(6.0)
658 .shadow(BoxShadow::new(
659 0.0,
660 4.0,
661 8.0,
662 Color::rgba(0.0, 0.0, 0.0, 0.5),
663 ));
664 deco.paint_into(&mut scene, rect(), 1.0);
665 assert_eq!(scene.len(), 2);
667 match scene.primitives()[0] {
668 Primitive::Rect { blur, .. } => {
669 assert!(blur > 0.0, "first primitive = the blurred shadow")
670 }
671 _ => panic!("expected a rectangle"),
672 }
673 match scene.primitives()[1] {
674 Primitive::Rect { blur, color, .. } => {
675 assert_eq!(blur, 0.0, "second primitive = the crisp background");
676 assert_eq!(color, Color::WHITE);
677 }
678 _ => panic!("expected a rectangle"),
679 }
680 }
681
682 #[test]
683 fn opacity_fades_all_colours() {
684 let mut scene = Scene::new();
685 BoxDecoration::filled(Color::rgb(1.0, 0.0, 0.0))
686 .border(Border::new(2.0, Color::rgb(0.0, 1.0, 0.0)))
687 .paint_into(&mut scene, rect(), 0.5);
688 match scene.primitives()[0] {
689 Primitive::Rect {
690 color,
691 border_color,
692 ..
693 } => {
694 assert_eq!(color.a, 0.5);
695 assert_eq!(border_color.a, 0.5);
696 }
697 _ => panic!("expected a rectangle"),
698 }
699 }
700
701 #[test]
702 fn border_only_paints_transparent_fill_with_stroke() {
703 let mut scene = Scene::new();
704 BoxDecoration::default()
705 .border(Border::new(1.0, Color::WHITE))
706 .paint_into(&mut scene, rect(), 1.0);
707 assert_eq!(scene.len(), 1);
708 match scene.primitives()[0] {
709 Primitive::Rect {
710 color,
711 border_width,
712 ..
713 } => {
714 assert_eq!(color, Color::TRANSPARENT);
715 assert_eq!(border_width, 1.0);
716 }
717 _ => panic!("expected a rectangle"),
718 }
719 }
720
721 #[test]
722 fn shadow_bounds_grow_with_blur_and_spread() {
723 let s = BoxShadow::new(0.0, 0.0, 4.0, Color::BLACK).spread(2.0);
724 let b = s.bounds(Rect::new(0.0, 0.0, 10.0, 10.0));
725 assert_eq!(b, Rect::new(-6.0, -6.0, 22.0, 22.0));
727 }
728}
729
730#[cfg(test)]
732mod lerp_tests {
733 use super::*;
734
735 const RED: Color = Color {
736 r: 1.0,
737 g: 0.0,
738 b: 0.0,
739 a: 1.0,
740 };
741 const BLUE: Color = Color {
742 r: 0.0,
743 g: 0.0,
744 b: 1.0,
745 a: 1.0,
746 };
747
748 #[test]
751 fn the_ends_are_the_ends_exactly() {
752 let a = BoxDecoration::filled(RED).radius(4.0);
753 let b = BoxDecoration::filled(BLUE).border(Border::new(2.0, RED));
754 assert_eq!(a.lerp(b, 0.0), a);
755 assert_eq!(a.lerp(b, 1.0), b);
756 assert_eq!(a.lerp(b, -0.5), a, "an undershoot stops at the start");
757 assert_eq!(a.lerp(b, 1.5), b, "an overshoot stops at the end");
758 assert_eq!(a.lerp(b, f32::NAN), a, "and no progress is no progress");
759 }
760
761 #[test]
765 fn a_fill_fading_out_does_not_pass_through_black() {
766 let mid = BoxDecoration::filled(RED).lerp(BoxDecoration::default(), 0.5);
767 let c = mid.color.expect("still painted, half faded");
768 assert_eq!((c.r, c.g, c.b), (1.0, 0.0, 0.0), "the same red");
769 assert!((c.a - 0.5).abs() < 1e-6, "at half its opacity: {c:?}");
770 let naive = RED.lerp(Color::TRANSPARENT, 0.5);
771 assert!(naive.r < 0.6, "the trap this avoids: {naive:?}");
772 }
773
774 #[test]
776 fn a_fill_fading_in_arrives_in_its_own_colour() {
777 let c = BoxDecoration::default()
778 .lerp(BoxDecoration::filled(BLUE), 0.25)
779 .color
780 .expect("already there, faintly");
781 assert_eq!((c.r, c.g, c.b), (0.0, 0.0, 1.0));
782 assert!((c.a - 0.25).abs() < 1e-6, "{c:?}");
783 }
784
785 #[test]
788 fn a_flat_fill_spreads_into_a_gradient() {
789 let flat = BoxDecoration::filled(RED);
790 let graded = BoxDecoration::filled(RED).gradient(LinearGradient::new(BLUE, [0.0, 1.0]));
791 let mid = flat.lerp(graded, 0.5);
792 assert_eq!(mid.color, Some(RED), "the start does not move");
793 let g = mid.gradient.expect("a gradient half-way to its far colour");
794 assert_eq!(g.end, RED.lerp(BLUE, 0.5));
795 assert_eq!(g.direction, [0.0, 1.0]);
796 }
797
798 #[test]
801 fn a_border_arrives_by_thickening_in_its_own_colour() {
802 let lined = BoxDecoration::filled(RED).border(Border::new(4.0, BLUE));
803 let quarter = BoxDecoration::filled(RED)
804 .lerp(lined, 0.25)
805 .border
806 .expect("a line already");
807 assert_eq!(quarter.width, 1.0);
808 assert_eq!(quarter.color, BLUE);
809 }
810
811 #[test]
813 fn a_shadow_grows_and_fades_in() {
814 let raised = BoxDecoration::filled(RED).shadow(BoxShadow::new(
815 0.0,
816 8.0,
817 16.0,
818 Color::rgba(0.0, 0.0, 0.0, 0.4),
819 ));
820 let half = BoxDecoration::filled(RED)
821 .lerp(raised, 0.5)
822 .shadow
823 .expect("a shadow already");
824 assert_eq!(half.offset, (0.0, 4.0));
825 assert_eq!(half.blur, 8.0);
826 assert!((half.color.a - 0.2).abs() < 1e-6, "{:?}", half.color);
827 }
828
829 #[test]
831 fn corners_travel_one_by_one() {
832 let r = BoxDecoration::default()
833 .radius(BorderRadius::top(8.0))
834 .lerp(BoxDecoration::default().radius(16.0), 0.5)
835 .radius;
836 assert_eq!((r.top_left, r.bottom_left), (12.0, 8.0));
837 }
838}