Skip to main content

frus_core/
decoration.rs

1//! The **box decoration** model: the vocabulary for painting a rectangle
2//! (background, gradient, border, rounded corners, shadow), independent of any
3//! widget or theme.
4//!
5//! A [`BoxDecoration`] is a pure `Copy` value that a widget assembles at paint
6//! time, then **lowers** into [`Scene`] primitives through
7//! [`BoxDecoration::paint_into`], in a **fixed order**: shadow → background
8//! (colour or gradient) → picture → border. It feeds layout too:
9//! [`BoxDecoration::content_padding`] reserves room for the border on taffy's
10//! behalf.
11
12use crate::{
13    BlurStyle, BorderSide, ClipShape, Color, DecorationImage, Insets, Path, Point, Primitive, Rect,
14    Scene, ShapeBorder, TextDirection,
15};
16
17/// Corner radii, **per corner** (logical px). `From<f32>` covers the uniform case:
18/// anywhere a radius is expected, a plain `10.0` still works.
19#[derive(Clone, Copy, Debug, Default, PartialEq)]
20pub struct BorderRadius {
21    /// The top-left corner's radius, in logical pixels.
22    pub top_left: f32,
23    /// The top-right corner's radius, in logical pixels.
24    pub top_right: f32,
25    /// The bottom-right corner's radius, in logical pixels.
26    pub bottom_right: f32,
27    /// The bottom-left corner's radius, in logical pixels.
28    pub bottom_left: f32,
29}
30
31/// **A corner radius named by the reading direction rather than by the wall**
32/// (`border_radius.dart:621`).
33///
34/// A [`BorderRadius`] says *top left*. This says *top start* — the corner where the text
35/// begins, which is the left one in English and the right one in Arabic. [`resolve`] turns
36/// one into the other once the direction is known.
37///
38/// The distinction is not decorative. A drawer rounds its **inner** edge, the one facing
39/// the page: for a leading drawer that is the *end* side in either direction, and a radius
40/// written as *right* is correct in English and wrong in Arabic. Every asymmetric radius
41/// in an interface that mirrors has this question, and answering it by hand at each site
42/// is how one of them ends up answered differently.
43///
44/// The reference has one type hierarchy for this (`BorderRadiusGeometry`, with a resolved
45/// and an unresolved subclass); this is two plain types and a `resolve`, because a widget
46/// here is handed a `&Theme` and so always has a direction available when it needs one.
47///
48/// [`resolve`]: Self::resolve
49#[derive(Clone, Copy, Debug, Default, PartialEq)]
50pub struct BorderRadiusDirectional {
51    /// The corner at the top of the line's **beginning**.
52    pub top_start: f32,
53    /// The corner at the top of the line's **end**.
54    pub top_end: f32,
55    /// The corner at the bottom of the line's **end**.
56    pub bottom_end: f32,
57    /// The corner at the bottom of the line's **beginning**.
58    pub bottom_start: f32,
59}
60
61impl BorderRadiusDirectional {
62    /// No rounding at all.
63    pub const ZERO: Self = Self::uniform(0.0);
64
65    /// The same radius on all four corners — which needs no direction, and is here so a
66    /// caller can write one without changing type halfway through an expression.
67    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    /// The two corners at the line's **beginning** — the left pair in English, the right
77    /// pair in Arabic.
78    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    /// The two corners at the line's **end**.
88    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    /// Both sides at once, each with its own radius — the reference's
98    /// `BorderRadiusDirectional.horizontal`.
99    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    /// The top pair and the bottom pair. Neither depends on the direction, so this is the
109    /// same as [`BorderRadius`]'s — and is here for the same reason as
110    /// [`uniform`](Self::uniform).
111    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    /// **The concrete radius**, once the direction is known: `start` becomes left where
121    /// the text runs left to right, and right where it does not.
122    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    /// Each corner `t` of the way from `self` to `other`.
148    ///
149    /// Not clamped: a corner past its target is what a spring asks for at the end of its
150    /// travel, and the one rule for a radius in flight is this one — an animated container
151    /// and a decoration transition both go through it.
152    #[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    /// No rounding at all.
164    pub const ZERO: Self = Self::uniform(0.0);
165
166    /// The same radius on all four corners.
167    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    /// Only the **top** corners rounded (headers, rising sheets).
177    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    /// Only the **bottom** corners rounded.
187    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    /// Only the **left** corners rounded.
197    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    /// Only the **right** corners rounded.
207    ///
208    /// With [`left`](Self::left) this completes the set: a shape rounded on one
209    /// **vertical** edge is what a side panel wants — square where it meets the window,
210    /// round where it meets the content — and until now only the horizontal pair
211    /// ([`top`](Self::top), [`bottom`](Self::bottom)) existed.
212    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    /// Radii **clamped at zero** — a negative radius is meaningless when painting.
222    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    /// Every corner grown by `by` — the envelope of a blurred shadow.
232    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    /// Every radius multiplied by `factor` (DPI scaling).
242    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    /// `[tl, tr, br, bl]`, ready for the GPU.
252    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/// **The shape a box decoration paints** — the reference's `BoxShape`
269/// (`box_border.dart:26`).
270#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
271pub enum BoxShape {
272    /// A rectangle, its corners rounded by the decoration's radius.
273    #[default]
274    Rectangle,
275    /// A circle, centred in the box, as wide as the box's shorter side. The decoration's
276    /// radius means nothing to it.
277    Circle,
278}
279
280/// One side of a [`BorderSide`] pair, the same on both sides of a lerp.
281fn lerp_side(a: BorderSide, b: BorderSide, t: f32) -> BorderSide {
282    // A side on one end only arrives by thickening, in its own colour.
283    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/// **A box's border, side by side** — the reference's `Border` (`box_border.dart:431`):
294/// each of the four sides its own colour and width.
295#[derive(Clone, Copy, Debug, Default, PartialEq)]
296pub struct Border {
297    /// The top side.
298    pub top: BorderSide,
299    /// The right side.
300    pub right: BorderSide,
301    /// The bottom side.
302    pub bottom: BorderSide,
303    /// The left side.
304    pub left: BorderSide,
305}
306
307impl Border {
308    /// No border on any side.
309    pub const NONE: Self = Self {
310        top: BorderSide::NONE,
311        right: BorderSide::NONE,
312        bottom: BorderSide::NONE,
313        left: BorderSide::NONE,
314    };
315
316    /// The same width and colour on all four sides.
317    pub const fn new(width: f32, color: Color) -> Self {
318        Self::all(BorderSide::new(color, width))
319    }
320
321    /// `side` on all four sides — the reference's `Border.all` and `fromBorderSide`.
322    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    /// `vertical` on the left and the right, `horizontal` on the top and the bottom — the
332    /// reference's `Border.symmetric`.
333    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    /// The four sides, top, right, bottom, left.
343    fn sides(&self) -> [BorderSide; 4] {
344        [self.top, self.right, self.bottom, self.left]
345    }
346
347    /// Whether the four sides are the same.
348    pub fn is_uniform(&self) -> bool {
349        let [t, r, b, l] = self.sides();
350        t == r && r == b && b == l
351    }
352
353    /// `true` when some side is visible — non-zero width and non-zero alpha.
354    pub fn is_visible(&self) -> bool {
355        self.sides().iter().any(BorderSide::is_drawn)
356    }
357
358    /// The room the border takes inside the box, side by side.
359    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    /// The border `t` of the way to `other`, side by side.
365    #[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/// **A box's border, by the line's start and end** — the reference's `BorderDirectional`
377/// (`box_border.dart:799`): the start side is the left in a left-to-right script and the
378/// right in a right-to-left one.
379#[derive(Clone, Copy, Debug, Default, PartialEq)]
380pub struct BorderDirectional {
381    /// The top side.
382    pub top: BorderSide,
383    /// The side where a line of text starts.
384    pub start: BorderSide,
385    /// The side where a line of text ends.
386    pub end: BorderSide,
387    /// The bottom side.
388    pub bottom: BorderSide,
389}
390
391impl BorderDirectional {
392    /// The sides on the screen, for `direction`.
393    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/// A box's border, by its physical sides or by the line's start and end.
408#[derive(Clone, Copy, Debug, PartialEq)]
409pub enum BoxBorder {
410    /// Left, right, top, bottom.
411    Physical(Border),
412    /// Start, end, top, bottom.
413    Directional(BorderDirectional),
414}
415
416impl BoxBorder {
417    /// The sides on the screen, for `direction`.
418    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/// A soft drop shadow.
439#[derive(Clone, Copy, Debug, PartialEq)]
440pub struct BoxShadow {
441    /// Colour; its alpha sets the intensity.
442    pub color: Color,
443    /// Offset `(dx, dy)`, in logical pixels.
444    pub offset: (f32, f32),
445    /// Blur radius.
446    pub blur: f32,
447    /// How far the shadow grows beyond the box, before blurring.
448    pub spread: f32,
449    /// Which side of the shadow's edge the blur keeps (milestone 645): both, by default.
450    pub blur_style: BlurStyle,
451}
452
453impl BoxShadow {
454    /// A shadow offset by `(dx, dy)` with `blur`, and no spread.
455    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    /// Sets the `spread`.
466    pub const fn spread(mut self, spread: f32) -> Self {
467        self.spread = spread;
468        self
469    }
470
471    /// Sets which side of the edge the blur keeps: a solid shadow with a soft edge, a
472    /// glow outside the box only, a shading inside it.
473    pub const fn blur_style(mut self, style: BlurStyle) -> Self {
474        self.blur_style = style;
475        self
476    }
477
478    /// **The three shadows a surface `elevation` high casts**, in `color`: a sharp one close
479    /// under it, a softer one further down, and a wide faint one all round. The reference's
480    /// own table, and its three strengths — a fifth, a seventh and an eighth of `color`'s
481    /// opacity — so `color` is the shadow's colour at full strength, black by default
482    /// (milestone 606).
483    ///
484    /// Between two heights of the table the shadows are interpolated, so a height that
485    /// animates moves smoothly; past twenty-four, it is twenty-four.
486    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            // A surface on the page casts nothing: its shadows fade in as it lifts.
504            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    /// The rectangle the shadow occupies around `rect` (offset + blur + spread).
517    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
528/// The reference's shadows per height: `(height, [(drop, blur, spread); 3])`, the key light's
529/// umbra, its penumbra, then the ambient light.
530/// One shadow of a height: its drop, its blur and its spread.
531type Layer = (f32, f32, f32);
532/// A height and its three shadows.
533type 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
558/// Paints the shadows of a surface `elevation` high, with corners `radius`, behind `rect`:
559/// [`BoxShadow::for_elevation`]'s three, each as [`BoxDecoration`] paints one. Nothing at
560/// a height of nought or in a colour with no opacity (milestone 606).
561pub 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/// The complete decoration of a box — the reference's `BoxDecoration`
583/// (`box_decoration.dart:81`).
584///
585/// The paint order is **fixed**, the reference's (`box_decoration.dart:571`): the shadows,
586/// in order, then the background, then the picture, then the border. The background is
587/// either flat (`color`) or a gradient, which replaces the colour. A border with no
588/// background paints an outline over transparency; a wholly empty decoration paints nothing
589/// at all.
590#[derive(Clone, Debug, Default, PartialEq)]
591pub struct BoxDecoration {
592    /// Background colour.
593    pub color: Option<Color>,
594    /// A gradient filling the background, in place of `color` — linear, radial or sweep
595    /// (milestone 644).
596    pub gradient: Option<crate::Gradient>,
597    /// The border, side by side.
598    pub border: Option<BoxBorder>,
599    /// Corner radii, per corner. A [`BoxShape::Circle`] has none.
600    pub radius: BorderRadius,
601    /// The shadows, painted in order, behind the box (`box_decoration.dart:448`).
602    pub shadows: Vec<BoxShadow>,
603    /// A rectangle or a circle.
604    pub shape: BoxShape,
605    /// A picture over the background, under the border, clipped to the shape
606    /// (milestone 645).
607    pub image: Option<DecorationImage>,
608    /// How the background — the colour or the gradient — meets what is painted under
609    /// the box (milestone 646): `None` paints it over. Nothing else of the decoration
610    /// is blended, and with no colour or gradient it has nothing to do.
611    pub background_blend_mode: Option<crate::BlendMode>,
612}
613
614/// A colour that may be absent on either side, `t` of the way across.
615///
616/// Absent means **nothing painted**, so a colour on one side only fades: it keeps its hue
617/// and only its alpha travels. Interpolating towards `Color::TRANSPARENT` instead would be
618/// interpolating towards transparent *black* — a red fill fading out would go dark red on
619/// the way, in a movement that was only ever about disappearing.
620fn 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    /// The decoration `t` of the way from `self` to `other` — what a decoration transition
631    /// paints mid-flight. Each part has its own rule, and the rules are the point:
632    ///
633    /// - **Colours mix** where both sides have one, in the space colours are stated in,
634    ///   as every animated colour in this framework does.
635    /// - **A part on one side only arrives or leaves; it does not come from nowhere.** A
636    ///   fill fades, keeping its hue. A border thickens from nought in its own colour. A
637    ///   shadow grows from under the box as it fades in. Each is the part scaled by how far
638    ///   along it is, which is also what the reference does with a part one side lacks.
639    /// - **A flat fill is a gradient whose two ends agree**, so flat to graded spreads the
640    ///   far end out of the fill rather than laying a gradient over it. Here a gradient
641    ///   *starts* at the fill colour, so the two always arrive and leave together.
642    /// - **Corners** travel one by one.
643    ///
644    /// The absent colour here is **not** the absent colour of a text style, and the two
645    /// rules differ for that reason. A text style that names no colour is asking the theme
646    /// for one — a real, visible colour — so it holds still rather than fade. A decoration
647    /// that names no fill paints nothing, so fading is exactly what reaching it means.
648    ///
649    /// `t` is clamped to `0..=1`, and a `t` that is not a number is no progress: past its
650    /// ends a colour has nowhere to go, and a decoration whose colours stopped at the end
651    /// while its corners kept going would be two clocks. At `0` and `1` the answer is the
652    /// end itself, exactly.
653    #[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        // A rectangle and a circle do not blend: the shape changes half way, as the
662        // reference's does (`box_decoration.dart:301`).
663        let shape = if t < 0.5 { self.shape } else { other.shape };
664        let mix = |a: f32, b: f32| a + (b - a) * t;
665        // The reference's `Gradient.lerp` (`box_decoration.dart:309`).
666        let gradient = crate::Gradient::lerp(self.gradient.as_ref(), other.gradient.as_ref(), t);
667        // Side by side, each arriving or leaving by thickening in its own colour. A
668        // directional border is read left to right here; two directional ones stay so.
669        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        // A shadow on one side only, scaled: it grows out from under the box as it fades
691        // in. Scaling the geometry alone would leave a hard-edged block of shadow colour
692        // under a box whose own fill may be fading too.
693        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        // The shadows pair by pair, and those one list has more of grow or shrink — the
701        // reference's `BoxShadow.lerpList` (`box_shadow.dart`).
702        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                    // The reference's rule: a plain blur takes the other's style.
711                    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            // Two pictures cross over; one alone fades.
730            image: DecorationImage::lerp(self.image.as_ref(), other.image.as_ref(), t),
731            // A mode does not blend into another: it changes half way, as the shape
732            // does. The reference's lerp drops it, so a blended box painted plainly
733            // through every transition.
734            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    /// A decoration with a flat background.
745    pub fn filled(color: Color) -> Self {
746        Self {
747            color: Some(color),
748            ..Default::default()
749        }
750    }
751
752    /// Sets the corner radii — uniform through `f32`, per corner through
753    /// [`BorderRadius`].
754    pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
755        self.radius = radius.into();
756        self
757    }
758
759    /// Sets the border, side by side or by the line's start and end.
760    pub fn border(mut self, border: impl Into<BoxBorder>) -> Self {
761        self.border = Some(border.into());
762        self
763    }
764
765    /// Adds a shadow, after (so over) those already there.
766    pub fn shadow(mut self, shadow: BoxShadow) -> Self {
767        self.shadows.push(shadow);
768        self
769    }
770
771    /// Sets the shadows, painted in order.
772    pub fn shadows(mut self, shadows: impl IntoIterator<Item = BoxShadow>) -> Self {
773        self.shadows = shadows.into_iter().collect();
774        self
775    }
776
777    /// Paints a rectangle or a circle.
778    pub fn shape(mut self, shape: BoxShape) -> Self {
779        self.shape = shape;
780        self
781    }
782
783    /// Fills the background with a gradient — linear, radial or sweep — in place of the
784    /// colour.
785    pub fn gradient(mut self, gradient: impl Into<crate::Gradient>) -> Self {
786        self.gradient = Some(gradient.into());
787        self
788    }
789
790    /// Paints a picture over the background, under the border.
791    pub fn image(mut self, image: DecorationImage) -> Self {
792        self.image = Some(image);
793        self
794    }
795
796    /// Blends the background — the colour or the gradient — with what is under the
797    /// box by `mode`, rather than painting it over.
798    pub fn background_blend_mode(mut self, mode: crate::BlendMode) -> Self {
799        self.background_blend_mode = Some(mode);
800        self
801    }
802
803    /// The inner margin the border needs — add it to the padding so the content is
804    /// not eaten by the line. This is what feeds taffy. A directional border is read
805    /// left to right; see [`Self::content_padding_in`].
806    pub fn content_padding(&self) -> Insets {
807        self.content_padding_in(TextDirection::Ltr)
808    }
809
810    /// The inner margin the border needs, for a box in `direction`.
811    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    /// The box the decoration paints in `rect`: `rect` itself, or the square a circle is
818    /// inscribed in (`box_decoration.dart:436`).
819    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    /// Lowers the decoration into `scene` primitives, in the fixed order
836    /// shadows → background → picture → border, for a box read left to right. See
837    /// [`Self::paint_into_in`].
838    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    /// Lowers the decoration into `scene` primitives, in the fixed order
843    /// shadows → background → picture → border. `opacity` (`0..=1`) modulates **every** colour,
844    /// which is how a fade-in works. `rect` is the box in absolute coordinates;
845    /// `direction` places a directional border's start and end.
846    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        // 1) The shadows, in order, behind everything else.
856        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        // 2/3) Background (flat or gradient), and a uniform border in the same primitive
867        // — unless a picture goes between them.
868        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        // A blend other than painting over: the background goes in a group of its own.
878        let blend = self
879            .background_blend_mode
880            .filter(|m| *m != crate::BlendMode::SrcOver && has_background);
881        // Anything between the fill and the border takes the line off the fill.
882        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            // Border only, with no background: an outline over transparency.
905            (None, _) if has_border && !apart => scene.draw_rect(
906                shape_rect,
907                Color::TRANSPARENT,
908                radius,
909                border_width,
910                border_color,
911            ),
912            // Nothing to paint.
913            (None, _) => {}
914        };
915
916        match blend {
917            // The background blended with what is under the box (`box_decoration.dart:413`),
918            // square, in a group whose shape is the box's: its edge is the group's
919            // coverage, so what the shape leaves out keeps what was there.
920            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        // The picture, over the background and clipped to the shape
933        // (`box_decoration.dart:476`).
934        if let Some(image) = image {
935            self.paint_image(scene, image, rect, shape_rect, radius, direction, opacity);
936        }
937        // Then the uniform border, when something came between it and the fill.
938        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        // 3) A border whose sides differ.
949        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    /// The picture painted into the whole box, clipped to the circle or the rounded
955    /// corners when there are any — the reference's `_paintBackgroundImage`.
956    #[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    /// The clip a layer takes to be the box's circle or rounded corners, within the
990    /// `outer` clip; `None` for square corners, which clip nothing.
991    ///
992    /// A layer's shape is inscribed in its clip, so it is exact only when the clip is
993    /// the whole shape. A box partly clipped away is clipped by its outline instead.
994    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
1018/// **A border whose sides differ** (`box_border.dart:681`). In one colour, the band between
1019/// the box and the box inset by each side, which keeps its corners and its circle; in
1020/// several, on a rectangle, each side as its own trapezoid, mitred at the corners.
1021fn 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        // The inner corners are the outer ones less the thicker of the two sides that meet
1046        // there.
1047        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    // Each side as a trapezoid from the outer edge to the inner one.
1061    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    /// **A directional radius names the corner by the line, not by the wall**
1091    /// (`border_radius.dart:621`), and `resolve` says which wall that is.
1092    #[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        // Both sides at once, each keeping its own number across the mirror.
1111        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        // What has no side does not move.
1132        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        // An invisible border (zero width) → no padding.
1155        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        // Two primitives: the shadow first, then the background.
1179        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        // grow = blur + spread = 6 on every side.
1239        assert_eq!(b, Rect::new(-6.0, -6.0, 22.0, 22.0));
1240    }
1241}
1242
1243/// The decoration in flight (milestone 501): one rule per part, and the ends exact.
1244#[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    /// **The ends are the ends, exactly** — and past them, and at a progress that is not a
1262    /// number, the nearer end.
1263    #[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    /// **A fill fading out keeps its hue.** The obvious interpolation — towards
1276    /// `Color::TRANSPARENT` — is towards transparent *black*, and half-way there a red is a
1277    /// half-opaque dark red: a movement that was only about disappearing darkens on the way.
1278    #[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    /// And one fading in arrives in its own colour, only fainter.
1289    #[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    /// **A gradient on one side only fades in by its opacity** over the fill, the
1300    /// reference's `Gradient.scale` (`box_decoration.dart:309`): its colours keep their hue.
1301    #[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    /// **A border arrives by thickening, in its own colour** — not as a colour on its way
1313    /// from nowhere.
1314    #[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    /// **A shadow arrives by growing out from under the box as it fades in.**
1327    #[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    /// Corners travel one by one, each from its own start.
1342    #[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    /// **At a height of the reference's table, its three shadows exactly**, each carrying its
1352    /// share of the colour: a fifth, a seventh, an eighth.
1353    #[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    /// **Between two heights, between their shadows**; past twenty-four, twenty-four; and
1377    /// the colour's own opacity scales all three.
1378    #[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        // Lifting off the page fades in rather than jumping to a height of one.
1393        let [low, ..] = BoxShadow::for_elevation(0.5, Color::BLACK);
1394        assert!((low.color.a - 0.1).abs() < 1e-6, "{low:?}");
1395    }
1396
1397    /// **Painted, a height is three soft shapes** grown past the box by their blur and
1398    /// spread, with the box's corners grown with them; at nought, or in a colour nobody can
1399    /// see, nothing.
1400    #[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/// The decoration's shape, its borders side by side, and its list of shadows
1440/// (milestone 641).
1441#[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    /// **A circle is centred in the box, as wide as its shorter side**, and its shadow is a
1457    /// circle too (`box_decoration.dart:436`). The radius it was given is not read.
1458    #[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    /// **The shadows are painted in order, behind the box** (`box_decoration.dart:452`).
1487    #[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    /// **Sides of different colours are four trapezoids**, each its own colour, mitred at
1505    /// the corners; a side with no width is not drawn.
1506    #[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        // With round corners as well, still side by side: one band would be one colour.
1530        // (The reference refuses several colours with round corners, `box_border.dart:711`.)
1531        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    /// **One colour, uneven widths, round corners: one band** between the box and the box
1547    /// inset by each side (`box_border.dart:686`).
1548    #[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                // The hole's corners are the box's less the thicker side meeting there:
1571                // 8 less 6 at the top left, so the hole starts 2 in from its own left.
1572                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    /// **A uniform border stays one primitive with the fill**, as before.
1588    #[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    /// **A directional border's start is the left in a left-to-right script and the right
1604    /// in a right-to-left one**, for the paint and for the room it takes.
1605    #[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        // The left side's trapezoid starts at the bottom left; the right side's at the top
1623        // right.
1624        assert_eq!(first_x(TextDirection::Ltr), 0.0);
1625        assert_eq!(first_x(TextDirection::Rtl), 50.0);
1626    }
1627
1628    /// **Two lists of shadows pair up; the longer one's extras grow or shrink**, and a
1629    /// circle and a rectangle swap half way.
1630    #[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    /// **The border's constructors** are the reference's.
1651    #[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    /// The kinds of primitive a decoration paints, in order, layers opened.
1677    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    /// **The picture goes between the background and the border**
1709    /// (`box_decoration.dart:476`): the fill loses its line, which is drawn over the
1710    /// picture.
1711    #[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        // A border alone, over the picture.
1726        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        // With no picture, the fill carries its line.
1737        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        // Without a background, a picture alone.
1743        let alone = BoxDecoration::default().image(picture());
1744        assert_eq!(
1745            kinds(painted_clipped(&alone, Rect::UNBOUNDED).primitives()),
1746            vec!["image"]
1747        );
1748    }
1749
1750    /// **The picture is clipped to the corners or the circle**, into the whole box.
1751    #[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        // Square corners clip nothing.
1796        let square = BoxDecoration::default().image(picture());
1797        assert!(matches!(
1798            painted_clipped(&square, Rect::UNBOUNDED).primitives()[0],
1799            Primitive::Image { .. }
1800        ));
1801    }
1802
1803    /// **A box partly clipped away clips its picture by the outline**: a shape inscribed
1804    /// in what is left of the box would be the wrong shape.
1805    #[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    /// **A shadow paints with its blur style**, and the reference's lerp keeps a styled
1841    /// blur over a plain one.
1842    #[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        // One side only: the style travels with the shadow.
1863        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    /// The background's group, and what is in it.
1876    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    /// **A blended background is a group in the box's shape** (`box_decoration.dart:413`),
1890    /// the fill square inside it: the shape's edge is the blend's coverage.
1891    #[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        // Square corners: the box, held to the clip.
1922        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        // Partly clipped away: by its outline.
1928        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    /// **Only the background is blended**: the border is drawn after, over it, and the
1933    /// scene's clip is given back.
1934    #[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    /// **Painting over is no blend at all, and no background has nothing to blend.**
1949    #[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    /// **A blend mode changes half way**, as the shape does.
1980    #[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}