Skip to main content

frus_widgets/
container.rs

1//! [`Container`]: a decorated box (size, padding, color, rounded corners, border,
2//! click) with an optional child.
3
4use frus_core::{
5    AlignmentGeometry, Border, BorderRadius, BoxDecoration, BoxShadow, Color, Curve, Insets,
6    LinearGradient, Rect, Scene, Size,
7};
8use frus_layout::{Align, Dimension, Justify, Style};
9
10use crate::interaction::Status;
11use crate::theme::Theme;
12use crate::widget::Widget;
13
14/// An easing curve (smoothstep) to soften the transitions.
15fn ease(t: f32) -> f32 {
16    let t = t.clamp(0.0, 1.0);
17    t * t * (3.0 - 2.0 * t)
18}
19
20/// A decorated rectangular box.
21pub struct Container<Msg = crate::callback::Callback> {
22    width: Dimension,
23    height: Dimension,
24    flex_grow: f32,
25    flex_shrink: f32,
26    padding: Insets,
27    /// The **outer** margin (around the box, outside the decoration).
28    margin: Insets,
29    radius: BorderRadius,
30    /// Clip the child to this box's rounded rectangle.
31    clip: bool,
32    /// A decoration painted **over** the child rather than behind it; see
33    /// [`Container::foreground`].
34    foreground: Option<BoxDecoration>,
35    border_width: f32,
36    border_color: Color,
37    color: Option<Color>,
38    hover_color: Option<Color>,
39    pressed_color: Option<Color>,
40    /// The gradient: (end color, direction in `[0,1]²` space).
41    gradient: Option<(Color, [f32; 2])>,
42    /// The shadow: (dx, dy, blur, color).
43    shadow: Option<(f32, f32, f32, Color)>,
44    on_click: Option<Msg>,
45    on_long_press: Option<Msg>,
46    /// A repaint boundary: it caches the painted subtree (see
47    /// [`crate::Widget::repaint_boundary`]).
48    repaint_boundary: bool,
49    /// A **group** opacity `[0,1]` applied to the whole subtree. `None` = opaque.
50    opacity: Option<f32>,
51    /// When the group opacity is **animated**: the transition's `(duration, curve)`.
52    /// `None` = a fixed opacity, with no transition.
53    opacity_anim: Option<(f32, Curve)>,
54    /// When the background is an **animated color**: the transition's `(target,
55    /// duration, curve)`. `None` = a fixed color.
56    color_anim: Option<(Color, f32, Curve)>,
57    /// When the **size** is animated: `(target, duration, curve)` — interpolated at
58    /// layout time. `None` = a fixed size.
59    size_anim: Option<(Size, f32, Curve)>,
60    /// When the **corner radius** is animated: `(target, duration, curve)` —
61    /// interpolated at paint time. `None` = a fixed radius.
62    radius_anim: Option<(BorderRadius, f32, Curve)>,
63    /// When the **padding** is animated: `(duration, curve)` — the target is
64    /// `self.padding`, interpolated at layout time. `None` = fixed padding.
65    padding_anim: Option<(f32, Curve)>,
66    /// `(duration, curve)` of an **animated anchor**; `None` = the anchor is fixed.
67    alignment_anim: Option<(f32, Curve)>,
68    /// The child's anchoring within the box, physical or directional (resolved for
69    /// RTL at render time). `None` = the default flex behaviour, in which the child
70    /// stretches to fill.
71    alignment: Option<AlignmentGeometry>,
72    children: Vec<Box<dyn Widget<Msg>>>,
73}
74
75impl<Msg> Container<Msg> {
76    /// Creates an empty container (automatic size, no decoration).
77    pub fn new() -> Self {
78        Self {
79            width: Dimension::Auto,
80            height: Dimension::Auto,
81            flex_grow: 0.0,
82            flex_shrink: 0.0,
83            padding: Insets::ZERO,
84            margin: Insets::ZERO,
85            radius: BorderRadius::ZERO,
86            clip: false,
87            foreground: None,
88            border_width: 0.0,
89            border_color: Color::TRANSPARENT,
90            color: None,
91            hover_color: None,
92            pressed_color: None,
93            gradient: None,
94            shadow: None,
95            on_click: None,
96            on_long_press: None,
97            repaint_boundary: false,
98            opacity: None,
99            opacity_anim: None,
100            color_anim: None,
101            size_anim: None,
102            radius_anim: None,
103            padding_anim: None,
104            alignment_anim: None,
105            alignment: None,
106            children: Vec::new(),
107        }
108    }
109
110    /// The **effective** layout padding: the inner padding plus, when a border is
111    /// visible, the room it reserves (the content of a bordered box is not eaten by
112    /// the stroke). The single source for `style()` and for the **target** of an
113    /// animated padding (`anim_padding`).
114    fn effective_padding(&self) -> Insets {
115        let mut padding = self.padding;
116        if Border::new(self.border_width, self.border_color).is_visible() {
117            padding.top += self.border_width;
118            padding.right += self.border_width;
119            padding.bottom += self.border_width;
120            padding.left += self.border_width;
121        }
122        padding
123    }
124
125    /// Marks this container as a **repaint boundary**: its subtree is cached and
126    /// reused for as long as its geometry and its descendants' interaction state
127    /// stay stable. To be placed around **static** content that would otherwise be
128    /// repainted on every frame of a neighbouring animation.
129    pub fn repaint_boundary(mut self) -> Self {
130        self.repaint_boundary = true;
131        self
132    }
133
134    /// Sets the width, in logical pixels.
135    pub fn width(mut self, width: f32) -> Self {
136        self.width = Dimension::Length(width);
137        self
138    }
139
140    /// Sets the height, in logical pixels.
141    pub fn height(mut self, height: f32) -> Self {
142        self.height = Dimension::Length(height);
143        self
144    }
145
146    /// Sets the width as a **fraction of the space the parent offers** — `1.0` fills it.
147    pub fn width_fraction(mut self, fraction: f32) -> Self {
148        self.width = Dimension::Percent(fraction);
149        self
150    }
151
152    /// Flex growth factor along the parent's main axis.
153    pub fn flex(mut self, grow: f32) -> Self {
154        self.flex_grow = grow;
155        self
156    }
157
158    /// How much of a row's deficit this box absorbs. The default is `0.0` — the
159    /// reference's rule, where an inflexible child is never squeezed and a row that does
160    /// not fit overflows and says so.
161    ///
162    /// `shrink(1.0)` asks for flexbox's behaviour instead: give way rather than let the
163    /// row run over. It is the right answer for a box whose size is a preference rather
164    /// than a requirement, and the wrong one for fixed chrome — an icon button at the end
165    /// of a row should keep its width however long the label beside it grows.
166    pub fn shrink(mut self, shrink: f32) -> Self {
167        self.flex_shrink = shrink;
168        self
169    }
170
171    /// This box never shrinks — the default said out loud, kept because a layout that
172    /// depends on it reads better for saying so. See [`Self::shrink`].
173    pub fn no_shrink(self) -> Self {
174        self.shrink(0.0)
175    }
176
177    /// Uniform inner padding, in logical pixels.
178    pub fn padding(mut self, padding: f32) -> Self {
179        self.padding = Insets::uniform(padding);
180        self
181    }
182
183    /// Inner padding per side (top, right, bottom, left).
184    pub fn padding_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
185        self.padding = Insets::new(top, right, bottom, left);
186        self
187    }
188
189    /// A uniform **outer** margin: space reserved **around** the box — outside the
190    /// decoration, it pushes the siblings away without growing the background or
191    /// the border.
192    pub fn margin(mut self, margin: f32) -> Self {
193        self.margin = Insets::uniform(margin);
194        self
195    }
196
197    /// Outer margin per side (top, right, bottom, left).
198    pub fn margin_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
199        self.margin = Insets::new(top, right, bottom, left);
200        self
201    }
202
203    /// The rounded corner radii: uniform via `f32` (`.radius(10.0)`) or per corner
204    /// via [`BorderRadius`] (`.radius(BorderRadius::top(12.0))`).
205    pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
206        self.radius = radius.into();
207        self
208    }
209
210    /// **Clips** the child to this box, corners included.
211    ///
212    /// Off by default, as in the reference: a decoration is painted *behind* a child
213    /// and does not confine it, so a box with rounded corners leaves an image inside it
214    /// square unless it is asked. Asking costs a compositing layer, which is why neither
215    /// framework does it for you.
216    ///
217    /// [`crate::ClipRRect`] is the same thing without the decoration, and is what to
218    /// reach for when there is no box to decorate.
219    pub fn clip(mut self) -> Self {
220        self.clip = true;
221        self
222    }
223
224    /// The border: width (in px) and color.
225    pub fn border(mut self, width: f32, color: Color) -> Self {
226        self.border_width = width;
227        self.border_color = color;
228        self
229    }
230
231    /// The background color at rest.
232    pub fn color(mut self, color: Color) -> Self {
233        self.color = Some(color);
234        self
235    }
236
237    /// The background color on hover.
238    pub fn hover_color(mut self, color: Color) -> Self {
239        self.hover_color = Some(color);
240        self
241    }
242
243    /// The background color when pressed.
244    pub fn pressed_color(mut self, color: Color) -> Self {
245        self.pressed_color = Some(color);
246        self
247    }
248
249    /// A linear background gradient (`color` → `end`), with `dir` in `[0,1]²` space
250    /// (`[0.0, 1.0]` = top→bottom, for instance).
251    pub fn gradient(mut self, end: Color, dir: [f32; 2]) -> Self {
252        self.gradient = Some((end, dir));
253        self
254    }
255
256    /// A drop shadow: the `(dx, dy)` offset, the blur radius and the color.
257    pub fn shadow(mut self, dx: f32, dy: f32, blur: f32, color: Color) -> Self {
258        self.shadow = Some((dx, dy, blur, color));
259        self
260    }
261
262    /// The message emitted when the container is clicked.
263    pub fn on_click(mut self, message: impl Into<Msg>) -> Self {
264        self.on_click = Some(message.into());
265        self
266    }
267
268    /// The message emitted by a **long press** (a press held without movement). A
269    /// long press supersedes the click.
270    pub fn on_long_press(mut self, message: impl Into<Msg>) -> Self {
271        self.on_long_press = Some(message.into());
272        self
273    }
274
275    /// Sets the container's child.
276    pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
277        self.children.clear();
278        self.children.push(Box::new(child));
279        self
280    }
281
282    /// Applies a **group opacity** `[0,1]` to the whole subtree, as one block: the
283    /// rendering goes through a composited layer, so overlaps are not
284    /// double-blended. `1.0` = no effect at all.
285    pub fn opacity(mut self, opacity: f32) -> Self {
286        self.opacity = Some(opacity.clamp(0.0, 1.0));
287        self
288    }
289
290    /// Like [`Container::opacity`], but the opacity **animates** toward `opacity` on
291    /// every change, with `duration` (in seconds) and `curve`. The fade applies to
292    /// the whole group.
293    pub fn animated_opacity(mut self, opacity: f32, duration: f32, curve: Curve) -> Self {
294        self.opacity = Some(opacity.clamp(0.0, 1.0));
295        self.opacity_anim = Some((duration, curve));
296        self
297    }
298
299    /// A background whose color **animates** toward `color` on every change, with
300    /// `duration` (in seconds) and `curve`. The runtime interpolates from the
301    /// current color; on mount it adopts `color` with no transition. Note: one box
302    /// shares a single `(duration, curve)` across its animations (opacity, color).
303    pub fn animated_color(mut self, color: Color, duration: f32, curve: Curve) -> Self {
304        self.color = Some(color);
305        self.color_anim = Some((color, duration, curve));
306        self
307    }
308
309    /// A box whose **size** animates toward `width×height` on every change, with
310    /// `duration` and `curve`. The interpolated size is injected **at layout time**
311    /// (the children reposition accordingly). On mount it adopts the target with no
312    /// transition.
313    pub fn animated_size(mut self, width: f32, height: f32, duration: f32, curve: Curve) -> Self {
314        self.width = Dimension::Length(width);
315        self.height = Dimension::Length(height);
316        self.size_anim = Some((Size::new(width, height), duration, curve));
317        self
318    }
319
320    /// A box whose **corner radius** animates toward `radius` on every change, with
321    /// `duration` and `curve` — the corners morph smoothly. Uniform via `f32`, or
322    /// per corner via [`BorderRadius`]. On mount it adopts the target with no
323    /// transition.
324    pub fn animated_radius(
325        mut self,
326        radius: impl Into<BorderRadius>,
327        duration: f32,
328        curve: Curve,
329    ) -> Self {
330        let radius = radius.into();
331        self.radius = radius;
332        self.radius_anim = Some((radius, duration, curve));
333        self
334    }
335
336    /// A box whose (uniform) **inner padding** animates toward `padding` on every
337    /// change, with `duration` and `curve`. The interpolated padding is injected
338    /// **at layout time** (the content repositions). On mount it adopts the target
339    /// with no transition.
340    pub fn animated_padding(mut self, padding: f32, duration: f32, curve: Curve) -> Self {
341        self.padding = Insets::uniform(padding);
342        self.padding_anim = Some((duration, curve));
343        self
344    }
345
346    /// A box whose **anchor** animates toward `alignment` on every change, with
347    /// `duration` and `curve`: the child slides through the free space instead of
348    /// jumping across it. On mount it adopts the anchor with no transition.
349    ///
350    /// It interpolates the anchor's own two fractions, so a **directional** anchor stays
351    /// directional the whole way and mirrors as a whole in a right-to-left script.
352    pub fn animated_alignment(
353        mut self,
354        alignment: impl Into<AlignmentGeometry>,
355        duration: f32,
356        curve: Curve,
357    ) -> Self {
358        self.alignment = Some(alignment.into());
359        self.alignment_anim = Some((duration, curve));
360        self
361    }
362
363    /// **Anchors the child** within the box: centred, in a corner, against an edge…
364    /// Accepts a **physical** anchor ([`Alignment`](frus_core::Alignment)) or a
365    /// **directional** one
366    /// ([`AlignmentDirectional`](frus_core::AlignmentDirectional), resolved for RTL
367    /// at render time) — both through `Into`. By default, with no anchor, the child
368    /// stretches to fill the container (the flex behaviour); setting an anchor
369    /// leaves the child at its natural size and positions it.
370    pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
371        self.alignment = Some(alignment.into());
372        self
373    }
374
375    /// Applies a **composite decoration** as one block: background, gradient, border,
376    /// radius and shadow gathered in a reusable [`BoxDecoration`]. Each part present
377    /// overrides the corresponding setting; the radius is always adopted. The
378    /// animations (color, radius…) still apply on top. (A shadow's `spread` is not
379    /// kept — the container's shadow model has none.)
380    /// A decoration painted **over the child** rather than behind it — the reference's
381    /// `foregroundDecoration`.
382    ///
383    /// [`Container::decoration`] and every part it is made of go *behind* the content,
384    /// which is what a background is for and no use at all when the point is that the
385    /// content does not cover it: an outline over a photograph, a wash across a tile
386    /// that is out of stock, a sheen over a card. There is no other way to say it short
387    /// of a [`crate::Stack`].
388    ///
389    /// It is painted on the container's own box, the same one the background uses, and
390    /// it is inside anything the container asks for — an opacity fades it with
391    /// everything else, [`Container::clip`] holds it to the corners.
392    ///
393    /// One convenience the reference does not have: a foreground that names **no**
394    /// radius takes the container's, since a square outline over a rounded box is
395    /// almost always a mistake rather than a request. Set one explicitly — including
396    /// [`frus_core::BorderRadius::ZERO`] — to say otherwise.
397    ///
398    /// ```
399    /// # use frus_widgets::Container;
400    /// # use frus_core::{Border, BoxDecoration, Color};
401    /// # let photo = Container::<()>::new();
402    /// Container::<()>::new()
403    ///     .radius(12.0)
404    ///     .child(photo)
405    ///     .foreground(BoxDecoration::default().border(Border::new(1.0, Color::rgba(0.0, 0.0, 0.0, 0.2))));
406    /// ```
407    pub fn foreground(mut self, decoration: BoxDecoration) -> Self {
408        self.foreground = Some(decoration);
409        self
410    }
411
412    /// Sets the box's fill, gradient, border and shadow from a
413    /// [`BoxDecoration`](frus_core::BoxDecoration) at once. What the decoration leaves unset
414    /// changes nothing.
415    pub fn decoration(mut self, decoration: BoxDecoration) -> Self {
416        if let Some(color) = decoration.color {
417            self.color = Some(color);
418        }
419        if let Some(gradient) = decoration.gradient {
420            self.gradient = Some((gradient.end, gradient.direction));
421        }
422        if let Some(border) = decoration.border {
423            self.border_width = border.width;
424            self.border_color = border.color;
425        }
426        self.radius = decoration.radius;
427        if let Some(shadow) = decoration.shadow {
428            self.shadow = Some((shadow.offset.0, shadow.offset.1, shadow.blur, shadow.color));
429        }
430        self
431    }
432}
433
434impl<Msg> Default for Container<Msg> {
435    fn default() -> Self {
436        Self::new()
437    }
438}
439
440impl<Msg: Clone> Widget<Msg> for Container<Msg> {
441    fn style(&self) -> Style {
442        let mut style = Style {
443            width: self.width,
444            height: self.height,
445            flex_grow: self.flex_grow,
446            flex_shrink: self.flex_shrink,
447            padding: self.effective_padding(),
448            margin: self.margin,
449            ..Default::default()
450        };
451        // Anchoring the child: taffy is left to place it at the **top left** of the
452        // content box, at its natural size (Start / Start, no stretching), and the
453        // walk then offsets it within the free space according to the `Alignment`'s
454        // fractions (a manual, fractional placement — outside the discrete flex).
455        if self.alignment.is_some() {
456            style.justify = Justify::Start;
457            style.align = Align::Start;
458        }
459        style
460    }
461
462    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
463        &self.children
464    }
465
466    fn paint(&self, bounds: Rect, status: Status, _theme: &Theme, scene: &mut Scene) {
467        // A background with an **animated color**: the color the runtime interpolates
468        // wins (the hover/press interpolation does not apply to an animated background).
469        // Otherwise two progressions in a row — rest → hover, then that → held. The press
470        // used to be the one step here that was instant; it is a progression since
471        // milestone 441, and crossing over from wherever the hover had got to is what
472        // keeps a pointer that presses mid-fade from jumping backwards first.
473        let color = if self.color_anim.is_some() {
474            status.anim_color.or(self.color)
475        } else {
476            let settled = match (self.color, self.hover_color) {
477                (Some(base), Some(hover)) => Some(base.lerp(hover, ease(status.hover_progress))),
478                _ => self.color,
479            };
480            let press = ease(status.press_progress.clamp(0.0, 1.0));
481            match (settled, self.pressed_color.or(self.hover_color)) {
482                (Some(from), Some(held)) => Some(from.lerp(held, press)),
483                // Nothing to cross over *from*: a container with a held colour and no
484                // resting one at all has no fill to fade, so it appears with the press.
485                (None, held) if press > 0.0 => held,
486                (settled, _) => settled,
487            }
488        };
489
490        // Compose the decoration (background/gradient/border/shadow) and lower it into
491        // primitives in the fixed order shadow → background → border. The opacity
492        // (the fade-in) modulates every color.
493        // An animated radius: the radius the runtime interpolates wins over the fixed one.
494        let radius = if self.radius_anim.is_some() {
495            status.anim_radius.unwrap_or(self.radius)
496        } else {
497            self.radius
498        };
499
500        let decoration = BoxDecoration {
501            color,
502            gradient: self
503                .gradient
504                .map(|(end, dir)| LinearGradient::new(end, dir)),
505            border: (self.border_width > 0.0)
506                .then(|| Border::new(self.border_width, self.border_color)),
507            radius,
508            shadow: self
509                .shadow
510                .map(|(dx, dy, blur, c)| BoxShadow::new(dx, dy, blur, c)),
511        };
512        decoration.paint_into(scene, bounds, status.opacity);
513    }
514
515    fn clip_shape(&self) -> Option<frus_core::ClipShape> {
516        self.clip
517            .then(|| frus_core::ClipShape::RRect(self.radius.clamped()))
518    }
519
520    fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
521        self.foreground.map(|decoration| {
522            if decoration.radius == frus_core::BorderRadius::ZERO {
523                decoration.radius(self.radius)
524            } else {
525                decoration
526            }
527        })
528    }
529
530    fn on_click(&self) -> Option<Msg> {
531        self.on_click.clone()
532    }
533
534    fn on_long_press(&self) -> Option<Msg> {
535        self.on_long_press.clone()
536    }
537
538    fn repaint_boundary(&self) -> bool {
539        self.repaint_boundary
540    }
541
542    fn opacity_group(&self) -> Option<f32> {
543        self.opacity
544    }
545
546    /// The animated opacity's target (only when `animated_opacity` is set) — this is
547    /// the value the runtime tweens and the walk reads back for the layer.
548    fn anim_target(&self) -> Option<f32> {
549        self.opacity_anim.as_ref().and(self.opacity)
550    }
551
552    fn anim_color(&self) -> Option<Color> {
553        self.color_anim.as_ref().and(self.color)
554    }
555
556    fn anim_size(&self) -> Option<Size> {
557        self.size_anim.as_ref().map(|(s, _, _)| *s)
558    }
559
560    fn anim_radius(&self) -> Option<BorderRadius> {
561        self.radius_anim.as_ref().map(|(r, _, _)| *r)
562    }
563
564    fn anim_padding(&self) -> Option<Insets> {
565        // The target = the effective padding (content + border), consistent with `style()`.
566        self.padding_anim.as_ref().map(|_| self.effective_padding())
567    }
568
569    fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
570        self.alignment
571    }
572
573    fn anim_offset(&self) -> Option<(f32, f32)> {
574        // The target = the anchor's own fractions, which is what the walk puts back into
575        // an anchor of the same kind.
576        self.alignment_anim
577            .as_ref()
578            .and(self.alignment)
579            .map(|a| a.fractions())
580    }
581
582    fn anim_duration(&self) -> f32 {
583        // One box's animations (opacity/color/size/radius/padding) share a single
584        // duration (in the order: opacity, color, size, radius, padding).
585        self.opacity_anim
586            .as_ref()
587            .map(|(d, _)| *d)
588            .or(self.color_anim.as_ref().map(|(_, d, _)| *d))
589            .or(self.size_anim.as_ref().map(|(_, d, _)| *d))
590            .or(self.radius_anim.as_ref().map(|(_, d, _)| *d))
591            .or(self.padding_anim.as_ref().map(|(d, _)| *d))
592            .or(self.alignment_anim.as_ref().map(|(d, _)| *d))
593            .unwrap_or(crate::runtime::ANIM_DURATION)
594    }
595
596    fn anim_curve(&self) -> Curve {
597        self.opacity_anim
598            .as_ref()
599            .map(|(_, c)| c.clone())
600            .or(self.color_anim.as_ref().map(|(_, _, c)| c.clone()))
601            .or(self.size_anim.as_ref().map(|(_, _, c)| c.clone()))
602            .or(self.radius_anim.as_ref().map(|(_, _, c)| c.clone()))
603            .or(self.padding_anim.as_ref().map(|(_, c)| c.clone()))
604            .or(self.alignment_anim.as_ref().map(|(_, c)| c.clone()))
605            .unwrap_or(Curve::Linear)
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612
613    /// A box in an over-budget row **keeps its width** — the reference's rule, and the
614    /// default since milestone 349. Flexbox's default was the opposite, and the smallest
615    /// fixed thing in the row paid for the whole deficit: milestone 333 found an icon
616    /// button at 13 px of 40, drawn off the card and out of the hit registry.
617    ///
618    /// `shrink(1.0)` asks for flexbox's behaviour back, for a box that would rather give
619    /// way than let the row overflow.
620    #[test]
621    fn a_box_in_an_over_budget_row_keeps_its_width() {
622        use crate::interaction::WidgetId;
623        use crate::runtime::Runtime;
624        use crate::theme::Theme;
625        use crate::Flex;
626
627        let widths = |row: &Flex<()>| -> Vec<f32> {
628            let runtime = Runtime::default();
629            let theme = Theme::light();
630            let mut layout = frus_layout::Layout::new();
631            let node = crate::ui::build_layout(row, WidgetId::ROOT, &runtime, &theme, &mut layout);
632            layout.compute_filled(node, 100.0, 50.0);
633            layout
634                .absolute_rects(node)
635                .iter()
636                .skip(1)
637                .take(2)
638                .map(|(r, _)| r.width)
639                .collect()
640        };
641
642        // 120 px of children in 100 px of row. Nobody was asked to give way, so nobody
643        // does, and the row overflows — which is now visible, striped and labelled.
644        let row = Flex::row()
645            .child(Container::<()>::new().width(80.0).height(20.0))
646            .child(Container::<()>::new().width(40.0).height(20.0));
647        assert_eq!(
648            widths(&row),
649            vec![80.0, 40.0],
650            "an inflexible child is never squeezed"
651        );
652
653        // Unless it says so, and then the whole deficit is its own.
654        let giving_way = Flex::row()
655            .child(Container::<()>::new().width(80.0).height(20.0).shrink(1.0))
656            .child(Container::<()>::new().width(40.0).height(20.0));
657        assert_eq!(widths(&giving_way), vec![60.0, 40.0]);
658    }
659
660    /// **A held box crosses over to its pressed colour** (milestone 441) instead of
661    /// snapping to it. The line that chose it read `Interaction::Pressed` and the comment
662    /// beside it said "Pressed: instant" — the one step of the rest → hover → held path
663    /// that was not a progression.
664    #[test]
665    fn a_held_box_crosses_over_to_its_pressed_colour() {
666        use frus_core::Primitive;
667
668        let rest = Color::rgb(0.2, 0.2, 0.2);
669        let held = Color::rgb(0.8, 0.8, 0.8);
670        let box_: Container<()> = Container::new()
671            .width(20.0)
672            .height(20.0)
673            .color(rest)
674            .pressed_color(held);
675
676        let fill = |press: f32| {
677            let mut scene = Scene::new();
678            Widget::<()>::paint(
679                &box_,
680                Rect::new(0.0, 0.0, 20.0, 20.0),
681                Status {
682                    press_progress: press,
683                    ..Default::default()
684                },
685                &crate::Theme::dark(),
686                &mut scene,
687            );
688            scene
689                .primitives()
690                .iter()
691                .find_map(|p| match p {
692                    Primitive::Rect { color, .. } => Some(*color),
693                    _ => None,
694                })
695                .expect("a box paints its background")
696        };
697
698        assert_eq!(fill(0.0), rest, "untouched");
699        assert_eq!(fill(1.0), held, "held");
700        let half = fill(0.5).r;
701        assert!(
702            half > rest.r && half < held.r,
703            "half way through the press: {half}"
704        );
705    }
706
707    /// A `Container` with a group opacity < 1 has its painted subtree wrapped in a
708    /// [`frus_core::Primitive::Layer`] at that opacity.
709    #[test]
710    fn opacity_group_wraps_subtree_in_a_layer() {
711        use frus_core::{Primitive, Size};
712        let root: Container<()> = Container::new()
713            .width(40.0)
714            .height(40.0)
715            .color(Color::rgb(1.0, 0.0, 0.0))
716            .opacity(0.5);
717        let rt = crate::runtime::Runtime::default();
718        let theme = crate::Theme::dark();
719        let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
720        let layer = ui.scene().primitives().iter().find_map(|p| match p {
721            Primitive::Layer {
722                opacity,
723                primitives,
724                ..
725            } => Some((*opacity, primitives.len())),
726            _ => None,
727        });
728        let (op, n) = layer.expect("a group opacity layer");
729        assert!((op - 0.5).abs() < 1e-6, "group opacity = {op}");
730        assert!(
731            n >= 1,
732            "the layer wraps the painted content ({n} primitives)"
733        );
734    }
735
736    /// Full opacity (`1.0`): no layer is emitted (the opaque path, at zero cost).
737    #[test]
738    fn full_opacity_emits_no_layer() {
739        use frus_core::{Primitive, Size};
740        let root: Container<()> = Container::new()
741            .width(40.0)
742            .height(40.0)
743            .color(Color::rgb(1.0, 0.0, 0.0))
744            .opacity(1.0);
745        let rt = crate::runtime::Runtime::default();
746        let theme = crate::Theme::dark();
747        let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
748        assert!(
749            !ui.scene()
750                .primitives()
751                .iter()
752                .any(|p| matches!(p, Primitive::Layer { .. })),
753            "no layer at full opacity"
754        );
755    }
756
757    /// `animated_opacity` declares an animated value (the runtime tweens it) with the
758    /// duration and curve supplied; `opacity` alone does not (a fixed opacity).
759    #[test]
760    fn animated_opacity_declares_anim_target() {
761        let animated: Container<()> = Container::new().animated_opacity(0.0, 0.3, Curve::ease_in());
762        assert_eq!(Widget::<()>::anim_target(&animated), Some(0.0));
763        assert_eq!(Widget::<()>::anim_duration(&animated), 0.3);
764        assert_eq!(Widget::<()>::anim_curve(&animated), Curve::ease_in());
765        assert_eq!(Widget::<()>::opacity_group(&animated), Some(0.0));
766
767        // A fixed opacity: a group, yes, but no animated value.
768        let fixed: Container<()> = Container::new().opacity(0.5);
769        assert_eq!(Widget::<()>::anim_target(&fixed), None);
770        assert_eq!(Widget::<()>::opacity_group(&fixed), Some(0.5));
771    }
772
773    /// An `animated_color` background paints the color the runtime **interpolates**
774    /// (not the target): mounted at red, transitioning to blue half-way → the
775    /// background rectangle is about half red, half blue.
776    #[test]
777    fn animated_color_paints_the_interpolated_color() {
778        use frus_core::{Primitive, Size};
779        let red = Color::rgb(1.0, 0.0, 0.0);
780        let blue = Color::rgb(0.0, 0.0, 1.0);
781        let mut rt = crate::runtime::Runtime::default();
782        let start: Container<()> =
783            Container::new()
784                .width(20.0)
785                .height(20.0)
786                .animated_color(red, 0.10, Curve::Linear);
787        rt.advance_colors(&start, 1.0); // montage → rouge
788        let to_blue: Container<()> =
789            Container::new()
790                .width(20.0)
791                .height(20.0)
792                .animated_color(blue, 0.10, Curve::Linear);
793        rt.advance_colors(&to_blue, 0.05); // t = 0.5
794
795        let theme = crate::Theme::dark();
796        let ui = crate::ui::build_ui(&to_blue, Size::new(20.0, 20.0), &rt, &theme);
797        let color = ui
798            .scene()
799            .primitives()
800            .iter()
801            .find_map(|p| match p {
802                Primitive::Rect { color, .. } => Some(*color),
803                _ => None,
804            })
805            .expect("un rectangle de fond");
806        assert!(
807            (color.r - 0.5).abs() < 0.1 && (color.b - 0.5).abs() < 0.1,
808            "the interpolated color is painted: {color:?}"
809        );
810    }
811
812    /// An `animated_size` **drives the layout**: half-way through (20×20 → 40×40,
813    /// linear), the background rectangle measures about 30×30 (the interpolated
814    /// size injected into layout through `effective_style`).
815    #[test]
816    fn animated_size_drives_the_layout() {
817        use frus_core::{Primitive, Size};
818        let mut rt = crate::runtime::Runtime::default();
819        let red = Color::rgb(1.0, 0.0, 0.0);
820        let start: Container<()> =
821            Container::new()
822                .color(red)
823                .animated_size(20.0, 20.0, 0.10, Curve::Linear);
824        rt.advance_sizes(&start, 1.0); // montage → 20×20
825        let to_big: Container<()> =
826            Container::new()
827                .color(red)
828                .animated_size(40.0, 40.0, 0.10, Curve::Linear);
829        rt.advance_sizes(&to_big, 0.05); // t = 0.5 → 30×30
830
831        let theme = crate::Theme::dark();
832        let ui = crate::ui::build_ui(&to_big, Size::new(100.0, 100.0), &rt, &theme);
833        let rect = ui
834            .scene()
835            .primitives()
836            .iter()
837            .find_map(|p| match p {
838                Primitive::Rect { rect, .. } => Some(*rect),
839                _ => None,
840            })
841            .expect("un rectangle de fond");
842        assert!(
843            (rect.width - 30.0).abs() < 1.0,
844            "interpolated width: {}",
845            rect.width
846        );
847        assert!(
848            (rect.height - 30.0).abs() < 1.0,
849            "interpolated height: {}",
850            rect.height
851        );
852    }
853
854    /// An `animated_radius` paints the **interpolated** radius: mounted at 0,
855    /// transitioning to 20 half-way → the background rectangle has a radius of ~10.
856    #[test]
857    fn animated_radius_paints_the_interpolated_radius() {
858        use frus_core::{Primitive, Size};
859        let red = Color::rgb(1.0, 0.0, 0.0);
860        let mut rt = crate::runtime::Runtime::default();
861        let sharp: Container<()> = Container::new()
862            .width(40.0)
863            .height(40.0)
864            .color(red)
865            .animated_radius(0.0, 0.10, Curve::Linear);
866        rt.advance_radii(&sharp, 1.0); // montage → 0
867        let round: Container<()> = Container::new()
868            .width(40.0)
869            .height(40.0)
870            .color(red)
871            .animated_radius(20.0, 0.10, Curve::Linear);
872        rt.advance_radii(&round, 0.05); // t = 0.5 → 10
873
874        let theme = crate::Theme::dark();
875        let ui = crate::ui::build_ui(&round, Size::new(60.0, 60.0), &rt, &theme);
876        let radius = ui
877            .scene()
878            .primitives()
879            .iter()
880            .find_map(|p| match p {
881                Primitive::Rect { radius, .. } => Some(*radius),
882                _ => None,
883            })
884            .expect("un rectangle de fond");
885        assert!(
886            (radius.top_left - 10.0).abs() < 1.0,
887            "interpolated radius: {}",
888            radius.top_left
889        );
890    }
891
892    /// An `animated_padding` **insets the child at layout time**: mounted at 0,
893    /// transitioning to 20 half-way → the child is inset by ~10 (the interpolated
894    /// padding).
895    #[test]
896    fn animated_padding_insets_the_child_at_layout() {
897        use frus_core::{Primitive, Size};
898        let red = Color::rgb(1.0, 0.0, 0.0);
899        let build = |pad: f32| {
900            Container::<()>::new()
901                .width(60.0)
902                .height(60.0)
903                .animated_padding(pad, 0.10, Curve::Linear)
904                .child(Container::new().width(20.0).height(20.0).color(red))
905        };
906        let mut rt = crate::runtime::Runtime::default();
907        rt.advance_paddings(&build(0.0), 1.0); // montage → 0
908        let bigger = build(20.0);
909        rt.advance_paddings(&bigger, 0.05); // t = 0.5 → 10
910
911        let theme = crate::Theme::dark();
912        let ui = crate::ui::build_ui(&bigger, Size::new(60.0, 60.0), &rt, &theme);
913        let rect = ui
914            .scene()
915            .primitives()
916            .iter()
917            .find_map(|p| match p {
918                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
919                _ => None,
920            })
921            .expect("the child's red background");
922        assert!(
923            (rect.x - 10.0).abs() < 1.0 && (rect.y - 10.0).abs() < 1.0,
924            "the child is inset by ~10 by the interpolated padding: {rect:?}"
925        );
926    }
927
928    /// `alignment(Center)` positions the (20×20) child at the centre of a 100×100 box
929    /// → its background sits at about (40, 40).
930    #[test]
931    fn alignment_centers_the_child() {
932        use frus_core::{Alignment, Primitive, Size};
933        let red = Color::rgb(1.0, 0.0, 0.0);
934        let root: Container<()> = Container::new()
935            .width(100.0)
936            .height(100.0)
937            .alignment(Alignment::CENTER)
938            .child(Container::new().width(20.0).height(20.0).color(red));
939        let rt = crate::runtime::Runtime::default();
940        let theme = crate::Theme::dark();
941        let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
942        let rect = ui
943            .scene()
944            .primitives()
945            .iter()
946            .find_map(|p| match p {
947                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
948                _ => None,
949            })
950            .expect("the child's red background");
951        assert!(
952            (rect.x - 40.0).abs() < 1.0 && (rect.y - 40.0).abs() < 1.0,
953            "centred: {rect:?}"
954        );
955    }
956
957    /// `alignment(BottomRight)` anchors the (20×20) child to the bottom-right corner
958    /// of a 100×100 box → its background sits at about (80, 80).
959    #[test]
960    fn alignment_anchors_child_to_a_corner() {
961        use frus_core::{Alignment, Primitive, Size};
962        let red = Color::rgb(1.0, 0.0, 0.0);
963        let root: Container<()> = Container::new()
964            .width(100.0)
965            .height(100.0)
966            .alignment(Alignment::BOTTOM_RIGHT)
967            .child(Container::new().width(20.0).height(20.0).color(red));
968        let rt = crate::runtime::Runtime::default();
969        let theme = crate::Theme::dark();
970        let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
971        let rect = ui
972            .scene()
973            .primitives()
974            .iter()
975            .find_map(|p| match p {
976                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
977                _ => None,
978            })
979            .expect("the child's red background");
980        assert!(
981            (rect.x - 80.0).abs() < 1.0 && (rect.y - 80.0).abs() < 1.0,
982            "bottom-right corner: {rect:?}"
983        );
984    }
985
986    /// A **fractional** anchor (outside the nine discrete positions) places the child
987    /// proportionally: `x = 0.5, y = -0.5` → fractions (0.75, 0.25) → in a 100×100
988    /// box with a 20×20 child (80×80 free), the background lands at about (60, 20).
989    /// This is what the discrete flex could not do — and what makes
990    /// `Tween<Alignment>` visually continuous.
991    #[test]
992    fn fractional_alignment_places_child_proportionally() {
993        use frus_core::{Alignment, Primitive, Size};
994        let red = Color::rgb(1.0, 0.0, 0.0);
995        let root: Container<()> = Container::new()
996            .width(100.0)
997            .height(100.0)
998            .alignment(Alignment::new(0.5, -0.5))
999            .child(Container::new().width(20.0).height(20.0).color(red));
1000        let rt = crate::runtime::Runtime::default();
1001        let theme = crate::Theme::dark();
1002        let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1003        let rect = ui
1004            .scene()
1005            .primitives()
1006            .iter()
1007            .find_map(|p| match p {
1008                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1009                _ => None,
1010            })
1011            .expect("the child's red background");
1012        assert!(
1013            (rect.x - 60.0).abs() < 1.0 && (rect.y - 20.0).abs() < 1.0,
1014            "fractionnel : {rect:?}"
1015        );
1016    }
1017
1018    /// A **directional** anchor follows the reading direction: `CENTER_START` places
1019    /// the child on the **left** in LTR, on the **right** in RTL (resolved at render
1020    /// time).
1021    #[test]
1022    fn directional_alignment_flips_the_child_in_rtl() {
1023        use frus_core::{AlignmentDirectional, Primitive, Size};
1024        let red = Color::rgb(1.0, 0.0, 0.0);
1025        let rt = crate::runtime::Runtime::default();
1026        let child_x = |theme: &crate::Theme| {
1027            let root: Container<()> = Container::new()
1028                .width(100.0)
1029                .height(100.0)
1030                .alignment(AlignmentDirectional::CENTER_START)
1031                .child(Container::new().width(20.0).height(20.0).color(red));
1032            let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, theme);
1033            ui.scene()
1034                .primitives()
1035                .iter()
1036                .find_map(|p| match p {
1037                    Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1038                    _ => None,
1039                })
1040                .expect("the child's red background")
1041        };
1042        assert!(
1043            child_x(&crate::Theme::dark()).abs() < 1.0,
1044            "start on the left in LTR"
1045        );
1046        assert!(
1047            (child_x(&crate::Theme::dark().rtl()) - 80.0).abs() < 1.0,
1048            "start on the right in RTL"
1049        );
1050    }
1051
1052    /// `decoration(...)` applies background, radius and border as one block: the
1053    /// background paints the given color and radius, and the border reserves its
1054    /// width at layout time.
1055    #[test]
1056    fn decoration_applies_composite_fields() {
1057        use frus_core::{BorderRadius, BoxDecoration, Primitive, Size};
1058        let green = Color::rgb(0.0, 1.0, 0.0);
1059        let deco = BoxDecoration {
1060            color: Some(green),
1061            radius: BorderRadius::uniform(8.0),
1062            border: Some(Border::new(2.0, Color::WHITE)),
1063            ..Default::default()
1064        };
1065        let root: Container<()> = Container::new().width(40.0).height(40.0).decoration(deco);
1066        // The border reserves its width in the layout padding.
1067        assert_eq!(Widget::style(&root).padding, Insets::uniform(2.0));
1068
1069        let rt = crate::runtime::Runtime::default();
1070        let theme = crate::Theme::dark();
1071        let ui = crate::ui::build_ui(&root, Size::new(40.0, 40.0), &rt, &theme);
1072        let (color, radius) = ui
1073            .scene()
1074            .primitives()
1075            .iter()
1076            .find_map(|p| match p {
1077                Primitive::Rect { color, radius, .. } if color.g > 0.5 => Some((*color, *radius)),
1078                _ => None,
1079            })
1080            .expect("the decorated green background");
1081        assert!(color.g > 0.9, "a green background: {color:?}");
1082        assert!(
1083            (radius.top_left - 8.0).abs() < 1e-3,
1084            "rayon composite : {}",
1085            radius.top_left
1086        );
1087    }
1088
1089    /// `margin(...)` reserves space **around** the box: it pushes the siblings away
1090    /// and offsets the background without growing it. In a column, a second child
1091    /// (20 tall) with a margin of 10 starts at `y = 20 (sibling) + 10 (margin)` and
1092    /// is inset by 10 on the left.
1093    #[test]
1094    fn margin_pushes_siblings_and_insets() {
1095        use frus_core::{Primitive, Size};
1096        let red = Color::rgb(1.0, 0.0, 0.0);
1097        let green = Color::rgb(0.0, 1.0, 0.0);
1098        let root = crate::Flex::<()>::column()
1099            .width(100.0)
1100            .child(Container::new().height(20.0).color(red))
1101            .child(Container::new().height(20.0).margin(10.0).color(green));
1102        let rt = crate::runtime::Runtime::default();
1103        let theme = crate::Theme::dark();
1104        let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
1105        let rect = ui
1106            .scene()
1107            .primitives()
1108            .iter()
1109            .find_map(|p| match p {
1110                Primitive::Rect { rect, color, .. } if color.g > 0.5 && color.r < 0.5 => {
1111                    Some(*rect)
1112                }
1113                _ => None,
1114            })
1115            .expect("the 2nd child's green background");
1116        assert!(
1117            (rect.y - 30.0).abs() < 0.5 && (rect.x - 10.0).abs() < 0.5,
1118            "margin: pushed to y=30, inset to x=10: {rect:?}"
1119        );
1120        assert!(
1121            (rect.height - 20.0).abs() < 0.5,
1122            "the margin does not grow the box: {rect:?}"
1123        );
1124    }
1125
1126    #[test]
1127    fn visible_border_reserves_layout_padding() {
1128        // A visible border: the layout padding reserves its width.
1129        let bordered: Container<()> = Container::new().padding(4.0).border(2.0, Color::WHITE);
1130        assert_eq!(Widget::style(&bordered).padding, Insets::uniform(6.0));
1131
1132        // With no border (or an invisible one): the padding is unchanged.
1133        let plain: Container<()> = Container::new().padding(4.0);
1134        assert_eq!(Widget::style(&plain).padding, Insets::uniform(4.0));
1135        let invisible: Container<()> = Container::new()
1136            .padding(4.0)
1137            .border(2.0, Color::TRANSPARENT);
1138        assert_eq!(Widget::style(&invisible).padding, Insets::uniform(4.0));
1139    }
1140}
1141
1142#[cfg(test)]
1143mod clip_tests {
1144    use super::*;
1145    use crate::{build_ui, Runtime, Size};
1146    use frus_core::Primitive;
1147
1148    fn layers(root: &Container<()>) -> Vec<frus_core::ClipShape> {
1149        let ui = build_ui(
1150            root,
1151            Size::new(100.0, 100.0),
1152            &Runtime::default(),
1153            &Theme::default(),
1154        );
1155        ui.scene()
1156            .primitives()
1157            .iter()
1158            .filter_map(|p| match p {
1159                Primitive::Layer { clip_shape, .. } => Some(clip_shape.clone()),
1160                _ => None,
1161            })
1162            .collect()
1163    }
1164
1165    /// Off by default, as in the reference: a decoration is painted behind a child and
1166    /// does not confine it.
1167    #[test]
1168    fn a_rounded_box_does_not_clip_unless_asked() {
1169        let plain = Container::<()>::new()
1170            .width(100.0)
1171            .height(100.0)
1172            .radius(12.0)
1173            .color(Color::rgb(1.0, 0.0, 0.0))
1174            .child(Container::<()>::new().width(100.0).height(100.0));
1175        assert!(layers(&plain).is_empty(), "no compositing layer");
1176    }
1177
1178    /// Asked, it wraps its child in a layer clipped to its own corners.
1179    #[test]
1180    fn clip_confines_the_child_to_the_corners() {
1181        let clipped = Container::<()>::new()
1182            .width(100.0)
1183            .height(100.0)
1184            .radius(12.0)
1185            .clip()
1186            .color(Color::rgb(1.0, 0.0, 0.0))
1187            .child(Container::<()>::new().width(100.0).height(100.0));
1188        let shapes = layers(&clipped);
1189        assert_eq!(shapes.len(), 1);
1190        match &shapes[0] {
1191            frus_core::ClipShape::RRect(r) => assert_eq!(r.top_left, 12.0),
1192            other => panic!("a rounded rectangle, not {other:?}"),
1193        }
1194    }
1195}
1196
1197#[cfg(test)]
1198mod foreground_tests {
1199    use super::*;
1200    use crate::{build_ui, Runtime, Size};
1201    use frus_core::{Primitive, Scene};
1202
1203    const BACK: Color = Color::rgb(1.0, 0.0, 0.0);
1204    const CHILD: Color = Color::rgb(0.0, 1.0, 0.0);
1205    const OVER: Color = Color::rgb(0.0, 0.0, 1.0);
1206
1207    fn scene(root: &Container<()>) -> Scene {
1208        build_ui(
1209            root,
1210            Size::new(100.0, 100.0),
1211            &Runtime::default(),
1212            &Theme::default(),
1213        )
1214        .scene()
1215        .clone()
1216    }
1217
1218    /// The order the three coloured boxes came out in, by their colour.
1219    fn order(root: &Container<()>) -> Vec<Color> {
1220        scene(root)
1221            .primitives()
1222            .iter()
1223            .filter_map(|p| match p {
1224                Primitive::Rect { color, .. } if *color == BACK || *color == CHILD => Some(*color),
1225                Primitive::Rect {
1226                    border_color,
1227                    border_width,
1228                    ..
1229                } if *border_width > 0.0 && *border_color == OVER => Some(OVER),
1230                _ => None,
1231            })
1232            .collect()
1233    }
1234
1235    fn card() -> Container<()> {
1236        Container::<()>::new()
1237            .width(100.0)
1238            .height(100.0)
1239            .color(BACK)
1240            .child(
1241                Container::<()>::new()
1242                    .width(100.0)
1243                    .height(100.0)
1244                    .color(CHILD),
1245            )
1246    }
1247
1248    /// Nothing is painted over a child unless a foreground asks for it.
1249    #[test]
1250    fn nothing_paints_over_the_child_by_default() {
1251        assert_eq!(order(&card()), vec![BACK, CHILD]);
1252    }
1253
1254    /// The whole point: after the child, not before it. A background could not say this.
1255    #[test]
1256    fn a_foreground_paints_after_the_child() {
1257        let card = card().foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1258        assert_eq!(order(&card), vec![BACK, CHILD, OVER]);
1259    }
1260
1261    /// It uses the container's own box, the same one the background does.
1262    #[test]
1263    fn it_covers_the_container_s_own_box() {
1264        let card = card()
1265            .padding(10.0)
1266            .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1267        let over = scene(&card)
1268            .primitives()
1269            .iter()
1270            .find_map(|p| match p {
1271                Primitive::Rect {
1272                    border_color,
1273                    border_width,
1274                    rect,
1275                    ..
1276                } if *border_width > 0.0 && *border_color == OVER => Some(*rect),
1277                _ => None,
1278            })
1279            .expect("the foreground is painted");
1280        assert_eq!((over.width, over.height), (100.0, 100.0));
1281    }
1282
1283    /// A foreground that names no radius takes the container's: a square outline over a
1284    /// rounded box is a mistake far more often than a request.
1285    #[test]
1286    fn a_silent_foreground_wears_the_container_s_corners() {
1287        let card = card()
1288            .radius(12.0)
1289            .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1290        assert_eq!(
1291            Widget::<()>::foreground(&card, &Theme::default())
1292                .unwrap()
1293                .radius,
1294            BorderRadius::uniform(12.0),
1295            "resolved against the container on the way out"
1296        );
1297    }
1298
1299    /// Said explicitly, the foreground's own radius stands.
1300    #[test]
1301    fn an_explicit_radius_is_left_alone() {
1302        let card = card().radius(12.0).foreground(
1303            BoxDecoration::default()
1304                .border(Border::new(2.0, OVER))
1305                .radius(4.0),
1306        );
1307        assert_eq!(
1308            Widget::<()>::foreground(&card, &Theme::default())
1309                .unwrap()
1310                .radius,
1311            BorderRadius::uniform(4.0)
1312        );
1313    }
1314}