1use crate::{
13 BlurStyle, BorderSide, ClipShape, Color, DecorationImage, Insets, Path, Point, Primitive, Rect,
14 Scene, ShapeBorder, TextDirection,
15};
16
17#[derive(Clone, Copy, Debug, Default, PartialEq)]
20pub struct BorderRadius {
21 pub top_left: f32,
23 pub top_right: f32,
25 pub bottom_right: f32,
27 pub bottom_left: f32,
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq)]
50pub struct BorderRadiusDirectional {
51 pub top_start: f32,
53 pub top_end: f32,
55 pub bottom_end: f32,
57 pub bottom_start: f32,
59}
60
61impl BorderRadiusDirectional {
62 pub const ZERO: Self = Self::uniform(0.0);
64
65 pub const fn uniform(radius: f32) -> Self {
68 Self {
69 top_start: radius,
70 top_end: radius,
71 bottom_end: radius,
72 bottom_start: radius,
73 }
74 }
75
76 pub const fn start(radius: f32) -> Self {
79 Self {
80 top_start: radius,
81 top_end: 0.0,
82 bottom_end: 0.0,
83 bottom_start: radius,
84 }
85 }
86
87 pub const fn end(radius: f32) -> Self {
89 Self {
90 top_start: 0.0,
91 top_end: radius,
92 bottom_end: radius,
93 bottom_start: 0.0,
94 }
95 }
96
97 pub const fn horizontal(start: f32, end: f32) -> Self {
100 Self {
101 top_start: start,
102 top_end: end,
103 bottom_end: end,
104 bottom_start: start,
105 }
106 }
107
108 pub const fn vertical(top: f32, bottom: f32) -> Self {
112 Self {
113 top_start: top,
114 top_end: top,
115 bottom_end: bottom,
116 bottom_start: bottom,
117 }
118 }
119
120 pub const fn resolve(self, direction: TextDirection) -> BorderRadius {
123 match direction {
124 TextDirection::Ltr => BorderRadius {
125 top_left: self.top_start,
126 top_right: self.top_end,
127 bottom_right: self.bottom_end,
128 bottom_left: self.bottom_start,
129 },
130 TextDirection::Rtl => BorderRadius {
131 top_left: self.top_end,
132 top_right: self.top_start,
133 bottom_right: self.bottom_start,
134 bottom_left: self.bottom_end,
135 },
136 }
137 }
138}
139
140impl From<f32> for BorderRadiusDirectional {
141 fn from(radius: f32) -> Self {
142 Self::uniform(radius)
143 }
144}
145
146impl BorderRadius {
147 #[must_use]
153 pub fn lerp(self, other: BorderRadius, t: f32) -> BorderRadius {
154 let mix = |a: f32, b: f32| a + (b - a) * t;
155 BorderRadius {
156 top_left: mix(self.top_left, other.top_left),
157 top_right: mix(self.top_right, other.top_right),
158 bottom_right: mix(self.bottom_right, other.bottom_right),
159 bottom_left: mix(self.bottom_left, other.bottom_left),
160 }
161 }
162
163 pub const ZERO: Self = Self::uniform(0.0);
165
166 pub const fn uniform(radius: f32) -> Self {
168 Self {
169 top_left: radius,
170 top_right: radius,
171 bottom_right: radius,
172 bottom_left: radius,
173 }
174 }
175
176 pub const fn top(radius: f32) -> Self {
178 Self {
179 top_left: radius,
180 top_right: radius,
181 bottom_right: 0.0,
182 bottom_left: 0.0,
183 }
184 }
185
186 pub const fn bottom(radius: f32) -> Self {
188 Self {
189 top_left: 0.0,
190 top_right: 0.0,
191 bottom_right: radius,
192 bottom_left: radius,
193 }
194 }
195
196 pub const fn left(radius: f32) -> Self {
198 Self {
199 top_left: radius,
200 top_right: 0.0,
201 bottom_right: 0.0,
202 bottom_left: radius,
203 }
204 }
205
206 pub const fn right(radius: f32) -> Self {
213 Self {
214 top_left: 0.0,
215 top_right: radius,
216 bottom_right: radius,
217 bottom_left: 0.0,
218 }
219 }
220
221 pub fn clamped(self) -> Self {
223 Self {
224 top_left: self.top_left.max(0.0),
225 top_right: self.top_right.max(0.0),
226 bottom_right: self.bottom_right.max(0.0),
227 bottom_left: self.bottom_left.max(0.0),
228 }
229 }
230
231 pub fn inflate(self, by: f32) -> Self {
233 Self {
234 top_left: self.top_left + by,
235 top_right: self.top_right + by,
236 bottom_right: self.bottom_right + by,
237 bottom_left: self.bottom_left + by,
238 }
239 }
240
241 pub fn scale(self, factor: f32) -> Self {
243 Self {
244 top_left: self.top_left * factor,
245 top_right: self.top_right * factor,
246 bottom_right: self.bottom_right * factor,
247 bottom_left: self.bottom_left * factor,
248 }
249 }
250
251 pub fn to_array(self) -> [f32; 4] {
253 [
254 self.top_left,
255 self.top_right,
256 self.bottom_right,
257 self.bottom_left,
258 ]
259 }
260}
261
262impl From<f32> for BorderRadius {
263 fn from(radius: f32) -> Self {
264 Self::uniform(radius)
265 }
266}
267
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
271pub enum BoxShape {
272 #[default]
274 Rectangle,
275 Circle,
278}
279
280fn lerp_side(a: BorderSide, b: BorderSide, t: f32) -> BorderSide {
282 match (a.width > 0.0, b.width > 0.0) {
284 (true, true) => {
285 BorderSide::new(a.color.lerp(b.color, t), a.width + (b.width - a.width) * t)
286 }
287 (true, false) => BorderSide::new(a.color, a.width * (1.0 - t)),
288 (false, true) => BorderSide::new(b.color, b.width * t),
289 (false, false) => BorderSide::NONE,
290 }
291}
292
293#[derive(Clone, Copy, Debug, Default, PartialEq)]
296pub struct Border {
297 pub top: BorderSide,
299 pub right: BorderSide,
301 pub bottom: BorderSide,
303 pub left: BorderSide,
305}
306
307impl Border {
308 pub const NONE: Self = Self {
310 top: BorderSide::NONE,
311 right: BorderSide::NONE,
312 bottom: BorderSide::NONE,
313 left: BorderSide::NONE,
314 };
315
316 pub const fn new(width: f32, color: Color) -> Self {
318 Self::all(BorderSide::new(color, width))
319 }
320
321 pub const fn all(side: BorderSide) -> Self {
323 Self {
324 top: side,
325 right: side,
326 bottom: side,
327 left: side,
328 }
329 }
330
331 pub const fn symmetric(vertical: BorderSide, horizontal: BorderSide) -> Self {
334 Self {
335 top: horizontal,
336 right: vertical,
337 bottom: horizontal,
338 left: vertical,
339 }
340 }
341
342 fn sides(&self) -> [BorderSide; 4] {
344 [self.top, self.right, self.bottom, self.left]
345 }
346
347 pub fn is_uniform(&self) -> bool {
349 let [t, r, b, l] = self.sides();
350 t == r && r == b && b == l
351 }
352
353 pub fn is_visible(&self) -> bool {
355 self.sides().iter().any(BorderSide::is_drawn)
356 }
357
358 pub fn dimensions(&self) -> Insets {
360 let w = |s: BorderSide| if s.is_drawn() { s.width } else { 0.0 };
361 Insets::new(w(self.top), w(self.right), w(self.bottom), w(self.left))
362 }
363
364 #[must_use]
366 pub fn lerp(self, other: Border, t: f32) -> Border {
367 Border {
368 top: lerp_side(self.top, other.top, t),
369 right: lerp_side(self.right, other.right, t),
370 bottom: lerp_side(self.bottom, other.bottom, t),
371 left: lerp_side(self.left, other.left, t),
372 }
373 }
374}
375
376#[derive(Clone, Copy, Debug, Default, PartialEq)]
380pub struct BorderDirectional {
381 pub top: BorderSide,
383 pub start: BorderSide,
385 pub end: BorderSide,
387 pub bottom: BorderSide,
389}
390
391impl BorderDirectional {
392 pub fn resolve(&self, direction: TextDirection) -> Border {
394 let (left, right) = match direction {
395 TextDirection::Ltr => (self.start, self.end),
396 TextDirection::Rtl => (self.end, self.start),
397 };
398 Border {
399 top: self.top,
400 right,
401 bottom: self.bottom,
402 left,
403 }
404 }
405}
406
407#[derive(Clone, Copy, Debug, PartialEq)]
409pub enum BoxBorder {
410 Physical(Border),
412 Directional(BorderDirectional),
414}
415
416impl BoxBorder {
417 pub fn resolve(&self, direction: TextDirection) -> Border {
419 match self {
420 BoxBorder::Physical(border) => *border,
421 BoxBorder::Directional(border) => border.resolve(direction),
422 }
423 }
424}
425
426impl From<Border> for BoxBorder {
427 fn from(border: Border) -> Self {
428 BoxBorder::Physical(border)
429 }
430}
431
432impl From<BorderDirectional> for BoxBorder {
433 fn from(border: BorderDirectional) -> Self {
434 BoxBorder::Directional(border)
435 }
436}
437
438#[derive(Clone, Copy, Debug, PartialEq)]
440pub struct BoxShadow {
441 pub color: Color,
443 pub offset: (f32, f32),
445 pub blur: f32,
447 pub spread: f32,
449 pub blur_style: BlurStyle,
451}
452
453impl BoxShadow {
454 pub const fn new(dx: f32, dy: f32, blur: f32, color: Color) -> Self {
456 Self {
457 color,
458 offset: (dx, dy),
459 blur,
460 spread: 0.0,
461 blur_style: BlurStyle::Normal,
462 }
463 }
464
465 pub const fn spread(mut self, spread: f32) -> Self {
467 self.spread = spread;
468 self
469 }
470
471 pub const fn blur_style(mut self, style: BlurStyle) -> Self {
474 self.blur_style = style;
475 self
476 }
477
478 pub fn for_elevation(elevation: f32, color: Color) -> [BoxShadow; 3] {
487 let e = elevation.clamp(0.0, 24.0);
488 let upper = ELEVATION_TABLE
489 .iter()
490 .position(|(level, _)| *level >= e)
491 .unwrap_or(ELEVATION_TABLE.len() - 1);
492 let lower = upper.saturating_sub(1);
493 let (a, b) = (ELEVATION_TABLE[lower], ELEVATION_TABLE[upper]);
494 let t = if b.0 > a.0 {
495 (e - a.0) / (b.0 - a.0)
496 } else {
497 1.0
498 };
499 let lerp = |x: f32, y: f32| x + (y - x) * t;
500 let layer = |i: usize, strength: f32| {
501 let (ya, blur_a, spread_a) = a.1[i];
502 let (yb, blur_b, spread_b) = b.1[i];
503 let fade = if e < 1.0 { e } else { 1.0 };
505 BoxShadow {
506 color: color.with_alpha(color.a * strength * fade),
507 offset: (0.0, lerp(ya, yb)),
508 blur: lerp(blur_a, blur_b),
509 spread: lerp(spread_a, spread_b),
510 blur_style: BlurStyle::Normal,
511 }
512 };
513 [layer(0, 0.2), layer(1, 0.14), layer(2, 0.12)]
514 }
515
516 pub fn bounds(&self, rect: Rect) -> Rect {
518 let grow = self.blur + self.spread;
519 Rect::new(
520 rect.x + self.offset.0 - grow,
521 rect.y + self.offset.1 - grow,
522 rect.width + 2.0 * grow,
523 rect.height + 2.0 * grow,
524 )
525 }
526}
527
528type Layer = (f32, f32, f32);
532type Height = (f32, [Layer; 3]);
534
535const ELEVATION_TABLE: [Height; 11] = [
536 (0.0, [(0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)]),
537 (1.0, [(2.0, 1.0, -1.0), (1.0, 1.0, 0.0), (1.0, 3.0, 0.0)]),
538 (2.0, [(3.0, 1.0, -2.0), (2.0, 2.0, 0.0), (1.0, 5.0, 0.0)]),
539 (3.0, [(3.0, 3.0, -2.0), (3.0, 4.0, 0.0), (1.0, 8.0, 0.0)]),
540 (4.0, [(2.0, 4.0, -1.0), (4.0, 5.0, 0.0), (1.0, 10.0, 0.0)]),
541 (6.0, [(3.0, 5.0, -1.0), (6.0, 10.0, 0.0), (1.0, 18.0, 0.0)]),
542 (8.0, [(5.0, 5.0, -3.0), (8.0, 10.0, 1.0), (3.0, 14.0, 2.0)]),
543 (9.0, [(5.0, 6.0, -3.0), (9.0, 12.0, 1.0), (3.0, 16.0, 2.0)]),
544 (
545 12.0,
546 [(7.0, 8.0, -4.0), (12.0, 17.0, 2.0), (5.0, 22.0, 4.0)],
547 ),
548 (
549 16.0,
550 [(8.0, 10.0, -5.0), (16.0, 24.0, 2.0), (6.0, 30.0, 5.0)],
551 ),
552 (
553 24.0,
554 [(11.0, 15.0, -7.0), (24.0, 38.0, 3.0), (9.0, 46.0, 8.0)],
555 ),
556];
557
558pub fn paint_elevation(
562 scene: &mut Scene,
563 rect: Rect,
564 radius: BorderRadius,
565 elevation: f32,
566 color: Color,
567) {
568 if elevation <= 0.0 || color.a <= 0.0 {
569 return;
570 }
571 for shadow in BoxShadow::for_elevation(elevation, color) {
572 let grow = (shadow.blur + shadow.spread).max(0.0);
573 scene.shadow(
574 shadow.bounds(rect),
575 shadow.color,
576 radius.inflate(grow),
577 shadow.blur,
578 );
579 }
580}
581
582#[derive(Clone, Debug, Default, PartialEq)]
591pub struct BoxDecoration {
592 pub color: Option<Color>,
594 pub gradient: Option<crate::Gradient>,
597 pub border: Option<BoxBorder>,
599 pub radius: BorderRadius,
601 pub shadows: Vec<BoxShadow>,
603 pub shape: BoxShape,
605 pub image: Option<DecorationImage>,
608 pub background_blend_mode: Option<crate::BlendMode>,
612}
613
614fn fade_between(a: Option<Color>, b: Option<Color>, t: f32) -> Option<Color> {
621 match (a, b) {
622 (Some(a), Some(b)) => Some(a.lerp(b, t)),
623 (Some(a), None) => Some(a.fade(1.0 - t)),
624 (None, Some(b)) => Some(b.fade(t)),
625 (None, None) => None,
626 }
627}
628
629impl BoxDecoration {
630 #[must_use]
654 pub fn lerp(self, other: BoxDecoration, t: f32) -> BoxDecoration {
655 if t.is_nan() || t <= 0.0 {
656 return self;
657 }
658 if t >= 1.0 {
659 return other;
660 }
661 let shape = if t < 0.5 { self.shape } else { other.shape };
664 let mix = |a: f32, b: f32| a + (b - a) * t;
665 let gradient = crate::Gradient::lerp(self.gradient.as_ref(), other.gradient.as_ref(), t);
667 let border = match (self.border, other.border) {
670 (Some(BoxBorder::Directional(a)), Some(BoxBorder::Directional(b))) => {
671 Some(BoxBorder::Directional(BorderDirectional {
672 top: lerp_side(a.top, b.top, t),
673 start: lerp_side(a.start, b.start, t),
674 end: lerp_side(a.end, b.end, t),
675 bottom: lerp_side(a.bottom, b.bottom, t),
676 }))
677 }
678 (Some(a), Some(b)) => Some(BoxBorder::Physical(
679 a.resolve(TextDirection::Ltr)
680 .lerp(b.resolve(TextDirection::Ltr), t),
681 )),
682 (Some(a), None) => Some(BoxBorder::Physical(
683 a.resolve(TextDirection::Ltr).lerp(Border::NONE, t),
684 )),
685 (None, Some(b)) => Some(BoxBorder::Physical(
686 Border::NONE.lerp(b.resolve(TextDirection::Ltr), t),
687 )),
688 (None, None) => None,
689 };
690 let grown = |s: BoxShadow, f: f32| BoxShadow {
694 color: s.color.fade(f),
695 offset: (s.offset.0 * f, s.offset.1 * f),
696 blur: s.blur * f,
697 spread: s.spread * f,
698 blur_style: s.blur_style,
699 };
700 let longest = self.shadows.len().max(other.shadows.len());
703 let shadows = (0..longest)
704 .map(|i| match (self.shadows.get(i), other.shadows.get(i)) {
705 (Some(a), Some(b)) => BoxShadow {
706 color: a.color.lerp(b.color, t),
707 offset: (mix(a.offset.0, b.offset.0), mix(a.offset.1, b.offset.1)),
708 blur: mix(a.blur, b.blur),
709 spread: mix(a.spread, b.spread),
710 blur_style: if a.blur_style == BlurStyle::Normal {
712 b.blur_style
713 } else {
714 a.blur_style
715 },
716 },
717 (Some(a), None) => grown(*a, 1.0 - t),
718 (None, Some(b)) => grown(*b, t),
719 (None, None) => unreachable!("within the longer list"),
720 })
721 .collect();
722 BoxDecoration {
723 color: fade_between(self.color, other.color, t),
724 gradient,
725 border,
726 radius: self.radius.lerp(other.radius, t),
727 shadows,
728 shape,
729 image: DecorationImage::lerp(self.image.as_ref(), other.image.as_ref(), t),
731 background_blend_mode: if t < 0.5 {
735 self.background_blend_mode
736 } else {
737 other.background_blend_mode
738 },
739 }
740 }
741}
742
743impl BoxDecoration {
744 pub fn filled(color: Color) -> Self {
746 Self {
747 color: Some(color),
748 ..Default::default()
749 }
750 }
751
752 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
755 self.radius = radius.into();
756 self
757 }
758
759 pub fn border(mut self, border: impl Into<BoxBorder>) -> Self {
761 self.border = Some(border.into());
762 self
763 }
764
765 pub fn shadow(mut self, shadow: BoxShadow) -> Self {
767 self.shadows.push(shadow);
768 self
769 }
770
771 pub fn shadows(mut self, shadows: impl IntoIterator<Item = BoxShadow>) -> Self {
773 self.shadows = shadows.into_iter().collect();
774 self
775 }
776
777 pub fn shape(mut self, shape: BoxShape) -> Self {
779 self.shape = shape;
780 self
781 }
782
783 pub fn gradient(mut self, gradient: impl Into<crate::Gradient>) -> Self {
786 self.gradient = Some(gradient.into());
787 self
788 }
789
790 pub fn image(mut self, image: DecorationImage) -> Self {
792 self.image = Some(image);
793 self
794 }
795
796 pub fn background_blend_mode(mut self, mode: crate::BlendMode) -> Self {
799 self.background_blend_mode = Some(mode);
800 self
801 }
802
803 pub fn content_padding(&self) -> Insets {
807 self.content_padding_in(TextDirection::Ltr)
808 }
809
810 pub fn content_padding_in(&self, direction: TextDirection) -> Insets {
812 self.border
813 .map(|b| b.resolve(direction).dimensions())
814 .unwrap_or(Insets::ZERO)
815 }
816
817 fn painted(&self, rect: Rect) -> (Rect, BorderRadius) {
820 match self.shape {
821 BoxShape::Rectangle => (rect, self.radius),
822 BoxShape::Circle => {
823 let side = rect.width.min(rect.height);
824 let square = Rect::new(
825 rect.x + (rect.width - side) * 0.5,
826 rect.y + (rect.height - side) * 0.5,
827 side,
828 side,
829 );
830 (square, BorderRadius::uniform(side * 0.5))
831 }
832 }
833 }
834
835 pub fn paint_into(&self, scene: &mut Scene, rect: Rect, opacity: f32) {
839 self.paint_into_in(scene, rect, opacity, TextDirection::Ltr);
840 }
841
842 pub fn paint_into_in(
847 &self,
848 scene: &mut Scene,
849 rect: Rect,
850 opacity: f32,
851 direction: TextDirection,
852 ) {
853 let (shape_rect, radius) = self.painted(rect);
854
855 for shadow in &self.shadows {
857 scene.styled_shadow(
858 shadow.bounds(shape_rect),
859 shadow.color.fade(opacity),
860 radius.inflate(shadow.blur + shadow.spread),
861 shadow.blur,
862 shadow.blur_style,
863 );
864 }
865
866 let border = self.border.map(|b| b.resolve(direction));
869 let uniform = border.filter(|b| b.is_uniform());
870 let (border_width, border_color) = match uniform {
871 Some(b) if b.top.width > 0.0 => (b.top.width, b.top.color.fade(opacity)),
872 _ => (0.0, Color::TRANSPARENT),
873 };
874 let has_border = uniform.is_some_and(|b| b.is_visible());
875 let image = self.image.as_ref();
876 let has_background = self.color.is_some() || self.gradient.is_some();
877 let blend = self
879 .background_blend_mode
880 .filter(|m| *m != crate::BlendMode::SrcOver && has_background);
881 let apart = image.is_some() || blend.is_some();
883 let (fill_border_width, fill_border_color) = if apart {
884 (0.0, Color::TRANSPARENT)
885 } else {
886 (border_width, border_color)
887 };
888 let fill = |scene: &mut Scene, radius: BorderRadius| match (self.color, &self.gradient) {
889 (_, Some(gradient)) => scene.shaded_rect(
890 shape_rect,
891 gradient.resolve(direction),
892 opacity,
893 radius,
894 fill_border_width,
895 fill_border_color,
896 ),
897 (Some(color), None) => scene.draw_rect(
898 shape_rect,
899 color.fade(opacity),
900 radius,
901 fill_border_width,
902 fill_border_color,
903 ),
904 (None, _) if has_border && !apart => scene.draw_rect(
906 shape_rect,
907 Color::TRANSPARENT,
908 radius,
909 border_width,
910 border_color,
911 ),
912 (None, _) => {}
914 };
915
916 match blend {
917 Some(mode) => {
921 let outer = scene.current_clip();
922 let (clip, shape) = self
923 .shape_clip(outer, shape_rect, radius)
924 .unwrap_or((outer.intersect(shape_rect), ClipShape::Rect));
925 scene.set_clip(clip);
926 scene.blended_in(mode, shape, |scene| fill(scene, BorderRadius::ZERO));
927 scene.set_clip(outer);
928 }
929 None => fill(scene, radius),
930 }
931
932 if let Some(image) = image {
935 self.paint_image(scene, image, rect, shape_rect, radius, direction, opacity);
936 }
937 if apart && has_border {
939 scene.draw_rect(
940 shape_rect,
941 Color::TRANSPARENT,
942 radius,
943 border_width,
944 border_color,
945 );
946 }
947
948 if let Some(border) = border.filter(|b| !b.is_uniform() && b.is_visible()) {
950 paint_sides(scene, shape_rect, radius, self.shape, &border, opacity);
951 }
952 }
953
954 #[allow(clippy::too_many_arguments)]
957 fn paint_image(
958 &self,
959 scene: &mut Scene,
960 image: &DecorationImage,
961 rect: Rect,
962 shape_rect: Rect,
963 radius: BorderRadius,
964 direction: TextDirection,
965 opacity: f32,
966 ) {
967 let start = scene.primitives().len();
968 image.paint(scene, rect, direction, opacity);
969 let Some((clip, clip_shape)) = self.shape_clip(scene.current_clip(), shape_rect, radius)
970 else {
971 return;
972 };
973 if scene.primitives().len() == start {
974 return;
975 }
976 let primitives = scene.split_off(start);
977 let owner = scene.current_owner();
978 scene.push_primitive(Primitive::Layer {
979 primitives,
980 opacity: 1.0,
981 clip,
982 clip_shape,
983 transform: None,
984 filter: crate::LayerFilter::NONE,
985 owner,
986 });
987 }
988
989 fn shape_clip(
995 &self,
996 outer: Rect,
997 shape_rect: Rect,
998 radius: BorderRadius,
999 ) -> Option<(Rect, ClipShape)> {
1000 let shape = match self.shape {
1001 BoxShape::Circle => ClipShape::Oval,
1002 BoxShape::Rectangle if radius != BorderRadius::ZERO => {
1003 ClipShape::RRect(radius.clamped())
1004 }
1005 BoxShape::Rectangle => return None,
1006 };
1007 Some(if outer.intersect(shape_rect) == shape_rect {
1008 (shape_rect, shape)
1009 } else {
1010 (
1011 outer.intersect(shape_rect),
1012 ClipShape::Path(ShapeBorder::rounded(radius).outline(shape_rect)),
1013 )
1014 })
1015 }
1016}
1017
1018fn paint_sides(
1022 scene: &mut Scene,
1023 rect: Rect,
1024 radius: BorderRadius,
1025 shape: BoxShape,
1026 border: &Border,
1027 opacity: f32,
1028) {
1029 let drawn: Vec<BorderSide> = border
1030 .sides()
1031 .into_iter()
1032 .filter(BorderSide::is_drawn)
1033 .collect();
1034 let one_colour = drawn.windows(2).all(|w| w[0].color == w[1].color);
1035 let rounded = shape == BoxShape::Circle || radius != BorderRadius::ZERO;
1036 let w = border.dimensions();
1037 let inner = Rect::new(
1038 rect.x + w.left,
1039 rect.y + w.top,
1040 (rect.width - w.left - w.right).max(0.0),
1041 (rect.height - w.top - w.bottom).max(0.0),
1042 );
1043 if one_colour && rounded {
1044 let color = drawn.first().map_or(Color::TRANSPARENT, |s| s.color);
1045 let less = |r: f32, a: f32, b: f32| (r - a.max(b)).max(0.0);
1048 let inner_radius = BorderRadius {
1049 top_left: less(radius.top_left, w.top, w.left),
1050 top_right: less(radius.top_right, w.top, w.right),
1051 bottom_right: less(radius.bottom_right, w.bottom, w.right),
1052 bottom_left: less(radius.bottom_left, w.bottom, w.left),
1053 };
1054 let band = ShapeBorder::rounded(radius)
1055 .outline(rect)
1056 .append(ShapeBorder::rounded(inner_radius).outline(inner));
1057 scene.fill_path(&band, color.fade(opacity));
1058 return;
1059 }
1060 let (l, t, r, b) = (rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
1062 let (il, it, ir, ib) = (
1063 inner.x,
1064 inner.y,
1065 inner.x + inner.width,
1066 inner.y + inner.height,
1067 );
1068 let quads = [
1069 (border.top, [(l, t), (r, t), (ir, it), (il, it)]),
1070 (border.right, [(r, t), (r, b), (ir, ib), (ir, it)]),
1071 (border.bottom, [(r, b), (l, b), (il, ib), (ir, ib)]),
1072 (border.left, [(l, b), (l, t), (il, it), (il, ib)]),
1073 ];
1074 for (side, [a, b2, c, d]) in quads {
1075 if !side.is_drawn() {
1076 continue;
1077 }
1078 let path = Path::new()
1079 .move_to(Point::new(a.0, a.1))
1080 .line_to(Point::new(b2.0, b2.1))
1081 .line_to(Point::new(c.0, c.1))
1082 .line_to(Point::new(d.0, d.1))
1083 .close();
1084 scene.fill_path(&path, side.color.fade(opacity));
1085 }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 #[test]
1093 fn a_directional_radius_follows_the_reading_direction() {
1094 let end = BorderRadiusDirectional::end(16.0);
1095 assert_eq!(
1096 end.resolve(TextDirection::Ltr),
1097 BorderRadius::right(16.0),
1098 "the line ends on the right in English"
1099 );
1100 assert_eq!(
1101 end.resolve(TextDirection::Rtl),
1102 BorderRadius::left(16.0),
1103 "and on the left in Arabic"
1104 );
1105
1106 let start = BorderRadiusDirectional::start(16.0);
1107 assert_eq!(start.resolve(TextDirection::Ltr), BorderRadius::left(16.0));
1108 assert_eq!(start.resolve(TextDirection::Rtl), BorderRadius::right(16.0));
1109
1110 let both = BorderRadiusDirectional::horizontal(4.0, 12.0);
1112 assert_eq!(
1113 both.resolve(TextDirection::Ltr),
1114 BorderRadius {
1115 top_left: 4.0,
1116 top_right: 12.0,
1117 bottom_right: 12.0,
1118 bottom_left: 4.0,
1119 }
1120 );
1121 assert_eq!(
1122 both.resolve(TextDirection::Rtl),
1123 BorderRadius {
1124 top_left: 12.0,
1125 top_right: 4.0,
1126 bottom_right: 4.0,
1127 bottom_left: 12.0,
1128 }
1129 );
1130
1131 for direction in [TextDirection::Ltr, TextDirection::Rtl] {
1133 assert_eq!(
1134 BorderRadiusDirectional::uniform(8.0).resolve(direction),
1135 BorderRadius::uniform(8.0)
1136 );
1137 assert_eq!(
1138 BorderRadiusDirectional::vertical(8.0, 0.0).resolve(direction),
1139 BorderRadius::top(8.0)
1140 );
1141 }
1142 }
1143 use super::*;
1144 use crate::Primitive;
1145
1146 fn rect() -> Rect {
1147 Rect::new(10.0, 20.0, 100.0, 40.0)
1148 }
1149
1150 #[test]
1151 fn content_padding_reserves_the_border() {
1152 let deco = BoxDecoration::filled(Color::WHITE).border(Border::new(2.0, Color::BLACK));
1153 assert_eq!(deco.content_padding(), Insets::uniform(2.0));
1154 let none = BoxDecoration::filled(Color::WHITE).border(Border::new(0.0, Color::BLACK));
1156 assert_eq!(none.content_padding(), Insets::ZERO);
1157 }
1158
1159 #[test]
1160 fn empty_decoration_paints_nothing() {
1161 let mut scene = Scene::new();
1162 BoxDecoration::default().paint_into(&mut scene, rect(), 1.0);
1163 assert!(scene.is_empty());
1164 }
1165
1166 #[test]
1167 fn fixed_paint_order_shadow_then_fill() {
1168 let mut scene = Scene::new();
1169 let deco = BoxDecoration::filled(Color::WHITE)
1170 .radius(6.0)
1171 .shadow(BoxShadow::new(
1172 0.0,
1173 4.0,
1174 8.0,
1175 Color::rgba(0.0, 0.0, 0.0, 0.5),
1176 ));
1177 deco.paint_into(&mut scene, rect(), 1.0);
1178 assert_eq!(scene.len(), 2);
1180 match scene.primitives()[0] {
1181 Primitive::Rect { blur, .. } => {
1182 assert!(blur > 0.0, "first primitive = the blurred shadow")
1183 }
1184 _ => panic!("expected a rectangle"),
1185 }
1186 match scene.primitives()[1] {
1187 Primitive::Rect { blur, color, .. } => {
1188 assert_eq!(blur, 0.0, "second primitive = the crisp background");
1189 assert_eq!(color, Color::WHITE);
1190 }
1191 _ => panic!("expected a rectangle"),
1192 }
1193 }
1194
1195 #[test]
1196 fn opacity_fades_all_colours() {
1197 let mut scene = Scene::new();
1198 BoxDecoration::filled(Color::rgb(1.0, 0.0, 0.0))
1199 .border(Border::new(2.0, Color::rgb(0.0, 1.0, 0.0)))
1200 .paint_into(&mut scene, rect(), 0.5);
1201 match scene.primitives()[0] {
1202 Primitive::Rect {
1203 color,
1204 border_color,
1205 ..
1206 } => {
1207 assert_eq!(color.a, 0.5);
1208 assert_eq!(border_color.a, 0.5);
1209 }
1210 _ => panic!("expected a rectangle"),
1211 }
1212 }
1213
1214 #[test]
1215 fn border_only_paints_transparent_fill_with_stroke() {
1216 let mut scene = Scene::new();
1217 BoxDecoration::default()
1218 .border(Border::new(1.0, Color::WHITE))
1219 .paint_into(&mut scene, rect(), 1.0);
1220 assert_eq!(scene.len(), 1);
1221 match scene.primitives()[0] {
1222 Primitive::Rect {
1223 color,
1224 border_width,
1225 ..
1226 } => {
1227 assert_eq!(color, Color::TRANSPARENT);
1228 assert_eq!(border_width, 1.0);
1229 }
1230 _ => panic!("expected a rectangle"),
1231 }
1232 }
1233
1234 #[test]
1235 fn shadow_bounds_grow_with_blur_and_spread() {
1236 let s = BoxShadow::new(0.0, 0.0, 4.0, Color::BLACK).spread(2.0);
1237 let b = s.bounds(Rect::new(0.0, 0.0, 10.0, 10.0));
1238 assert_eq!(b, Rect::new(-6.0, -6.0, 22.0, 22.0));
1240 }
1241}
1242
1243#[cfg(test)]
1245mod lerp_tests {
1246 use super::*;
1247
1248 const RED: Color = Color {
1249 r: 1.0,
1250 g: 0.0,
1251 b: 0.0,
1252 a: 1.0,
1253 };
1254 const BLUE: Color = Color {
1255 r: 0.0,
1256 g: 0.0,
1257 b: 1.0,
1258 a: 1.0,
1259 };
1260
1261 #[test]
1264 fn the_ends_are_the_ends_exactly() {
1265 let a = BoxDecoration::filled(RED).radius(4.0);
1266 let b = BoxDecoration::filled(BLUE).border(Border::new(2.0, RED));
1267 let lerp = |t: f32| a.clone().lerp(b.clone(), t);
1268 assert_eq!(lerp(0.0), a);
1269 assert_eq!(lerp(1.0), b);
1270 assert_eq!(lerp(-0.5), a, "an undershoot stops at the start");
1271 assert_eq!(lerp(1.5), b, "an overshoot stops at the end");
1272 assert_eq!(lerp(f32::NAN), a, "and no progress is no progress");
1273 }
1274
1275 #[test]
1279 fn a_fill_fading_out_does_not_pass_through_black() {
1280 let mid = BoxDecoration::filled(RED).lerp(BoxDecoration::default(), 0.5);
1281 let c = mid.color.expect("still painted, half faded");
1282 assert_eq!((c.r, c.g, c.b), (1.0, 0.0, 0.0), "the same red");
1283 assert!((c.a - 0.5).abs() < 1e-6, "at half its opacity: {c:?}");
1284 let naive = RED.lerp(Color::TRANSPARENT, 0.5);
1285 assert!(naive.r < 0.6, "the trap this avoids: {naive:?}");
1286 }
1287
1288 #[test]
1290 fn a_fill_fading_in_arrives_in_its_own_colour() {
1291 let c = BoxDecoration::default()
1292 .lerp(BoxDecoration::filled(BLUE), 0.25)
1293 .color
1294 .expect("already there, faintly");
1295 assert_eq!((c.r, c.g, c.b), (0.0, 0.0, 1.0));
1296 assert!((c.a - 0.25).abs() < 1e-6, "{c:?}");
1297 }
1298
1299 #[test]
1302 fn a_gradient_on_one_side_fades_in() {
1303 let flat = BoxDecoration::filled(RED);
1304 let graded =
1305 BoxDecoration::filled(RED).gradient(crate::LinearGradient::new(vec![RED, BLUE]));
1306 let mid = flat.lerp(graded, 0.25);
1307 assert_eq!(mid.color, Some(RED), "the fill does not move");
1308 let g = mid.gradient.expect("a gradient fading in");
1309 assert_eq!(g.colors()[1], BLUE.fade(0.25));
1310 }
1311
1312 #[test]
1315 fn a_border_arrives_by_thickening_in_its_own_colour() {
1316 let lined = BoxDecoration::filled(RED).border(Border::new(4.0, BLUE));
1317 let quarter = BoxDecoration::filled(RED)
1318 .lerp(lined, 0.25)
1319 .border
1320 .expect("a line already")
1321 .resolve(TextDirection::Ltr);
1322 assert_eq!(quarter.top.width, 1.0);
1323 assert_eq!(quarter.top.color, BLUE);
1324 }
1325
1326 #[test]
1328 fn a_shadow_grows_and_fades_in() {
1329 let raised = BoxDecoration::filled(RED).shadow(BoxShadow::new(
1330 0.0,
1331 8.0,
1332 16.0,
1333 Color::rgba(0.0, 0.0, 0.0, 0.4),
1334 ));
1335 let half = BoxDecoration::filled(RED).lerp(raised, 0.5).shadows[0];
1336 assert_eq!(half.offset, (0.0, 4.0));
1337 assert_eq!(half.blur, 8.0);
1338 assert!((half.color.a - 0.2).abs() < 1e-6, "{:?}", half.color);
1339 }
1340
1341 #[test]
1343 fn corners_travel_one_by_one() {
1344 let r = BoxDecoration::default()
1345 .radius(BorderRadius::top(8.0))
1346 .lerp(BoxDecoration::default().radius(16.0), 0.5)
1347 .radius;
1348 assert_eq!((r.top_left, r.bottom_left), (12.0, 8.0));
1349 }
1350
1351 #[test]
1354 fn a_height_of_the_table_casts_its_three_shadows() {
1355 let [umbra, penumbra, ambient] = BoxShadow::for_elevation(3.0, Color::BLACK);
1356 assert_eq!(
1357 (umbra.offset, umbra.blur, umbra.spread),
1358 ((0.0, 3.0), 3.0, -2.0)
1359 );
1360 assert_eq!(
1361 (penumbra.offset, penumbra.blur, penumbra.spread),
1362 ((0.0, 3.0), 4.0, 0.0)
1363 );
1364 assert_eq!(
1365 (ambient.offset, ambient.blur, ambient.spread),
1366 ((0.0, 1.0), 8.0, 0.0)
1367 );
1368 assert_eq!(
1369 [umbra.color.a, penumbra.color.a, ambient.color.a],
1370 [0.2, 0.14, 0.12]
1371 );
1372 let [_, twenty_four, _] = BoxShadow::for_elevation(24.0, Color::BLACK);
1373 assert_eq!((twenty_four.offset, twenty_four.blur), ((0.0, 24.0), 38.0));
1374 }
1375
1376 #[test]
1379 fn heights_between_and_beyond_the_table() {
1380 let [_, half, _] = BoxShadow::for_elevation(5.0, Color::BLACK);
1381 assert_eq!(
1382 (half.offset.1, half.blur),
1383 (5.0, 7.5),
1384 "half way from 4 to 6"
1385 );
1386 assert_eq!(
1387 BoxShadow::for_elevation(40.0, Color::BLACK),
1388 BoxShadow::for_elevation(24.0, Color::BLACK)
1389 );
1390 let [faint, ..] = BoxShadow::for_elevation(1.0, Color::rgba(0.0, 0.0, 1.0, 0.5));
1391 assert_eq!(faint.color, Color::rgba(0.0, 0.0, 1.0, 0.1));
1392 let [low, ..] = BoxShadow::for_elevation(0.5, Color::BLACK);
1394 assert!((low.color.a - 0.1).abs() < 1e-6, "{low:?}");
1395 }
1396
1397 #[test]
1401 fn a_height_paints_three_shadows_and_nought_paints_none() {
1402 let rect = Rect::new(10.0, 20.0, 100.0, 40.0);
1403 let mut scene = Scene::new();
1404 paint_elevation(
1405 &mut scene,
1406 rect,
1407 BorderRadius::uniform(8.0),
1408 3.0,
1409 Color::BLACK,
1410 );
1411 let shadows: Vec<_> = scene
1412 .primitives()
1413 .iter()
1414 .filter_map(|p| match p {
1415 crate::Primitive::Rect {
1416 rect, radius, blur, ..
1417 } => Some((*rect, *radius, *blur)),
1418 _ => None,
1419 })
1420 .collect();
1421 assert_eq!(shadows.len(), 3);
1422 let (penumbra, radius, blur) = shadows[1];
1423 assert_eq!(blur, 4.0);
1424 assert_eq!(
1425 penumbra,
1426 Rect::new(6.0, 19.0, 108.0, 48.0),
1427 "dropped 3, grown 4"
1428 );
1429 assert_eq!(radius, BorderRadius::uniform(12.0));
1430
1431 for (height, colour) in [(0.0, Color::BLACK), (3.0, Color::TRANSPARENT)] {
1432 let mut none = Scene::new();
1433 paint_elevation(&mut none, rect, BorderRadius::ZERO, height, colour);
1434 assert!(none.primitives().is_empty(), "{height} {colour:?}");
1435 }
1436 }
1437}
1438
1439#[cfg(test)]
1442mod box_decoration_tests {
1443 use super::*;
1444 use crate::{PathVerb, Primitive};
1445
1446 const RED: Color = Color::rgb(1.0, 0.0, 0.0);
1447 const GREEN: Color = Color::rgb(0.0, 1.0, 0.0);
1448 const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
1449
1450 fn painted(deco: &BoxDecoration, rect: Rect, direction: TextDirection) -> Vec<Primitive> {
1451 let mut scene = Scene::new();
1452 deco.paint_into_in(&mut scene, rect, 1.0, direction);
1453 scene.primitives().to_vec()
1454 }
1455
1456 #[test]
1459 fn a_circle_is_centred_on_the_shorter_side() {
1460 let deco = BoxDecoration::filled(RED)
1461 .radius(3.0)
1462 .shape(BoxShape::Circle)
1463 .shadow(BoxShadow::new(0.0, 0.0, 4.0, BLUE));
1464 let prims = painted(&deco, Rect::new(0.0, 0.0, 100.0, 40.0), TextDirection::Ltr);
1465 match &prims[1] {
1466 Primitive::Rect {
1467 rect,
1468 radius,
1469 color,
1470 ..
1471 } => {
1472 assert_eq!(*color, RED);
1473 assert_eq!(*rect, Rect::new(30.0, 0.0, 40.0, 40.0));
1474 assert_eq!(*radius, BorderRadius::uniform(20.0), "round, not 3");
1475 }
1476 other => panic!("the fill, not {other:?}"),
1477 }
1478 match &prims[0] {
1479 Primitive::Rect { radius, .. } => {
1480 assert_eq!(*radius, BorderRadius::uniform(24.0), "a round shadow")
1481 }
1482 other => panic!("the shadow, not {other:?}"),
1483 }
1484 }
1485
1486 #[test]
1488 fn the_shadows_are_painted_in_order_behind_the_box() {
1489 let deco = BoxDecoration::filled(RED).shadows([
1490 BoxShadow::new(0.0, 1.0, 2.0, GREEN),
1491 BoxShadow::new(0.0, 4.0, 8.0, BLUE),
1492 ]);
1493 let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 50.0), TextDirection::Ltr);
1494 let colours: Vec<Color> = prims
1495 .iter()
1496 .filter_map(|p| match p {
1497 Primitive::Rect { color, .. } => Some(*color),
1498 _ => None,
1499 })
1500 .collect();
1501 assert_eq!(colours, vec![GREEN, BLUE, RED]);
1502 }
1503
1504 #[test]
1507 fn sides_of_different_colours_are_drawn_one_by_one() {
1508 let border = Border {
1509 top: BorderSide::new(RED, 2.0),
1510 right: BorderSide::new(GREEN, 4.0),
1511 bottom: BorderSide::new(BLUE, 2.0),
1512 left: BorderSide::NONE,
1513 };
1514 let deco = BoxDecoration::default().border(border);
1515 let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), TextDirection::Ltr);
1516 let fills: Vec<Color> = prims
1517 .iter()
1518 .filter_map(|p| match p {
1519 Primitive::Path { fill, .. } => *fill,
1520 _ => None,
1521 })
1522 .collect();
1523 assert_eq!(
1524 fills,
1525 vec![RED, GREEN, BLUE],
1526 "three sides, the fourth has none"
1527 );
1528 assert_eq!(deco.content_padding(), Insets::new(2.0, 4.0, 2.0, 0.0));
1529 let rounded = BoxDecoration::default().border(border).radius(8.0);
1532 let fills: Vec<Color> = painted(
1533 &rounded,
1534 Rect::new(0.0, 0.0, 50.0, 30.0),
1535 TextDirection::Ltr,
1536 )
1537 .iter()
1538 .filter_map(|p| match p {
1539 Primitive::Path { fill, .. } => *fill,
1540 _ => None,
1541 })
1542 .collect();
1543 assert_eq!(fills, vec![RED, GREEN, BLUE]);
1544 }
1545
1546 #[test]
1549 fn one_colour_with_corners_is_one_band() {
1550 let border = Border {
1551 top: BorderSide::new(RED, 6.0),
1552 ..Border::new(2.0, RED)
1553 };
1554 let deco = BoxDecoration::default().border(border).radius(8.0);
1555 let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), TextDirection::Ltr);
1556 let bands: Vec<&Primitive> = prims
1557 .iter()
1558 .filter(|p| matches!(p, Primitive::Path { .. }))
1559 .collect();
1560 assert_eq!(bands.len(), 1, "one band");
1561 match bands[0] {
1562 Primitive::Path { path, fill, .. } => {
1563 assert_eq!(*fill, Some(RED));
1564 let moves = path
1565 .verbs()
1566 .iter()
1567 .filter(|v| matches!(v, PathVerb::MoveTo(_)))
1568 .count();
1569 assert_eq!(moves, 2, "the outline and the hole in it");
1570 let hole = path
1573 .verbs()
1574 .iter()
1575 .filter_map(|v| match v {
1576 PathVerb::MoveTo(p) => Some(*p),
1577 _ => None,
1578 })
1579 .nth(1)
1580 .expect("the hole");
1581 assert_eq!((hole.x, hole.y), (4.0, 6.0));
1582 }
1583 _ => unreachable!(),
1584 }
1585 }
1586
1587 #[test]
1589 fn a_uniform_border_is_drawn_with_the_fill() {
1590 let deco = BoxDecoration::filled(RED).border(Border::new(3.0, BLUE));
1591 let prims = painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), TextDirection::Ltr);
1592 assert_eq!(prims.len(), 1);
1593 match &prims[0] {
1594 Primitive::Rect {
1595 border_width,
1596 border_color,
1597 ..
1598 } => assert_eq!((*border_width, *border_color), (3.0, BLUE)),
1599 other => panic!("{other:?}"),
1600 }
1601 }
1602
1603 #[test]
1606 fn a_directional_border_follows_the_script() {
1607 let border = BorderDirectional {
1608 start: BorderSide::new(RED, 5.0),
1609 ..Default::default()
1610 };
1611 let deco = BoxDecoration::default().border(border);
1612 assert_eq!(deco.content_padding_in(TextDirection::Ltr).left, 5.0);
1613 assert_eq!(deco.content_padding_in(TextDirection::Rtl).right, 5.0);
1614 let first_x =
1615 |direction| match &painted(&deco, Rect::new(0.0, 0.0, 50.0, 30.0), direction)[0] {
1616 Primitive::Path { path, .. } => match path.verbs()[0] {
1617 PathVerb::MoveTo(p) => p.x,
1618 _ => unreachable!(),
1619 },
1620 other => panic!("{other:?}"),
1621 };
1622 assert_eq!(first_x(TextDirection::Ltr), 0.0);
1625 assert_eq!(first_x(TextDirection::Rtl), 50.0);
1626 }
1627
1628 #[test]
1631 fn shadow_lists_pair_up_and_shapes_swap_half_way() {
1632 let one = BoxDecoration::default().shadow(BoxShadow::new(0.0, 2.0, 4.0, BLUE));
1633 let two = BoxDecoration::default().shape(BoxShape::Circle).shadows([
1634 BoxShadow::new(0.0, 6.0, 8.0, BLUE),
1635 BoxShadow::new(0.0, 10.0, 20.0, RED),
1636 ]);
1637 let mid = one.clone().lerp(two.clone(), 0.5);
1638 assert_eq!(mid.shadows.len(), 2);
1639 assert_eq!(mid.shadows[0].offset, (0.0, 4.0));
1640 assert_eq!(
1641 mid.shadows[1].offset,
1642 (0.0, 5.0),
1643 "the extra grows from nothing"
1644 );
1645 assert!((mid.shadows[1].color.a - 0.5).abs() < 1e-6);
1646 assert_eq!(mid.shape, BoxShape::Circle);
1647 assert_eq!(one.lerp(two, 0.49).shape, BoxShape::Rectangle);
1648 }
1649
1650 #[test]
1652 fn the_border_constructors() {
1653 let side = BorderSide::new(RED, 1.0);
1654 let other = BorderSide::new(BLUE, 2.0);
1655 assert!(Border::all(side).is_uniform());
1656 let sym = Border::symmetric(side, other);
1657 assert_eq!(
1658 (sym.left, sym.right, sym.top, sym.bottom),
1659 (side, side, other, other)
1660 );
1661 assert!(!sym.is_uniform());
1662 assert!(!Border::NONE.is_visible());
1663 let half = Border::NONE.lerp(Border::all(other), 0.5);
1664 assert_eq!(
1665 half.top,
1666 BorderSide::new(BLUE, 1.0),
1667 "thickening in its own colour"
1668 );
1669 }
1670
1671 fn picture() -> crate::DecorationImage {
1672 let pixels = crate::ImageData::from_rgba(4, 4, vec![255; 64]).into_handle();
1673 crate::DecorationImage::new(pixels).fit(crate::BoxFit::Fill)
1674 }
1675
1676 fn kinds(primitives: &[Primitive]) -> Vec<String> {
1678 primitives
1679 .iter()
1680 .map(|p| match p {
1681 Primitive::Rect {
1682 border_width,
1683 color,
1684 blur,
1685 ..
1686 } if *blur > 0.0 => format!("shadow {color:?} {border_width}"),
1687 Primitive::Rect {
1688 border_width,
1689 color,
1690 ..
1691 } => format!("rect {color:?} {border_width}"),
1692 Primitive::Image { .. } => "image".to_string(),
1693 Primitive::Layer { primitives, .. } => {
1694 format!("layer [{}]", kinds(primitives).join(", "))
1695 }
1696 other => format!("{other:?}"),
1697 })
1698 .collect()
1699 }
1700
1701 fn painted_clipped(decoration: &BoxDecoration, clip: Rect) -> Scene {
1702 let mut scene = Scene::new();
1703 scene.set_clip(clip);
1704 decoration.paint_into(&mut scene, Rect::new(0.0, 0.0, 40.0, 20.0), 1.0);
1705 scene
1706 }
1707
1708 #[test]
1712 fn a_picture_lies_between_the_fill_and_the_border() {
1713 let decoration = BoxDecoration::filled(RED)
1714 .border(Border::new(2.0, BLUE))
1715 .image(picture());
1716 let scene = painted_clipped(&decoration, Rect::UNBOUNDED);
1717 assert_eq!(
1718 kinds(scene.primitives()),
1719 vec![
1720 format!("rect {RED:?} 0"),
1721 "image".to_string(),
1722 format!("rect {:?} 2", Color::TRANSPARENT),
1723 ]
1724 );
1725 let outline = BoxDecoration::default()
1727 .border(Border::new(2.0, BLUE))
1728 .image(picture());
1729 assert_eq!(
1730 kinds(painted_clipped(&outline, Rect::UNBOUNDED).primitives()),
1731 vec![
1732 "image".to_string(),
1733 format!("rect {:?} 2", Color::TRANSPARENT)
1734 ]
1735 );
1736 let plain = BoxDecoration::filled(RED).border(Border::new(2.0, BLUE));
1738 assert_eq!(
1739 kinds(painted_clipped(&plain, Rect::UNBOUNDED).primitives()),
1740 vec![format!("rect {RED:?} 2")]
1741 );
1742 let alone = BoxDecoration::default().image(picture());
1744 assert_eq!(
1745 kinds(painted_clipped(&alone, Rect::UNBOUNDED).primitives()),
1746 vec!["image"]
1747 );
1748 }
1749
1750 #[test]
1752 fn a_picture_takes_the_shape_of_its_box() {
1753 let rounded = BoxDecoration::default().radius(6.0).image(picture());
1754 let scene = painted_clipped(&rounded, Rect::UNBOUNDED);
1755 let Primitive::Layer {
1756 clip,
1757 clip_shape,
1758 primitives,
1759 ..
1760 } = &scene.primitives()[0]
1761 else {
1762 panic!("a clipped layer");
1763 };
1764 assert_eq!(*clip, Rect::new(0.0, 0.0, 40.0, 20.0));
1765 assert_eq!(*clip_shape, ClipShape::RRect(BorderRadius::uniform(6.0)));
1766 let Primitive::Image { rect, .. } = &primitives[0] else {
1767 panic!("the picture");
1768 };
1769 assert_eq!(*rect, Rect::new(0.0, 0.0, 40.0, 20.0), "filling the box");
1770
1771 let circle = BoxDecoration::default()
1772 .shape(BoxShape::Circle)
1773 .image(picture());
1774 let scene = painted_clipped(&circle, Rect::UNBOUNDED);
1775 let Primitive::Layer {
1776 clip,
1777 clip_shape,
1778 primitives,
1779 ..
1780 } = &scene.primitives()[0]
1781 else {
1782 panic!("a clipped layer");
1783 };
1784 assert_eq!(
1785 *clip,
1786 Rect::new(10.0, 0.0, 20.0, 20.0),
1787 "the circle's square"
1788 );
1789 assert_eq!(*clip_shape, ClipShape::Oval);
1790 let Primitive::Image { rect, .. } = &primitives[0] else {
1791 panic!("the picture");
1792 };
1793 assert_eq!(*rect, Rect::new(0.0, 0.0, 40.0, 20.0), "into the whole box");
1794
1795 let square = BoxDecoration::default().image(picture());
1797 assert!(matches!(
1798 painted_clipped(&square, Rect::UNBOUNDED).primitives()[0],
1799 Primitive::Image { .. }
1800 ));
1801 }
1802
1803 #[test]
1806 fn a_partly_hidden_box_clips_its_picture_by_its_outline() {
1807 let rounded = BoxDecoration::default().radius(6.0).image(picture());
1808 let scene = painted_clipped(&rounded, Rect::new(0.0, 0.0, 30.0, 100.0));
1809 let Primitive::Layer {
1810 clip, clip_shape, ..
1811 } = &scene.primitives()[0]
1812 else {
1813 panic!("a clipped layer");
1814 };
1815 assert_eq!(*clip, Rect::new(0.0, 0.0, 30.0, 20.0));
1816 let ClipShape::Path(path) = clip_shape else {
1817 panic!("the outline, as a path");
1818 };
1819 assert_eq!(
1820 *path,
1821 ShapeBorder::rounded(6.0).outline(Rect::new(0.0, 0.0, 40.0, 20.0))
1822 );
1823 }
1824
1825 #[test]
1826 fn a_picture_on_one_side_fades() {
1827 let shown = BoxDecoration::default().image(picture());
1828 let mid = BoxDecoration::default().lerp(shown.clone(), 0.25);
1829 let scene = painted_clipped(&mid, Rect::UNBOUNDED);
1830 let Primitive::Image { tint, .. } = &scene.primitives()[0] else {
1831 panic!("the picture");
1832 };
1833 assert_eq!(tint.a, 0.25);
1834 assert_eq!(
1835 BoxDecoration::default().lerp(shown.clone(), 1.0).image,
1836 shown.image
1837 );
1838 }
1839
1840 #[test]
1843 fn a_shadow_keeps_its_blur_style() {
1844 let glow = BoxShadow::new(0.0, 0.0, 8.0, BLUE).blur_style(BlurStyle::Outer);
1845 let scene = painted_clipped(&BoxDecoration::default().shadow(glow), Rect::UNBOUNDED);
1846 let Primitive::Rect { blur_style, .. } = &scene.primitives()[0] else {
1847 panic!("the shadow");
1848 };
1849 assert_eq!(*blur_style, BlurStyle::Outer);
1850 let plain = BoxShadow::new(0.0, 0.0, 8.0, BLUE);
1851 let solid = plain.blur_style(BlurStyle::Solid);
1852 let mix = |a: BoxShadow, b: BoxShadow| {
1853 BoxDecoration::default()
1854 .shadow(a)
1855 .lerp(BoxDecoration::default().shadow(b), 0.5)
1856 .shadows[0]
1857 .blur_style
1858 };
1859 assert_eq!(mix(plain, solid), BlurStyle::Solid);
1860 assert_eq!(mix(solid, plain), BlurStyle::Solid);
1861 assert_eq!(mix(glow, solid), BlurStyle::Outer);
1862 let arriving = BoxDecoration::default().lerp(BoxDecoration::default().shadow(glow), 0.5);
1864 assert_eq!(arriving.shadows[0].blur_style, BlurStyle::Outer);
1865 assert_eq!(plain.blur_style, BlurStyle::Normal, "plain by default");
1866 assert!(BoxShadow::for_elevation(4.0, BLUE)
1867 .iter()
1868 .all(|s| s.blur_style == BlurStyle::Normal));
1869 }
1870
1871 fn blended(mode: crate::BlendMode) -> BoxDecoration {
1872 BoxDecoration::filled(RED).background_blend_mode(mode)
1873 }
1874
1875 fn group(scene: &Scene) -> (Rect, ClipShape, Option<crate::BlendMode>, Vec<Primitive>) {
1877 match &scene.primitives()[0] {
1878 Primitive::Layer {
1879 clip,
1880 clip_shape,
1881 filter,
1882 primitives,
1883 ..
1884 } => (*clip, clip_shape.clone(), filter.blend, primitives.clone()),
1885 other => panic!("a group, not {other:?}"),
1886 }
1887 }
1888
1889 #[test]
1892 fn a_blended_background_is_a_group_in_the_box_shape() {
1893 let multiply = crate::BlendMode::Multiply;
1894 let rounded = blended(multiply).radius(6.0);
1895 let (clip, shape, mode, inside) = group(&painted_clipped(&rounded, Rect::UNBOUNDED));
1896 assert_eq!(clip, Rect::new(0.0, 0.0, 40.0, 20.0));
1897 assert_eq!(shape, ClipShape::RRect(BorderRadius::uniform(6.0)));
1898 assert_eq!(mode, Some(multiply));
1899 let [Primitive::Rect {
1900 radius,
1901 color,
1902 border_width,
1903 ..
1904 }] = inside.as_slice()
1905 else {
1906 panic!("the fill alone");
1907 };
1908 assert_eq!(
1909 *radius,
1910 BorderRadius::ZERO,
1911 "square: the group gives the shape"
1912 );
1913 assert_eq!(*color, RED);
1914 assert_eq!(*border_width, 0.0);
1915
1916 let circle = blended(multiply).shape(BoxShape::Circle);
1917 let (clip, shape, ..) = group(&painted_clipped(&circle, Rect::UNBOUNDED));
1918 assert_eq!(clip, Rect::new(10.0, 0.0, 20.0, 20.0));
1919 assert_eq!(shape, ClipShape::Oval);
1920
1921 let square = blended(multiply);
1923 let (clip, shape, ..) = group(&painted_clipped(&square, Rect::new(0.0, 0.0, 30.0, 50.0)));
1924 assert_eq!(clip, Rect::new(0.0, 0.0, 30.0, 20.0));
1925 assert_eq!(shape, ClipShape::Rect);
1926
1927 let (_, shape, ..) = group(&painted_clipped(&rounded, Rect::new(0.0, 0.0, 30.0, 50.0)));
1929 assert!(matches!(shape, ClipShape::Path(_)), "the outline");
1930 }
1931
1932 #[test]
1935 fn a_blended_background_leaves_the_border_plain() {
1936 let lined = blended(crate::BlendMode::Screen).border(Border::new(2.0, BLUE));
1937 let scene = painted_clipped(&lined, Rect::new(0.0, 0.0, 100.0, 100.0));
1938 assert_eq!(
1939 kinds(scene.primitives()),
1940 vec![
1941 format!("layer [rect {RED:?} 0]"),
1942 format!("rect {:?} 2", Color::TRANSPARENT),
1943 ]
1944 );
1945 assert_eq!(scene.current_clip(), Rect::new(0.0, 0.0, 100.0, 100.0));
1946 }
1947
1948 #[test]
1950 fn an_ordinary_or_empty_background_is_not_grouped() {
1951 let over = blended(crate::BlendMode::SrcOver);
1952 assert_eq!(
1953 kinds(painted_clipped(&over, Rect::UNBOUNDED).primitives()),
1954 vec![format!("rect {RED:?} 0")]
1955 );
1956 let empty = BoxDecoration::default()
1957 .border(Border::new(2.0, BLUE))
1958 .background_blend_mode(crate::BlendMode::Multiply);
1959 assert_eq!(
1960 kinds(painted_clipped(&empty, Rect::UNBOUNDED).primitives()),
1961 vec![format!("rect {:?} 2", Color::TRANSPARENT)]
1962 );
1963 let graded = BoxDecoration::default()
1964 .gradient(crate::LinearGradient::new(vec![RED, BLUE]))
1965 .background_blend_mode(crate::BlendMode::Multiply);
1966 let (.., inside) = group(&painted_clipped(&graded, Rect::UNBOUNDED));
1967 assert!(
1968 matches!(
1969 &inside[0],
1970 Primitive::Rect {
1971 shader: Some(_),
1972 ..
1973 }
1974 ),
1975 "a gradient is a background too"
1976 );
1977 }
1978
1979 #[test]
1981 fn a_blend_mode_changes_half_way() {
1982 let plain = BoxDecoration::filled(RED);
1983 let multiplied = blended(crate::BlendMode::Multiply);
1984 assert_eq!(
1985 plain
1986 .clone()
1987 .lerp(multiplied.clone(), 0.4)
1988 .background_blend_mode,
1989 None
1990 );
1991 assert_eq!(
1992 plain.lerp(multiplied.clone(), 0.5).background_blend_mode,
1993 Some(crate::BlendMode::Multiply)
1994 );
1995 assert_eq!(
1996 multiplied
1997 .lerp(BoxDecoration::filled(RED), 0.4)
1998 .background_blend_mode,
1999 Some(crate::BlendMode::Multiply)
2000 );
2001 }
2002}