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) → border. It feeds layout too:
9//! [`BoxDecoration::content_padding`] reserves room for the border on taffy's
10//! behalf.
11
12use crate::{Color, Insets, Rect, Scene, TextDirection};
13
14/// Corner radii, **per corner** (logical px). `From<f32>` covers the uniform case:
15/// anywhere a radius is expected, a plain `10.0` still works.
16#[derive(Clone, Copy, Debug, Default, PartialEq)]
17pub struct BorderRadius {
18    /// The top-left corner's radius, in logical pixels.
19    pub top_left: f32,
20    /// The top-right corner's radius, in logical pixels.
21    pub top_right: f32,
22    /// The bottom-right corner's radius, in logical pixels.
23    pub bottom_right: f32,
24    /// The bottom-left corner's radius, in logical pixels.
25    pub bottom_left: f32,
26}
27
28/// **A corner radius named by the reading direction rather than by the wall**
29/// (`border_radius.dart:621`).
30///
31/// A [`BorderRadius`] says *top left*. This says *top start* — the corner where the text
32/// begins, which is the left one in English and the right one in Arabic. [`resolve`] turns
33/// one into the other once the direction is known.
34///
35/// The distinction is not decorative. A drawer rounds its **inner** edge, the one facing
36/// the page: for a leading drawer that is the *end* side in either direction, and a radius
37/// written as *right* is correct in English and wrong in Arabic. Every asymmetric radius
38/// in an interface that mirrors has this question, and answering it by hand at each site
39/// is how one of them ends up answered differently.
40///
41/// The reference has one type hierarchy for this (`BorderRadiusGeometry`, with a resolved
42/// and an unresolved subclass); this is two plain types and a `resolve`, because a widget
43/// here is handed a `&Theme` and so always has a direction available when it needs one.
44///
45/// [`resolve`]: Self::resolve
46#[derive(Clone, Copy, Debug, Default, PartialEq)]
47pub struct BorderRadiusDirectional {
48    /// The corner at the top of the line's **beginning**.
49    pub top_start: f32,
50    /// The corner at the top of the line's **end**.
51    pub top_end: f32,
52    /// The corner at the bottom of the line's **end**.
53    pub bottom_end: f32,
54    /// The corner at the bottom of the line's **beginning**.
55    pub bottom_start: f32,
56}
57
58impl BorderRadiusDirectional {
59    /// No rounding at all.
60    pub const ZERO: Self = Self::uniform(0.0);
61
62    /// The same radius on all four corners — which needs no direction, and is here so a
63    /// caller can write one without changing type halfway through an expression.
64    pub const fn uniform(radius: f32) -> Self {
65        Self {
66            top_start: radius,
67            top_end: radius,
68            bottom_end: radius,
69            bottom_start: radius,
70        }
71    }
72
73    /// The two corners at the line's **beginning** — the left pair in English, the right
74    /// pair in Arabic.
75    pub const fn start(radius: f32) -> Self {
76        Self {
77            top_start: radius,
78            top_end: 0.0,
79            bottom_end: 0.0,
80            bottom_start: radius,
81        }
82    }
83
84    /// The two corners at the line's **end**.
85    pub const fn end(radius: f32) -> Self {
86        Self {
87            top_start: 0.0,
88            top_end: radius,
89            bottom_end: radius,
90            bottom_start: 0.0,
91        }
92    }
93
94    /// Both sides at once, each with its own radius — the reference's
95    /// `BorderRadiusDirectional.horizontal`.
96    pub const fn horizontal(start: f32, end: f32) -> Self {
97        Self {
98            top_start: start,
99            top_end: end,
100            bottom_end: end,
101            bottom_start: start,
102        }
103    }
104
105    /// The top pair and the bottom pair. Neither depends on the direction, so this is the
106    /// same as [`BorderRadius`]'s — and is here for the same reason as
107    /// [`uniform`](Self::uniform).
108    pub const fn vertical(top: f32, bottom: f32) -> Self {
109        Self {
110            top_start: top,
111            top_end: top,
112            bottom_end: bottom,
113            bottom_start: bottom,
114        }
115    }
116
117    /// **The concrete radius**, once the direction is known: `start` becomes left where
118    /// the text runs left to right, and right where it does not.
119    pub const fn resolve(self, direction: TextDirection) -> BorderRadius {
120        match direction {
121            TextDirection::Ltr => BorderRadius {
122                top_left: self.top_start,
123                top_right: self.top_end,
124                bottom_right: self.bottom_end,
125                bottom_left: self.bottom_start,
126            },
127            TextDirection::Rtl => BorderRadius {
128                top_left: self.top_end,
129                top_right: self.top_start,
130                bottom_right: self.bottom_start,
131                bottom_left: self.bottom_end,
132            },
133        }
134    }
135}
136
137impl From<f32> for BorderRadiusDirectional {
138    fn from(radius: f32) -> Self {
139        Self::uniform(radius)
140    }
141}
142
143impl BorderRadius {
144    /// Each corner `t` of the way from `self` to `other`.
145    ///
146    /// Not clamped: a corner past its target is what a spring asks for at the end of its
147    /// travel, and the one rule for a radius in flight is this one — an animated container
148    /// and a decoration transition both go through it.
149    #[must_use]
150    pub fn lerp(self, other: BorderRadius, t: f32) -> BorderRadius {
151        let mix = |a: f32, b: f32| a + (b - a) * t;
152        BorderRadius {
153            top_left: mix(self.top_left, other.top_left),
154            top_right: mix(self.top_right, other.top_right),
155            bottom_right: mix(self.bottom_right, other.bottom_right),
156            bottom_left: mix(self.bottom_left, other.bottom_left),
157        }
158    }
159
160    /// No rounding at all.
161    pub const ZERO: Self = Self::uniform(0.0);
162
163    /// The same radius on all four corners.
164    pub const fn uniform(radius: f32) -> Self {
165        Self {
166            top_left: radius,
167            top_right: radius,
168            bottom_right: radius,
169            bottom_left: radius,
170        }
171    }
172
173    /// Only the **top** corners rounded (headers, rising sheets).
174    pub const fn top(radius: f32) -> Self {
175        Self {
176            top_left: radius,
177            top_right: radius,
178            bottom_right: 0.0,
179            bottom_left: 0.0,
180        }
181    }
182
183    /// Only the **bottom** corners rounded.
184    pub const fn bottom(radius: f32) -> Self {
185        Self {
186            top_left: 0.0,
187            top_right: 0.0,
188            bottom_right: radius,
189            bottom_left: radius,
190        }
191    }
192
193    /// Only the **left** corners rounded.
194    pub const fn left(radius: f32) -> Self {
195        Self {
196            top_left: radius,
197            top_right: 0.0,
198            bottom_right: 0.0,
199            bottom_left: radius,
200        }
201    }
202
203    /// Only the **right** corners rounded.
204    ///
205    /// With [`left`](Self::left) this completes the set: a shape rounded on one
206    /// **vertical** edge is what a side panel wants — square where it meets the window,
207    /// round where it meets the content — and until now only the horizontal pair
208    /// ([`top`](Self::top), [`bottom`](Self::bottom)) existed.
209    pub const fn right(radius: f32) -> Self {
210        Self {
211            top_left: 0.0,
212            top_right: radius,
213            bottom_right: radius,
214            bottom_left: 0.0,
215        }
216    }
217
218    /// Radii **clamped at zero** — a negative radius is meaningless when painting.
219    pub fn clamped(self) -> Self {
220        Self {
221            top_left: self.top_left.max(0.0),
222            top_right: self.top_right.max(0.0),
223            bottom_right: self.bottom_right.max(0.0),
224            bottom_left: self.bottom_left.max(0.0),
225        }
226    }
227
228    /// Every corner grown by `by` — the envelope of a blurred shadow.
229    pub fn inflate(self, by: f32) -> Self {
230        Self {
231            top_left: self.top_left + by,
232            top_right: self.top_right + by,
233            bottom_right: self.bottom_right + by,
234            bottom_left: self.bottom_left + by,
235        }
236    }
237
238    /// Every radius multiplied by `factor` (DPI scaling).
239    pub fn scale(self, factor: f32) -> Self {
240        Self {
241            top_left: self.top_left * factor,
242            top_right: self.top_right * factor,
243            bottom_right: self.bottom_right * factor,
244            bottom_left: self.bottom_left * factor,
245        }
246    }
247
248    /// `[tl, tr, br, bl]`, ready for the GPU.
249    pub fn to_array(self) -> [f32; 4] {
250        [
251            self.top_left,
252            self.top_right,
253            self.bottom_right,
254            self.bottom_left,
255        ]
256    }
257}
258
259impl From<f32> for BorderRadius {
260    fn from(radius: f32) -> Self {
261        Self::uniform(radius)
262    }
263}
264
265/// A uniform border — the same width and colour on all four sides.
266#[derive(Clone, Copy, Debug, PartialEq)]
267pub struct Border {
268    /// Width, in logical pixels.
269    pub width: f32,
270    /// The line's colour.
271    pub color: Color,
272}
273
274impl Border {
275    /// A uniform border.
276    pub const fn new(width: f32, color: Color) -> Self {
277        Self { width, color }
278    }
279
280    /// `true` when the border is visible — non-zero width and non-zero alpha.
281    pub fn is_visible(&self) -> bool {
282        self.width > 0.0 && self.color.a > 0.0
283    }
284}
285
286/// A **linear** gradient: from the background (`BoxDecoration::color`) to `end`,
287/// along `direction`, expressed in `[0,1]²` space (`[0,1]` = top→bottom, `[1,0]` =
288/// left→right).
289#[derive(Clone, Copy, Debug, PartialEq)]
290pub struct LinearGradient {
291    /// The end colour; the start colour is the decoration's background.
292    pub end: Color,
293    /// The gradient's direction, in `[0,1]²` space.
294    pub direction: [f32; 2],
295}
296
297impl LinearGradient {
298    /// A linear gradient towards `end`, in the given direction.
299    pub const fn new(end: Color, direction: [f32; 2]) -> Self {
300        Self { end, direction }
301    }
302}
303
304/// A soft drop shadow.
305#[derive(Clone, Copy, Debug, PartialEq)]
306pub struct BoxShadow {
307    /// Colour; its alpha sets the intensity.
308    pub color: Color,
309    /// Offset `(dx, dy)`, in logical pixels.
310    pub offset: (f32, f32),
311    /// Blur radius.
312    pub blur: f32,
313    /// How far the shadow grows beyond the box, before blurring.
314    pub spread: f32,
315}
316
317impl BoxShadow {
318    /// A shadow offset by `(dx, dy)` with `blur`, and no spread.
319    pub const fn new(dx: f32, dy: f32, blur: f32, color: Color) -> Self {
320        Self {
321            color,
322            offset: (dx, dy),
323            blur,
324            spread: 0.0,
325        }
326    }
327
328    /// Sets the `spread`.
329    pub const fn spread(mut self, spread: f32) -> Self {
330        self.spread = spread;
331        self
332    }
333
334    /// The rectangle the shadow occupies around `rect` (offset + blur + spread).
335    pub fn bounds(&self, rect: Rect) -> Rect {
336        let grow = self.blur + self.spread;
337        Rect::new(
338            rect.x + self.offset.0 - grow,
339            rect.y + self.offset.1 - grow,
340            rect.width + 2.0 * grow,
341            rect.height + 2.0 * grow,
342        )
343    }
344}
345
346/// The complete decoration of a rectangular box.
347///
348/// The paint order is **fixed**: shadow → background → border. The background is
349/// either flat (`color`) or a gradient (`color` → `gradient.end`). A border with no
350/// background paints an outline over transparency; a wholly empty decoration paints
351/// nothing at all.
352#[derive(Clone, Copy, Debug, Default, PartialEq)]
353pub struct BoxDecoration {
354    /// Background colour, which doubles as the start colour of any gradient.
355    pub color: Option<Color>,
356    /// A linear gradient for the background.
357    pub gradient: Option<LinearGradient>,
358    /// A uniform border.
359    pub border: Option<Border>,
360    /// Corner radii, per corner.
361    pub radius: BorderRadius,
362    /// Drop shadow.
363    pub shadow: Option<BoxShadow>,
364}
365
366/// A colour that may be absent on either side, `t` of the way across.
367///
368/// Absent means **nothing painted**, so a colour on one side only fades: it keeps its hue
369/// and only its alpha travels. Interpolating towards `Color::TRANSPARENT` instead would be
370/// interpolating towards transparent *black* — a red fill fading out would go dark red on
371/// the way, in a movement that was only ever about disappearing.
372fn fade_between(a: Option<Color>, b: Option<Color>, t: f32) -> Option<Color> {
373    match (a, b) {
374        (Some(a), Some(b)) => Some(a.lerp(b, t)),
375        (Some(a), None) => Some(a.fade(1.0 - t)),
376        (None, Some(b)) => Some(b.fade(t)),
377        (None, None) => None,
378    }
379}
380
381impl BoxDecoration {
382    /// The decoration `t` of the way from `self` to `other` — what a decoration transition
383    /// paints mid-flight. Each part has its own rule, and the rules are the point:
384    ///
385    /// - **Colours mix** where both sides have one, in the space colours are stated in,
386    ///   as every animated colour in this framework does.
387    /// - **A part on one side only arrives or leaves; it does not come from nowhere.** A
388    ///   fill fades, keeping its hue. A border thickens from nought in its own colour. A
389    ///   shadow grows from under the box as it fades in. Each is the part scaled by how far
390    ///   along it is, which is also what the reference does with a part one side lacks.
391    /// - **A flat fill is a gradient whose two ends agree**, so flat to graded spreads the
392    ///   far end out of the fill rather than laying a gradient over it. Here a gradient
393    ///   *starts* at the fill colour, so the two always arrive and leave together.
394    /// - **Corners** travel one by one.
395    ///
396    /// The absent colour here is **not** the absent colour of a text style, and the two
397    /// rules differ for that reason. A text style that names no colour is asking the theme
398    /// for one — a real, visible colour — so it holds still rather than fade. A decoration
399    /// that names no fill paints nothing, so fading is exactly what reaching it means.
400    ///
401    /// `t` is clamped to `0..=1`, and a `t` that is not a number is no progress: past its
402    /// ends a colour has nowhere to go, and a decoration whose colours stopped at the end
403    /// while its corners kept going would be two clocks. At `0` and `1` the answer is the
404    /// end itself, exactly.
405    #[must_use]
406    pub fn lerp(self, other: BoxDecoration, t: f32) -> BoxDecoration {
407        if t.is_nan() || t <= 0.0 {
408            return self;
409        }
410        if t >= 1.0 {
411            return other;
412        }
413        let mix = |a: f32, b: f32| a + (b - a) * t;
414        let gradient = match (self.gradient, other.gradient) {
415            (Some(a), Some(b)) => Some(LinearGradient {
416                end: a.end.lerp(b.end, t),
417                direction: [
418                    mix(a.direction[0], b.direction[0]),
419                    mix(a.direction[1], b.direction[1]),
420                ],
421            }),
422            // The side without a gradient is a flat fill: its far end is its own colour.
423            (Some(a), None) => {
424                fade_between(Some(a.end), other.color, t).map(|end| LinearGradient {
425                    end,
426                    direction: a.direction,
427                })
428            }
429            (None, Some(b)) => fade_between(self.color, Some(b.end), t).map(|end| LinearGradient {
430                end,
431                direction: b.direction,
432            }),
433            (None, None) => None,
434        };
435        let border = match (self.border, other.border) {
436            (Some(a), Some(b)) => Some(Border {
437                width: mix(a.width, b.width),
438                color: a.color.lerp(b.color, t),
439            }),
440            (Some(a), None) => Some(Border {
441                width: a.width * (1.0 - t),
442                ..a
443            }),
444            (None, Some(b)) => Some(Border {
445                width: b.width * t,
446                ..b
447            }),
448            (None, None) => None,
449        };
450        // A shadow on one side only, scaled: it grows out from under the box as it fades
451        // in. Scaling the geometry alone would leave a hard-edged block of shadow colour
452        // under a box whose own fill may be fading too.
453        let grown = |s: BoxShadow, f: f32| BoxShadow {
454            color: s.color.fade(f),
455            offset: (s.offset.0 * f, s.offset.1 * f),
456            blur: s.blur * f,
457            spread: s.spread * f,
458        };
459        let shadow = match (self.shadow, other.shadow) {
460            (Some(a), Some(b)) => Some(BoxShadow {
461                color: a.color.lerp(b.color, t),
462                offset: (mix(a.offset.0, b.offset.0), mix(a.offset.1, b.offset.1)),
463                blur: mix(a.blur, b.blur),
464                spread: mix(a.spread, b.spread),
465            }),
466            (Some(a), None) => Some(grown(a, 1.0 - t)),
467            (None, Some(b)) => Some(grown(b, t)),
468            (None, None) => None,
469        };
470        BoxDecoration {
471            color: fade_between(self.color, other.color, t),
472            gradient,
473            border,
474            radius: self.radius.lerp(other.radius, t),
475            shadow,
476        }
477    }
478}
479
480impl BoxDecoration {
481    /// A decoration with a flat background.
482    pub fn filled(color: Color) -> Self {
483        Self {
484            color: Some(color),
485            ..Default::default()
486        }
487    }
488
489    /// Sets the corner radii — uniform through `f32`, per corner through
490    /// [`BorderRadius`].
491    pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
492        self.radius = radius.into();
493        self
494    }
495
496    /// Adds a uniform border.
497    pub fn border(mut self, border: Border) -> Self {
498        self.border = Some(border);
499        self
500    }
501
502    /// Adds a shadow.
503    pub fn shadow(mut self, shadow: BoxShadow) -> Self {
504        self.shadow = Some(shadow);
505        self
506    }
507
508    /// Adds a linear gradient to the background.
509    pub fn gradient(mut self, gradient: LinearGradient) -> Self {
510        self.gradient = Some(gradient);
511        self
512    }
513
514    /// The inner margin the border needs — add it to the padding so the content is
515    /// not eaten by the line. This is what feeds taffy.
516    pub fn content_padding(&self) -> Insets {
517        match self.border {
518            Some(b) if b.is_visible() => Insets::uniform(b.width),
519            _ => Insets::ZERO,
520        }
521    }
522
523    /// Lowers the decoration into `scene` primitives, in the fixed order
524    /// shadow → background → border. `opacity` (`0..=1`) modulates **every** colour,
525    /// which is how a fade-in works. `rect` is the box in absolute coordinates.
526    pub fn paint_into(&self, scene: &mut Scene, rect: Rect, opacity: f32) {
527        // 1) The shadow, behind everything else.
528        if let Some(shadow) = self.shadow {
529            scene.shadow(
530                shadow.bounds(rect),
531                shadow.color.fade(opacity),
532                self.radius.inflate(shadow.blur + shadow.spread),
533                shadow.blur,
534            );
535        }
536
537        // 2/3) Background (flat or gradient) plus border, in a single primitive.
538        let (border_width, border_color) = match self.border {
539            Some(b) => (b.width, b.color.fade(opacity)),
540            None => (0.0, Color::TRANSPARENT),
541        };
542        let has_border = self.border.map(|b| b.is_visible()).unwrap_or(false);
543
544        match (self.color, self.gradient) {
545            (Some(color), Some(gradient)) => scene.gradient_rect(
546                rect,
547                color.fade(opacity),
548                gradient.end.fade(opacity),
549                gradient.direction,
550                self.radius,
551                border_width,
552                border_color,
553            ),
554            (Some(color), None) => scene.draw_rect(
555                rect,
556                color.fade(opacity),
557                self.radius,
558                border_width,
559                border_color,
560            ),
561            // Border only, with no background: an outline over transparency.
562            (None, _) if has_border => scene.draw_rect(
563                rect,
564                Color::TRANSPARENT,
565                self.radius,
566                border_width,
567                border_color,
568            ),
569            // Nothing to paint.
570            (None, _) => {}
571        }
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    /// **A directional radius names the corner by the line, not by the wall**
578    /// (`border_radius.dart:621`), and `resolve` says which wall that is.
579    #[test]
580    fn a_directional_radius_follows_the_reading_direction() {
581        let end = BorderRadiusDirectional::end(16.0);
582        assert_eq!(
583            end.resolve(TextDirection::Ltr),
584            BorderRadius::right(16.0),
585            "the line ends on the right in English"
586        );
587        assert_eq!(
588            end.resolve(TextDirection::Rtl),
589            BorderRadius::left(16.0),
590            "and on the left in Arabic"
591        );
592
593        let start = BorderRadiusDirectional::start(16.0);
594        assert_eq!(start.resolve(TextDirection::Ltr), BorderRadius::left(16.0));
595        assert_eq!(start.resolve(TextDirection::Rtl), BorderRadius::right(16.0));
596
597        // Both sides at once, each keeping its own number across the mirror.
598        let both = BorderRadiusDirectional::horizontal(4.0, 12.0);
599        assert_eq!(
600            both.resolve(TextDirection::Ltr),
601            BorderRadius {
602                top_left: 4.0,
603                top_right: 12.0,
604                bottom_right: 12.0,
605                bottom_left: 4.0,
606            }
607        );
608        assert_eq!(
609            both.resolve(TextDirection::Rtl),
610            BorderRadius {
611                top_left: 12.0,
612                top_right: 4.0,
613                bottom_right: 4.0,
614                bottom_left: 12.0,
615            }
616        );
617
618        // What has no side does not move.
619        for direction in [TextDirection::Ltr, TextDirection::Rtl] {
620            assert_eq!(
621                BorderRadiusDirectional::uniform(8.0).resolve(direction),
622                BorderRadius::uniform(8.0)
623            );
624            assert_eq!(
625                BorderRadiusDirectional::vertical(8.0, 0.0).resolve(direction),
626                BorderRadius::top(8.0)
627            );
628        }
629    }
630    use super::*;
631    use crate::Primitive;
632
633    fn rect() -> Rect {
634        Rect::new(10.0, 20.0, 100.0, 40.0)
635    }
636
637    #[test]
638    fn content_padding_reserves_the_border() {
639        let deco = BoxDecoration::filled(Color::WHITE).border(Border::new(2.0, Color::BLACK));
640        assert_eq!(deco.content_padding(), Insets::uniform(2.0));
641        // An invisible border (zero width) → no padding.
642        let none = BoxDecoration::filled(Color::WHITE).border(Border::new(0.0, Color::BLACK));
643        assert_eq!(none.content_padding(), Insets::ZERO);
644    }
645
646    #[test]
647    fn empty_decoration_paints_nothing() {
648        let mut scene = Scene::new();
649        BoxDecoration::default().paint_into(&mut scene, rect(), 1.0);
650        assert!(scene.is_empty());
651    }
652
653    #[test]
654    fn fixed_paint_order_shadow_then_fill() {
655        let mut scene = Scene::new();
656        let deco = BoxDecoration::filled(Color::WHITE)
657            .radius(6.0)
658            .shadow(BoxShadow::new(
659                0.0,
660                4.0,
661                8.0,
662                Color::rgba(0.0, 0.0, 0.0, 0.5),
663            ));
664        deco.paint_into(&mut scene, rect(), 1.0);
665        // Two primitives: the shadow first, then the background.
666        assert_eq!(scene.len(), 2);
667        match scene.primitives()[0] {
668            Primitive::Rect { blur, .. } => {
669                assert!(blur > 0.0, "first primitive = the blurred shadow")
670            }
671            _ => panic!("expected a rectangle"),
672        }
673        match scene.primitives()[1] {
674            Primitive::Rect { blur, color, .. } => {
675                assert_eq!(blur, 0.0, "second primitive = the crisp background");
676                assert_eq!(color, Color::WHITE);
677            }
678            _ => panic!("expected a rectangle"),
679        }
680    }
681
682    #[test]
683    fn opacity_fades_all_colours() {
684        let mut scene = Scene::new();
685        BoxDecoration::filled(Color::rgb(1.0, 0.0, 0.0))
686            .border(Border::new(2.0, Color::rgb(0.0, 1.0, 0.0)))
687            .paint_into(&mut scene, rect(), 0.5);
688        match scene.primitives()[0] {
689            Primitive::Rect {
690                color,
691                border_color,
692                ..
693            } => {
694                assert_eq!(color.a, 0.5);
695                assert_eq!(border_color.a, 0.5);
696            }
697            _ => panic!("expected a rectangle"),
698        }
699    }
700
701    #[test]
702    fn border_only_paints_transparent_fill_with_stroke() {
703        let mut scene = Scene::new();
704        BoxDecoration::default()
705            .border(Border::new(1.0, Color::WHITE))
706            .paint_into(&mut scene, rect(), 1.0);
707        assert_eq!(scene.len(), 1);
708        match scene.primitives()[0] {
709            Primitive::Rect {
710                color,
711                border_width,
712                ..
713            } => {
714                assert_eq!(color, Color::TRANSPARENT);
715                assert_eq!(border_width, 1.0);
716            }
717            _ => panic!("expected a rectangle"),
718        }
719    }
720
721    #[test]
722    fn shadow_bounds_grow_with_blur_and_spread() {
723        let s = BoxShadow::new(0.0, 0.0, 4.0, Color::BLACK).spread(2.0);
724        let b = s.bounds(Rect::new(0.0, 0.0, 10.0, 10.0));
725        // grow = blur + spread = 6 on every side.
726        assert_eq!(b, Rect::new(-6.0, -6.0, 22.0, 22.0));
727    }
728}
729
730/// The decoration in flight (milestone 501): one rule per part, and the ends exact.
731#[cfg(test)]
732mod lerp_tests {
733    use super::*;
734
735    const RED: Color = Color {
736        r: 1.0,
737        g: 0.0,
738        b: 0.0,
739        a: 1.0,
740    };
741    const BLUE: Color = Color {
742        r: 0.0,
743        g: 0.0,
744        b: 1.0,
745        a: 1.0,
746    };
747
748    /// **The ends are the ends, exactly** — and past them, and at a progress that is not a
749    /// number, the nearer end.
750    #[test]
751    fn the_ends_are_the_ends_exactly() {
752        let a = BoxDecoration::filled(RED).radius(4.0);
753        let b = BoxDecoration::filled(BLUE).border(Border::new(2.0, RED));
754        assert_eq!(a.lerp(b, 0.0), a);
755        assert_eq!(a.lerp(b, 1.0), b);
756        assert_eq!(a.lerp(b, -0.5), a, "an undershoot stops at the start");
757        assert_eq!(a.lerp(b, 1.5), b, "an overshoot stops at the end");
758        assert_eq!(a.lerp(b, f32::NAN), a, "and no progress is no progress");
759    }
760
761    /// **A fill fading out keeps its hue.** The obvious interpolation — towards
762    /// `Color::TRANSPARENT` — is towards transparent *black*, and half-way there a red is a
763    /// half-opaque dark red: a movement that was only about disappearing darkens on the way.
764    #[test]
765    fn a_fill_fading_out_does_not_pass_through_black() {
766        let mid = BoxDecoration::filled(RED).lerp(BoxDecoration::default(), 0.5);
767        let c = mid.color.expect("still painted, half faded");
768        assert_eq!((c.r, c.g, c.b), (1.0, 0.0, 0.0), "the same red");
769        assert!((c.a - 0.5).abs() < 1e-6, "at half its opacity: {c:?}");
770        let naive = RED.lerp(Color::TRANSPARENT, 0.5);
771        assert!(naive.r < 0.6, "the trap this avoids: {naive:?}");
772    }
773
774    /// And one fading in arrives in its own colour, only fainter.
775    #[test]
776    fn a_fill_fading_in_arrives_in_its_own_colour() {
777        let c = BoxDecoration::default()
778            .lerp(BoxDecoration::filled(BLUE), 0.25)
779            .color
780            .expect("already there, faintly");
781        assert_eq!((c.r, c.g, c.b), (0.0, 0.0, 1.0));
782        assert!((c.a - 0.25).abs() < 1e-6, "{c:?}");
783    }
784
785    /// **A flat fill is a gradient whose two ends agree**, so flat to graded spreads the
786    /// far end out of the fill: the start does not move and the end travels.
787    #[test]
788    fn a_flat_fill_spreads_into_a_gradient() {
789        let flat = BoxDecoration::filled(RED);
790        let graded = BoxDecoration::filled(RED).gradient(LinearGradient::new(BLUE, [0.0, 1.0]));
791        let mid = flat.lerp(graded, 0.5);
792        assert_eq!(mid.color, Some(RED), "the start does not move");
793        let g = mid.gradient.expect("a gradient half-way to its far colour");
794        assert_eq!(g.end, RED.lerp(BLUE, 0.5));
795        assert_eq!(g.direction, [0.0, 1.0]);
796    }
797
798    /// **A border arrives by thickening, in its own colour** — not as a colour on its way
799    /// from nowhere.
800    #[test]
801    fn a_border_arrives_by_thickening_in_its_own_colour() {
802        let lined = BoxDecoration::filled(RED).border(Border::new(4.0, BLUE));
803        let quarter = BoxDecoration::filled(RED)
804            .lerp(lined, 0.25)
805            .border
806            .expect("a line already");
807        assert_eq!(quarter.width, 1.0);
808        assert_eq!(quarter.color, BLUE);
809    }
810
811    /// **A shadow arrives by growing out from under the box as it fades in.**
812    #[test]
813    fn a_shadow_grows_and_fades_in() {
814        let raised = BoxDecoration::filled(RED).shadow(BoxShadow::new(
815            0.0,
816            8.0,
817            16.0,
818            Color::rgba(0.0, 0.0, 0.0, 0.4),
819        ));
820        let half = BoxDecoration::filled(RED)
821            .lerp(raised, 0.5)
822            .shadow
823            .expect("a shadow already");
824        assert_eq!(half.offset, (0.0, 4.0));
825        assert_eq!(half.blur, 8.0);
826        assert!((half.color.a - 0.2).abs() < 1e-6, "{:?}", half.color);
827    }
828
829    /// Corners travel one by one, each from its own start.
830    #[test]
831    fn corners_travel_one_by_one() {
832        let r = BoxDecoration::default()
833            .radius(BorderRadius::top(8.0))
834            .lerp(BoxDecoration::default().radius(16.0), 0.5)
835            .radius;
836        assert_eq!((r.top_left, r.bottom_left), (12.0, 8.0));
837    }
838}