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, BoxBorder, BoxDecoration, BoxShadow, BoxShape, Color,
6    Curve, DecorationImage, Gradient, Insets, InsetsGeometry, 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: InsetsGeometry,
27    /// The **outer** margin (around the box, outside the decoration).
28    margin: InsetsGeometry,
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    /// The border, side by side (milestone 641).
36    border: Option<BoxBorder>,
37    color: Option<Color>,
38    hover_color: Option<Color>,
39    pressed_color: Option<Color>,
40    /// A gradient from the colour towards an end colour, along a direction: kept apart,
41    /// because its first colour is the container's colour, which may animate.
42    towards: Option<(Color, [f32; 2])>,
43    /// A gradient said in full (milestone 644).
44    gradient: Option<Gradient>,
45    /// The shadows, painted in order (milestone 641).
46    shadows: Vec<BoxShadow>,
47    /// A rectangle or a circle (milestone 641).
48    shape: BoxShape,
49    /// A picture over the background, under the border (milestone 645).
50    image: Option<DecorationImage>,
51    /// How the background meets what is under the box (milestone 646).
52    background_blend_mode: Option<frus_core::BlendMode>,
53    on_click: Option<Msg>,
54    on_long_press: Option<Msg>,
55    /// A repaint boundary: it caches the painted subtree (see
56    /// [`crate::Widget::repaint_boundary`]).
57    repaint_boundary: bool,
58    /// A **group** opacity `[0,1]` applied to the whole subtree. `None` = opaque.
59    opacity: Option<f32>,
60    /// When the group opacity is **animated**: the transition's `(duration, curve)`.
61    /// `None` = a fixed opacity, with no transition.
62    opacity_anim: Option<(f32, Curve)>,
63    /// When the background is an **animated color**: the transition's `(target,
64    /// duration, curve)`. `None` = a fixed color.
65    color_anim: Option<(Color, f32, Curve)>,
66    /// When the **size** is animated: `(target, duration, curve)` — interpolated at
67    /// layout time. `None` = a fixed size.
68    size_anim: Option<(Size, f32, Curve)>,
69    /// When the **corner radius** is animated: `(target, duration, curve)` —
70    /// interpolated at paint time. `None` = a fixed radius.
71    radius_anim: Option<(BorderRadius, f32, Curve)>,
72    /// When the **padding** is animated: `(duration, curve)` — the target is
73    /// `self.padding`, interpolated at layout time. `None` = fixed padding.
74    padding_anim: Option<(f32, Curve)>,
75    /// `(duration, curve)` of an **animated anchor**; `None` = the anchor is fixed.
76    alignment_anim: Option<(f32, Curve)>,
77    /// The child's anchoring within the box, physical or directional (resolved for
78    /// RTL at render time). `None` = the default flex behaviour, in which the child
79    /// stretches to fill.
80    alignment: Option<AlignmentGeometry>,
81    /// Whether it sizes itself by the reference's rules (`true`), or always by its content —
82    /// what a widget built on a container that answers the question itself asks for
83    /// ([`Container::hugging`]).
84    reference_sizing: bool,
85    /// The reference's `constraints`: `(min_width, max_width, min_height, max_height)`,
86    /// each unset where nothing was said.
87    limits: [Option<f32>; 4],
88    /// The reference's `transform`, as this framework's three: a shift, a scale and a turn.
89    shift: Option<(f32, f32)>,
90    scale: Option<(f32, f32)>,
91    turn: Option<f32>,
92    /// The point the scale and the turn are about — the reference's `transformAlignment`.
93    pivot: frus_core::Alignment,
94    children: Vec<Box<dyn Widget<Msg>>>,
95}
96
97impl<Msg> Container<Msg> {
98    /// The axes the reference's container takes the room on: every axis it was given no
99    /// size on, when it has no child or aligns its child.
100    fn reference_fill(&self) -> crate::widget::FillAxes {
101        let fills = self.reference_sizing && (self.children.is_empty() || self.alignment.is_some());
102        crate::widget::FillAxes {
103            horizontal: fills && matches!(self.width, Dimension::Auto),
104            vertical: fills && matches!(self.height, Dimension::Auto),
105        }
106    }
107
108    /// A container sized by its content whatever the reference's rules say: for a widget
109    /// built on one that decides for itself how much room to take, as [`crate::Aligned`]
110    /// does with its factors.
111    pub(crate) fn hugging(mut self) -> Self {
112        self.reference_sizing = false;
113        self
114    }
115
116    /// **At least `width` wide** — part of the reference's `constraints`. A width given
117    /// with [`width`](Self::width) is held within the constraints, as the reference
118    /// tightens them.
119    #[must_use]
120    pub fn min_width(mut self, width: f32) -> Self {
121        self.limits[0] = Some(width);
122        self
123    }
124
125    /// **At most `width` wide.**
126    #[must_use]
127    pub fn max_width(mut self, width: f32) -> Self {
128        self.limits[1] = Some(width);
129        self
130    }
131
132    /// **At least `height` tall.**
133    #[must_use]
134    pub fn min_height(mut self, height: f32) -> Self {
135        self.limits[2] = Some(height);
136        self
137    }
138
139    /// **At most `height` tall.**
140    #[must_use]
141    pub fn max_height(mut self, height: f32) -> Self {
142        self.limits[3] = Some(height);
143        self
144    }
145
146    /// **Shifted by `(dx, dy)`** when painted, as the reference's `transform` does: the
147    /// layout keeps its place and the box and its child are drawn moved.
148    #[must_use]
149    pub fn translated(mut self, dx: f32, dy: f32) -> Self {
150        self.shift = Some((dx, dy));
151        self
152    }
153
154    /// **Scaled by `(sx, sy)`** when painted, about the
155    /// [`transform_alignment`](Self::transform_alignment).
156    #[must_use]
157    pub fn scaled(mut self, sx: f32, sy: f32) -> Self {
158        self.scale = Some((sx, sy));
159        self
160    }
161
162    /// **Turned by `radians`** when painted, about the
163    /// [`transform_alignment`](Self::transform_alignment).
164    #[must_use]
165    pub fn rotated(mut self, radians: f32) -> Self {
166        self.turn = Some(radians);
167        self
168    }
169
170    /// **The point a scale and a turn are about**, within the box. Unset, its centre.
171    #[must_use]
172    pub fn transform_alignment(mut self, pivot: frus_core::Alignment) -> Self {
173        self.pivot = pivot;
174        self
175    }
176
177    /// Creates an empty container (automatic size, no decoration).
178    pub fn new() -> Self {
179        Self {
180            width: Dimension::Auto,
181            height: Dimension::Auto,
182            flex_grow: 0.0,
183            flex_shrink: 0.0,
184            padding: InsetsGeometry::Physical(Insets::ZERO),
185            margin: InsetsGeometry::Physical(Insets::ZERO),
186            radius: BorderRadius::ZERO,
187            clip: false,
188            foreground: None,
189            border: None,
190            color: None,
191            hover_color: None,
192            pressed_color: None,
193            towards: None,
194            gradient: None,
195            shadows: Vec::new(),
196            shape: BoxShape::Rectangle,
197            image: None,
198            background_blend_mode: None,
199            on_click: None,
200            on_long_press: None,
201            repaint_boundary: false,
202            opacity: None,
203            opacity_anim: None,
204            color_anim: None,
205            size_anim: None,
206            radius_anim: None,
207            padding_anim: None,
208            alignment_anim: None,
209            alignment: None,
210            reference_sizing: true,
211            limits: [None; 4],
212            shift: None,
213            scale: None,
214            turn: None,
215            pivot: frus_core::Alignment::CENTER,
216            children: Vec::new(),
217        }
218    }
219
220    /// The **effective** layout padding: the inner padding plus, when a border is
221    /// visible, the room it reserves (the content of a bordered box is not eaten by
222    /// the stroke). The single source for `style()` and for the **target** of an
223    /// animated padding (`anim_padding`).
224    fn effective_padding(&self) -> Insets {
225        self.effective_padding_in(frus_core::TextDirection::Ltr)
226    }
227
228    /// The padding the layout is given — the content's and the border's — for a frame in
229    /// `direction` (milestone 588: see [`InsetsGeometry::laid_out`]).
230    fn effective_padding_in(&self, direction: frus_core::TextDirection) -> Insets {
231        let mut padding = self.padding.laid_out(direction);
232        // Each side's own width, in the reading direction (milestone 641).
233        if let Some(border) = self.border {
234            let w = border.resolve(direction).dimensions();
235            padding.top += w.top;
236            padding.right += w.right;
237            padding.bottom += w.bottom;
238            padding.left += w.left;
239        }
240        padding
241    }
242
243    /// Marks this container as a **repaint boundary**: its subtree is cached and
244    /// reused for as long as its geometry and its descendants' interaction state
245    /// stay stable. To be placed around **static** content that would otherwise be
246    /// repainted on every frame of a neighbouring animation.
247    pub fn repaint_boundary(mut self) -> Self {
248        self.repaint_boundary = true;
249        self
250    }
251
252    /// Sets the width, in logical pixels.
253    pub fn width(mut self, width: f32) -> Self {
254        self.width = Dimension::Length(width);
255        self
256    }
257
258    /// Sets the height, in logical pixels.
259    pub fn height(mut self, height: f32) -> Self {
260        self.height = Dimension::Length(height);
261        self
262    }
263
264    /// Sets the width as a **fraction of the space the parent offers** — `1.0` fills it.
265    pub fn width_fraction(mut self, fraction: f32) -> Self {
266        self.width = Dimension::Percent(fraction);
267        self
268    }
269
270    /// Flex growth factor along the parent's main axis.
271    pub fn flex(mut self, grow: f32) -> Self {
272        self.flex_grow = grow;
273        self
274    }
275
276    /// How much of a row's deficit this box absorbs. The default is `0.0` — the
277    /// reference's rule, where an inflexible child is never squeezed and a row that does
278    /// not fit overflows and says so.
279    ///
280    /// `shrink(1.0)` asks for flexbox's behaviour instead: give way rather than let the
281    /// row run over. It is the right answer for a box whose size is a preference rather
282    /// than a requirement, and the wrong one for fixed chrome — an icon button at the end
283    /// of a row should keep its width however long the label beside it grows.
284    pub fn shrink(mut self, shrink: f32) -> Self {
285        self.flex_shrink = shrink;
286        self
287    }
288
289    /// This box never shrinks — the default said out loud, kept because a layout that
290    /// depends on it reads better for saying so. See [`Self::shrink`].
291    pub fn no_shrink(self) -> Self {
292        self.shrink(0.0)
293    }
294
295    /// Uniform inner padding, in logical pixels.
296    pub fn padding(mut self, padding: f32) -> Self {
297        self.padding = InsetsGeometry::Physical(Insets::uniform(padding));
298        self
299    }
300
301    /// Inner padding per side (top, right, bottom, left).
302    pub fn padding_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
303        self.padding = InsetsGeometry::Physical(Insets::new(top, right, bottom, left));
304        self
305    }
306
307    /// Sets the padding from any insets: one number, physical [`Insets`] — whose left stays
308    /// on the left in a right-to-left script — or [`InsetsDirectional`](frus_core::InsetsDirectional),
309    /// whose start is the right there.
310    pub fn padding_insets(mut self, padding: impl Into<InsetsGeometry>) -> Self {
311        self.padding = padding.into();
312        self
313    }
314
315    /// Sets the margin from any insets, as [`padding_insets`](Self::padding_insets) does the
316    /// padding.
317    pub fn margin_insets(mut self, margin: impl Into<InsetsGeometry>) -> Self {
318        self.margin = margin.into();
319        self
320    }
321
322    /// A uniform **outer** margin: space reserved **around** the box — outside the
323    /// decoration, it pushes the siblings away without growing the background or
324    /// the border.
325    pub fn margin(mut self, margin: f32) -> Self {
326        self.margin = InsetsGeometry::Physical(Insets::uniform(margin));
327        self
328    }
329
330    /// Outer margin per side (top, right, bottom, left).
331    pub fn margin_each(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
332        self.margin = InsetsGeometry::Physical(Insets::new(top, right, bottom, left));
333        self
334    }
335
336    /// The rounded corner radii: uniform via `f32` (`.radius(10.0)`) or per corner
337    /// via [`BorderRadius`] (`.radius(BorderRadius::top(12.0))`).
338    pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
339        self.radius = radius.into();
340        self
341    }
342
343    /// **Clips** the child to this box, corners included.
344    ///
345    /// Off by default, as in the reference: a decoration is painted *behind* a child
346    /// and does not confine it, so a box with rounded corners leaves an image inside it
347    /// square unless it is asked. Asking costs a compositing layer, which is why neither
348    /// framework does it for you.
349    ///
350    /// [`crate::ClipRRect`] is the same thing without the decoration, and is what to
351    /// reach for when there is no box to decorate.
352    pub fn clip(mut self) -> Self {
353        self.clip = true;
354        self
355    }
356
357    /// The border: width (in px) and color, the same on all four sides.
358    pub fn border(mut self, width: f32, color: Color) -> Self {
359        self.border = (width > 0.0).then(|| Border::new(width, color).into());
360        self
361    }
362
363    /// **The border, side by side** — a [`Border`] or a
364    /// [`BorderDirectional`](frus_core::BorderDirectional), each side its own width and colour
365    /// (milestone 641).
366    pub fn box_border(mut self, border: impl Into<BoxBorder>) -> Self {
367        self.border = Some(border.into());
368        self
369    }
370
371    /// **A rectangle or a circle** (milestone 641). A circle is centred in the box, as wide
372    /// as its shorter side, and its radius is not read.
373    pub fn shape(mut self, shape: BoxShape) -> Self {
374        self.shape = shape;
375        self
376    }
377
378    /// **The shadows**, painted in order behind the box (milestone 641).
379    pub fn shadows(mut self, shadows: impl IntoIterator<Item = BoxShadow>) -> Self {
380        self.shadows = shadows.into_iter().collect();
381        self
382    }
383
384    /// The background color at rest.
385    pub fn color(mut self, color: Color) -> Self {
386        self.color = Some(color);
387        self
388    }
389
390    /// The background color on hover.
391    pub fn hover_color(mut self, color: Color) -> Self {
392        self.hover_color = Some(color);
393        self
394    }
395
396    /// The background color when pressed.
397    pub fn pressed_color(mut self, color: Color) -> Self {
398        self.pressed_color = Some(color);
399        self
400    }
401
402    /// A linear background gradient (`color` → `end`), with `dir` in `[0,1]²` space
403    /// (`[0.0, 1.0]` = top→bottom, for instance).
404    pub fn gradient(mut self, end: Color, dir: [f32; 2]) -> Self {
405        self.towards = Some((end, dir));
406        self
407    }
408
409    /// A picture over the background, under the border, clipped to the corners or the
410    /// circle — fitted, aligned, repeated or sliced as the [`DecorationImage`] says.
411    pub fn image(mut self, image: DecorationImage) -> Self {
412        self.image = Some(image);
413        self
414    }
415
416    /// Blends the background — the colour or the gradient — with what is under the box
417    /// by `mode` rather than painting it over: a multiply that darkens the picture
418    /// behind, a colour that tints it, a difference that inverts it.
419    pub fn background_blend_mode(mut self, mode: frus_core::BlendMode) -> Self {
420        self.background_blend_mode = Some(mode);
421        self
422    }
423
424    /// A drop shadow: the `(dx, dy)` offset, the blur radius and the color. Each call adds
425    /// one, over those already there.
426    pub fn shadow(mut self, dx: f32, dy: f32, blur: f32, color: Color) -> Self {
427        self.shadows.push(BoxShadow::new(dx, dy, blur, color));
428        self
429    }
430
431    /// The message emitted when the container is clicked.
432    pub fn on_click(mut self, message: impl Into<Msg>) -> Self {
433        self.on_click = Some(message.into());
434        self
435    }
436
437    /// The message emitted by a **long press** (a press held without movement). A
438    /// long press supersedes the click.
439    pub fn on_long_press(mut self, message: impl Into<Msg>) -> Self {
440        self.on_long_press = Some(message.into());
441        self
442    }
443
444    /// Sets the container's child.
445    pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
446        self.children.clear();
447        self.children.push(Box::new(child));
448        self
449    }
450
451    /// Applies a **group opacity** `[0,1]` to the whole subtree, as one block: the
452    /// rendering goes through a composited layer, so overlaps are not
453    /// double-blended. `1.0` = no effect at all.
454    pub fn opacity(mut self, opacity: f32) -> Self {
455        self.opacity = Some(opacity.clamp(0.0, 1.0));
456        self
457    }
458
459    /// Like [`Container::opacity`], but the opacity **animates** toward `opacity` on
460    /// every change, with `duration` (in seconds) and `curve`. The fade applies to
461    /// the whole group.
462    pub fn animated_opacity(mut self, opacity: f32, duration: f32, curve: Curve) -> Self {
463        self.opacity = Some(opacity.clamp(0.0, 1.0));
464        self.opacity_anim = Some((duration, curve));
465        self
466    }
467
468    /// A background whose color **animates** toward `color` on every change, with
469    /// `duration` (in seconds) and `curve`. The runtime interpolates from the
470    /// current color; on mount it adopts `color` with no transition. Note: one box
471    /// shares a single `(duration, curve)` across its animations (opacity, color).
472    pub fn animated_color(mut self, color: Color, duration: f32, curve: Curve) -> Self {
473        self.color = Some(color);
474        self.color_anim = Some((color, duration, curve));
475        self
476    }
477
478    /// A box whose **size** animates toward `width×height` on every change, with
479    /// `duration` and `curve`. The interpolated size is injected **at layout time**
480    /// (the children reposition accordingly). On mount it adopts the target with no
481    /// transition.
482    pub fn animated_size(mut self, width: f32, height: f32, duration: f32, curve: Curve) -> Self {
483        self.width = Dimension::Length(width);
484        self.height = Dimension::Length(height);
485        self.size_anim = Some((Size::new(width, height), duration, curve));
486        self
487    }
488
489    /// A box whose **corner radius** animates toward `radius` on every change, with
490    /// `duration` and `curve` — the corners morph smoothly. Uniform via `f32`, or
491    /// per corner via [`BorderRadius`]. On mount it adopts the target with no
492    /// transition.
493    pub fn animated_radius(
494        mut self,
495        radius: impl Into<BorderRadius>,
496        duration: f32,
497        curve: Curve,
498    ) -> Self {
499        let radius = radius.into();
500        self.radius = radius;
501        self.radius_anim = Some((radius, duration, curve));
502        self
503    }
504
505    /// A box whose (uniform) **inner padding** animates toward `padding` on every
506    /// change, with `duration` and `curve`. The interpolated padding is injected
507    /// **at layout time** (the content repositions). On mount it adopts the target
508    /// with no transition.
509    pub fn animated_padding(mut self, padding: f32, duration: f32, curve: Curve) -> Self {
510        self.padding = InsetsGeometry::Physical(Insets::uniform(padding));
511        self.padding_anim = Some((duration, curve));
512        self
513    }
514
515    /// A box whose **anchor** animates toward `alignment` on every change, with
516    /// `duration` and `curve`: the child slides through the free space instead of
517    /// jumping across it. On mount it adopts the anchor with no transition.
518    ///
519    /// It interpolates the anchor's own two fractions, so a **directional** anchor stays
520    /// directional the whole way and mirrors as a whole in a right-to-left script.
521    pub fn animated_alignment(
522        mut self,
523        alignment: impl Into<AlignmentGeometry>,
524        duration: f32,
525        curve: Curve,
526    ) -> Self {
527        self.alignment = Some(alignment.into());
528        self.alignment_anim = Some((duration, curve));
529        self
530    }
531
532    /// **Anchors the child** within the box: centred, in a corner, against an edge…
533    /// Accepts a **physical** anchor ([`Alignment`](frus_core::Alignment)) or a
534    /// **directional** one
535    /// ([`AlignmentDirectional`](frus_core::AlignmentDirectional), resolved for RTL
536    /// at render time) — both through `Into`. By default, with no anchor, the child
537    /// stretches to fill the container (the flex behaviour); setting an anchor
538    /// leaves the child at its natural size and positions it.
539    pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
540        self.alignment = Some(alignment.into());
541        self
542    }
543
544    /// A decoration painted **over the child** rather than behind it — the reference's
545    /// `foregroundDecoration`.
546    ///
547    /// [`Container::decoration`] and every part it is made of go *behind* the content,
548    /// which is what a background is for and no use at all when the point is that the
549    /// content does not cover it: an outline over a photograph, a wash across a tile
550    /// that is out of stock, a sheen over a card. There is no other way to say it short
551    /// of a [`crate::Stack`].
552    ///
553    /// It is painted on the container's own box, the same one the background uses, and
554    /// it is inside anything the container asks for — an opacity fades it with
555    /// everything else, [`Container::clip`] holds it to the corners.
556    ///
557    /// One convenience the reference does not have: a foreground that names **no**
558    /// radius takes the container's, since a square outline over a rounded box is
559    /// almost always a mistake rather than a request. Set one explicitly — including
560    /// [`frus_core::BorderRadius::ZERO`] — to say otherwise.
561    ///
562    /// ```
563    /// # use frus_widgets::Container;
564    /// # use frus_core::{Border, BoxDecoration, Color};
565    /// # let photo = Container::<()>::new();
566    /// Container::<()>::new()
567    ///     .radius(12.0)
568    ///     .child(photo)
569    ///     .foreground(BoxDecoration::default().border(Border::new(1.0, Color::rgba(0.0, 0.0, 0.0, 0.2))));
570    /// ```
571    pub fn foreground(mut self, decoration: BoxDecoration) -> Self {
572        self.foreground = Some(decoration);
573        self
574    }
575
576    /// Sets the box's fill, gradient, picture, blend mode, border, shadows and shape from a
577    /// [`BoxDecoration`](frus_core::BoxDecoration) at once. What the decoration leaves unset
578    /// changes nothing; the radius and the shape are always adopted.
579    pub fn decoration(mut self, decoration: BoxDecoration) -> Self {
580        if let Some(color) = decoration.color {
581            self.color = Some(color);
582        }
583        if let Some(gradient) = decoration.gradient {
584            self.gradient = Some(gradient);
585        }
586        if let Some(image) = decoration.image {
587            self.image = Some(image);
588        }
589        if let Some(mode) = decoration.background_blend_mode {
590            self.background_blend_mode = Some(mode);
591        }
592        if let Some(border) = decoration.border {
593            self.border = Some(border);
594        }
595        self.radius = decoration.radius;
596        if !decoration.shadows.is_empty() {
597            self.shadows = decoration.shadows;
598        }
599        self.shape = decoration.shape;
600        self
601    }
602}
603
604impl<Msg> Default for Container<Msg> {
605    fn default() -> Self {
606        Self::new()
607    }
608}
609
610impl<Msg> Container<Msg> {
611    /// The style, with the padding and the margin laid out for `direction`.
612    fn style_in(&self, direction: frus_core::TextDirection) -> Style {
613        let mut style = Style {
614            width: self.width,
615            height: self.height,
616            flex_grow: self.flex_grow,
617            flex_shrink: self.flex_shrink,
618            padding: self.effective_padding_in(direction),
619            margin: self.margin.laid_out(direction),
620            ..Default::default()
621        };
622        let limit = |v: Option<f32>| v.map_or(Dimension::Auto, Dimension::Length);
623        style.min_width = limit(self.limits[0]);
624        style.max_width = limit(self.limits[1]);
625        style.min_height = limit(self.limits[2]);
626        style.max_height = limit(self.limits[3]);
627        // Anchoring the child: taffy is left to place it at the **top left** of the
628        // content box, at its natural size (Start / Start, no stretching), and the
629        // walk then offsets it within the free space according to the `Alignment`'s
630        // fractions (a manual, fractional placement — outside the discrete flex).
631        if self.alignment.is_some() {
632            style.justify = Justify::Start;
633            style.align = Align::Start;
634        }
635        style
636    }
637}
638
639impl<Msg: Clone> Widget<Msg> for Container<Msg> {
640    fn style(&self) -> Style {
641        self.style_in(frus_core::TextDirection::Ltr)
642    }
643
644    /// The padding and the margin laid out for the reading direction the subtree is in
645    /// (milestone 588).
646    fn style_themed(&self, theme: &Theme) -> Style {
647        self.style_in(theme.direction)
648    }
649
650    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
651        &self.children
652    }
653
654    fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
655        // A background with an **animated color**: the color the runtime interpolates
656        // wins (the hover/press interpolation does not apply to an animated background).
657        // Otherwise two progressions in a row — rest → hover, then that → held. The press
658        // used to be the one step here that was instant; it is a progression since
659        // milestone 441, and crossing over from wherever the hover had got to is what
660        // keeps a pointer that presses mid-fade from jumping backwards first.
661        let color = if self.color_anim.is_some() {
662            status.anim_color.or(self.color)
663        } else {
664            let settled = match (self.color, self.hover_color) {
665                (Some(base), Some(hover)) => Some(base.lerp(hover, ease(status.hover_progress))),
666                _ => self.color,
667            };
668            let press = ease(status.press_progress.clamp(0.0, 1.0));
669            match (settled, self.pressed_color.or(self.hover_color)) {
670                (Some(from), Some(held)) => Some(from.lerp(held, press)),
671                // Nothing to cross over *from*: a container with a held colour and no
672                // resting one at all has no fill to fade, so it appears with the press.
673                (None, held) if press > 0.0 => held,
674                (settled, _) => settled,
675            }
676        };
677
678        // Compose the decoration (background/gradient/border/shadow) and lower it into
679        // primitives in the fixed order shadow → background → border. The opacity
680        // (the fade-in) modulates every color.
681        // An animated radius: the radius the runtime interpolates wins over the fixed one.
682        let radius = if self.radius_anim.is_some() {
683            status.anim_radius.unwrap_or(self.radius)
684        } else {
685            self.radius
686        };
687
688        // A gradient towards an end colour starts at this frame's colour, along its
689        // direction: the reference's `begin` and `end`, opposite each other about the centre.
690        let gradient = self.gradient.clone().or_else(|| {
691            let (end, dir) = self.towards?;
692            let start = color?;
693            Some(Gradient::from(
694                LinearGradient::new(vec![start, end]).between(
695                    frus_core::Alignment::new(-dir[0], -dir[1]),
696                    frus_core::Alignment::new(dir[0], dir[1]),
697                ),
698            ))
699        });
700        let decoration = BoxDecoration {
701            color,
702            gradient,
703            border: self.border,
704            radius,
705            shadows: self.shadows.clone(),
706            shape: self.shape,
707            image: self.image.clone(),
708            background_blend_mode: self.background_blend_mode,
709        };
710        decoration.paint_into_in(scene, bounds, status.opacity, theme.direction);
711    }
712
713    fn clip_shape(&self) -> Option<frus_core::ClipShape> {
714        self.clip.then(|| match self.shape {
715            BoxShape::Rectangle => frus_core::ClipShape::RRect(self.radius.clamped()),
716            // The ellipse inscribed in the box: the circle, in a square one.
717            BoxShape::Circle => frus_core::ClipShape::Oval,
718        })
719    }
720
721    fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
722        self.foreground.clone().map(|decoration| {
723            if decoration.radius == frus_core::BorderRadius::ZERO {
724                decoration.radius(self.radius)
725            } else {
726                decoration
727            }
728        })
729    }
730
731    fn on_click(&self) -> Option<Msg> {
732        self.on_click.clone()
733    }
734
735    fn on_long_press(&self) -> Option<Msg> {
736        self.on_long_press.clone()
737    }
738
739    fn repaint_boundary(&self) -> bool {
740        self.repaint_boundary
741    }
742
743    fn opacity_group(&self) -> Option<f32> {
744        self.opacity
745    }
746
747    /// The animated opacity's target (only when `animated_opacity` is set) — this is
748    /// the value the runtime tweens and the walk reads back for the layer.
749    fn anim_target(&self) -> Option<f32> {
750        self.opacity_anim.as_ref().and(self.opacity)
751    }
752
753    fn anim_color(&self) -> Option<Color> {
754        self.color_anim.as_ref().and(self.color)
755    }
756
757    fn anim_size(&self) -> Option<Size> {
758        self.size_anim.as_ref().map(|(s, _, _)| *s)
759    }
760
761    fn anim_radius(&self) -> Option<BorderRadius> {
762        self.radius_anim.as_ref().map(|(r, _, _)| *r)
763    }
764
765    fn anim_padding(&self) -> Option<Insets> {
766        // The target = the effective padding (content + border), consistent with `style()`.
767        self.padding_anim.as_ref().map(|_| self.effective_padding())
768    }
769
770    fn alignment_geometry(&self) -> Option<AlignmentGeometry> {
771        self.alignment
772    }
773
774    fn transform_translate(&self) -> Option<(f32, f32)> {
775        self.shift
776    }
777
778    fn transform_scale(&self) -> Option<(f32, f32, frus_core::Alignment)> {
779        self.scale.map(|(sx, sy)| (sx, sy, self.pivot))
780    }
781
782    fn transform_rotate(&self) -> Option<(f32, frus_core::Alignment)> {
783        self.turn.map(|r| (r, self.pivot))
784    }
785
786    /// **The room on offer**, on each axis it was given no size on, when it has no child
787    /// or when it aligns its child — the reference's two rules (`container.dart:390`,
788    /// `:396`). Empty, a container expands to what its parent allows; aligning, it fills
789    /// its parent and places its child inside. Along a row or a column shared with other
790    /// children the request is not granted, so it is nothing there, as the reference's
791    /// `LimitedBox(0)` is in unbounded room. Otherwise it is its child's size.
792    fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
793        self.reference_fill()
794    }
795
796    fn anim_offset(&self) -> Option<(f32, f32)> {
797        // The target = the anchor's own fractions, which is what the walk puts back into
798        // an anchor of the same kind.
799        self.alignment_anim
800            .as_ref()
801            .and(self.alignment)
802            .map(|a| a.fractions())
803    }
804
805    fn anim_duration(&self) -> f32 {
806        // One box's animations (opacity/color/size/radius/padding) share a single
807        // duration (in the order: opacity, color, size, radius, padding).
808        self.opacity_anim
809            .as_ref()
810            .map(|(d, _)| *d)
811            .or(self.color_anim.as_ref().map(|(_, d, _)| *d))
812            .or(self.size_anim.as_ref().map(|(_, d, _)| *d))
813            .or(self.radius_anim.as_ref().map(|(_, d, _)| *d))
814            .or(self.padding_anim.as_ref().map(|(d, _)| *d))
815            .or(self.alignment_anim.as_ref().map(|(d, _)| *d))
816            .unwrap_or(crate::runtime::ANIM_DURATION)
817    }
818
819    fn anim_curve(&self) -> Curve {
820        self.opacity_anim
821            .as_ref()
822            .map(|(_, c)| c.clone())
823            .or(self.color_anim.as_ref().map(|(_, _, c)| c.clone()))
824            .or(self.size_anim.as_ref().map(|(_, _, c)| c.clone()))
825            .or(self.radius_anim.as_ref().map(|(_, _, c)| c.clone()))
826            .or(self.padding_anim.as_ref().map(|(_, c)| c.clone()))
827            .or(self.alignment_anim.as_ref().map(|(_, c)| c.clone()))
828            .unwrap_or(Curve::Linear)
829    }
830}
831
832#[cfg(test)]
833mod tests {
834    use super::*;
835
836    /// A box in an over-budget row **keeps its width** — the reference's rule, and the
837    /// default since milestone 349. Flexbox's default was the opposite, and the smallest
838    /// fixed thing in the row paid for the whole deficit: milestone 333 found an icon
839    /// button at 13 px of 40, drawn off the card and out of the hit registry.
840    ///
841    /// `shrink(1.0)` asks for flexbox's behaviour back, for a box that would rather give
842    /// way than let the row overflow.
843    #[test]
844    fn a_box_in_an_over_budget_row_keeps_its_width() {
845        use crate::interaction::WidgetId;
846        use crate::runtime::Runtime;
847        use crate::theme::Theme;
848        use crate::Flex;
849
850        let widths = |row: &Flex<()>| -> Vec<f32> {
851            let runtime = Runtime::default();
852            let theme = Theme::light();
853            let mut layout = frus_layout::Layout::new();
854            let node = crate::ui::build_layout(row, WidgetId::ROOT, &runtime, &theme, &mut layout);
855            layout.compute_filled(node, 100.0, 50.0);
856            layout
857                .absolute_rects(node)
858                .iter()
859                .skip(1)
860                .take(2)
861                .map(|(r, _)| r.width)
862                .collect()
863        };
864
865        // 120 px of children in 100 px of row. Nobody was asked to give way, so nobody
866        // does, and the row overflows — which is now visible, striped and labelled.
867        let row = Flex::row()
868            .child(Container::<()>::new().width(80.0).height(20.0))
869            .child(Container::<()>::new().width(40.0).height(20.0));
870        assert_eq!(
871            widths(&row),
872            vec![80.0, 40.0],
873            "an inflexible child is never squeezed"
874        );
875
876        // Unless it says so, and then the whole deficit is its own.
877        let giving_way = Flex::row()
878            .child(Container::<()>::new().width(80.0).height(20.0).shrink(1.0))
879            .child(Container::<()>::new().width(40.0).height(20.0));
880        assert_eq!(widths(&giving_way), vec![60.0, 40.0]);
881    }
882
883    /// **A held box crosses over to its pressed colour** (milestone 441) instead of
884    /// snapping to it. The line that chose it read `Interaction::Pressed` and the comment
885    /// beside it said "Pressed: instant" — the one step of the rest → hover → held path
886    /// that was not a progression.
887    #[test]
888    fn a_held_box_crosses_over_to_its_pressed_colour() {
889        use frus_core::Primitive;
890
891        let rest = Color::rgb(0.2, 0.2, 0.2);
892        let held = Color::rgb(0.8, 0.8, 0.8);
893        let box_: Container<()> = Container::new()
894            .width(20.0)
895            .height(20.0)
896            .color(rest)
897            .pressed_color(held);
898
899        let fill = |press: f32| {
900            let mut scene = Scene::new();
901            Widget::<()>::paint(
902                &box_,
903                Rect::new(0.0, 0.0, 20.0, 20.0),
904                Status {
905                    press_progress: press,
906                    ..Default::default()
907                },
908                &crate::Theme::dark(),
909                &mut scene,
910            );
911            scene
912                .primitives()
913                .iter()
914                .find_map(|p| match p {
915                    Primitive::Rect { color, .. } => Some(*color),
916                    _ => None,
917                })
918                .expect("a box paints its background")
919        };
920
921        assert_eq!(fill(0.0), rest, "untouched");
922        assert_eq!(fill(1.0), held, "held");
923        let half = fill(0.5).r;
924        assert!(
925            half > rest.r && half < held.r,
926            "half way through the press: {half}"
927        );
928    }
929
930    /// A `Container` with a group opacity < 1 has its painted subtree wrapped in a
931    /// [`frus_core::Primitive::Layer`] at that opacity.
932    #[test]
933    fn opacity_group_wraps_subtree_in_a_layer() {
934        use frus_core::{Primitive, Size};
935        let root: Container<()> = Container::new()
936            .width(40.0)
937            .height(40.0)
938            .color(Color::rgb(1.0, 0.0, 0.0))
939            .opacity(0.5);
940        let rt = crate::runtime::Runtime::default();
941        let theme = crate::Theme::dark();
942        let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
943        let layer = ui.scene().primitives().iter().find_map(|p| match p {
944            Primitive::Layer {
945                opacity,
946                primitives,
947                ..
948            } => Some((*opacity, primitives.len())),
949            _ => None,
950        });
951        let (op, n) = layer.expect("a group opacity layer");
952        assert!((op - 0.5).abs() < 1e-6, "group opacity = {op}");
953        assert!(
954            n >= 1,
955            "the layer wraps the painted content ({n} primitives)"
956        );
957    }
958
959    /// Full opacity (`1.0`): no layer is emitted (the opaque path, at zero cost).
960    #[test]
961    fn full_opacity_emits_no_layer() {
962        use frus_core::{Primitive, Size};
963        let root: Container<()> = Container::new()
964            .width(40.0)
965            .height(40.0)
966            .color(Color::rgb(1.0, 0.0, 0.0))
967            .opacity(1.0);
968        let rt = crate::runtime::Runtime::default();
969        let theme = crate::Theme::dark();
970        let ui = crate::ui::build_ui(&root, Size::new(64.0, 64.0), &rt, &theme);
971        assert!(
972            !ui.scene()
973                .primitives()
974                .iter()
975                .any(|p| matches!(p, Primitive::Layer { .. })),
976            "no layer at full opacity"
977        );
978    }
979
980    /// `animated_opacity` declares an animated value (the runtime tweens it) with the
981    /// duration and curve supplied; `opacity` alone does not (a fixed opacity).
982    #[test]
983    fn animated_opacity_declares_anim_target() {
984        let animated: Container<()> = Container::new().animated_opacity(0.0, 0.3, Curve::ease_in());
985        assert_eq!(Widget::<()>::anim_target(&animated), Some(0.0));
986        assert_eq!(Widget::<()>::anim_duration(&animated), 0.3);
987        assert_eq!(Widget::<()>::anim_curve(&animated), Curve::ease_in());
988        assert_eq!(Widget::<()>::opacity_group(&animated), Some(0.0));
989
990        // A fixed opacity: a group, yes, but no animated value.
991        let fixed: Container<()> = Container::new().opacity(0.5);
992        assert_eq!(Widget::<()>::anim_target(&fixed), None);
993        assert_eq!(Widget::<()>::opacity_group(&fixed), Some(0.5));
994    }
995
996    /// An `animated_color` background paints the color the runtime **interpolates**
997    /// (not the target): mounted at red, transitioning to blue half-way → the
998    /// background rectangle is about half red, half blue.
999    #[test]
1000    fn animated_color_paints_the_interpolated_color() {
1001        use frus_core::{Primitive, Size};
1002        let red = Color::rgb(1.0, 0.0, 0.0);
1003        let blue = Color::rgb(0.0, 0.0, 1.0);
1004        let mut rt = crate::runtime::Runtime::default();
1005        let start: Container<()> =
1006            Container::new()
1007                .width(20.0)
1008                .height(20.0)
1009                .animated_color(red, 0.10, Curve::Linear);
1010        rt.advance_colors(&start, 1.0); // montage → rouge
1011        let to_blue: Container<()> =
1012            Container::new()
1013                .width(20.0)
1014                .height(20.0)
1015                .animated_color(blue, 0.10, Curve::Linear);
1016        rt.advance_colors(&to_blue, 0.05); // t = 0.5
1017
1018        let theme = crate::Theme::dark();
1019        let ui = crate::ui::build_ui(&to_blue, Size::new(20.0, 20.0), &rt, &theme);
1020        let color = ui
1021            .scene()
1022            .primitives()
1023            .iter()
1024            .find_map(|p| match p {
1025                Primitive::Rect { color, .. } => Some(*color),
1026                _ => None,
1027            })
1028            .expect("un rectangle de fond");
1029        assert!(
1030            (color.r - 0.5).abs() < 0.1 && (color.b - 0.5).abs() < 0.1,
1031            "the interpolated color is painted: {color:?}"
1032        );
1033    }
1034
1035    /// An `animated_size` **drives the layout**: half-way through (20×20 → 40×40,
1036    /// linear), the background rectangle measures about 30×30 (the interpolated
1037    /// size injected into layout through `effective_style`).
1038    #[test]
1039    fn animated_size_drives_the_layout() {
1040        use frus_core::{Primitive, Size};
1041        let mut rt = crate::runtime::Runtime::default();
1042        let red = Color::rgb(1.0, 0.0, 0.0);
1043        let start: Container<()> =
1044            Container::new()
1045                .color(red)
1046                .animated_size(20.0, 20.0, 0.10, Curve::Linear);
1047        rt.advance_sizes(&start, 1.0); // montage → 20×20
1048        let to_big: Container<()> =
1049            Container::new()
1050                .color(red)
1051                .animated_size(40.0, 40.0, 0.10, Curve::Linear);
1052        rt.advance_sizes(&to_big, 0.05); // t = 0.5 → 30×30
1053
1054        let theme = crate::Theme::dark();
1055        let ui = crate::ui::build_ui(&to_big, Size::new(100.0, 100.0), &rt, &theme);
1056        let rect = ui
1057            .scene()
1058            .primitives()
1059            .iter()
1060            .find_map(|p| match p {
1061                Primitive::Rect { rect, .. } => Some(*rect),
1062                _ => None,
1063            })
1064            .expect("un rectangle de fond");
1065        assert!(
1066            (rect.width - 30.0).abs() < 1.0,
1067            "interpolated width: {}",
1068            rect.width
1069        );
1070        assert!(
1071            (rect.height - 30.0).abs() < 1.0,
1072            "interpolated height: {}",
1073            rect.height
1074        );
1075    }
1076
1077    /// An `animated_radius` paints the **interpolated** radius: mounted at 0,
1078    /// transitioning to 20 half-way → the background rectangle has a radius of ~10.
1079    #[test]
1080    fn animated_radius_paints_the_interpolated_radius() {
1081        use frus_core::{Primitive, Size};
1082        let red = Color::rgb(1.0, 0.0, 0.0);
1083        let mut rt = crate::runtime::Runtime::default();
1084        let sharp: Container<()> = Container::new()
1085            .width(40.0)
1086            .height(40.0)
1087            .color(red)
1088            .animated_radius(0.0, 0.10, Curve::Linear);
1089        rt.advance_radii(&sharp, 1.0); // montage → 0
1090        let round: Container<()> = Container::new()
1091            .width(40.0)
1092            .height(40.0)
1093            .color(red)
1094            .animated_radius(20.0, 0.10, Curve::Linear);
1095        rt.advance_radii(&round, 0.05); // t = 0.5 → 10
1096
1097        let theme = crate::Theme::dark();
1098        let ui = crate::ui::build_ui(&round, Size::new(60.0, 60.0), &rt, &theme);
1099        let radius = ui
1100            .scene()
1101            .primitives()
1102            .iter()
1103            .find_map(|p| match p {
1104                Primitive::Rect { radius, .. } => Some(*radius),
1105                _ => None,
1106            })
1107            .expect("un rectangle de fond");
1108        assert!(
1109            (radius.top_left - 10.0).abs() < 1.0,
1110            "interpolated radius: {}",
1111            radius.top_left
1112        );
1113    }
1114
1115    /// An `animated_padding` **insets the child at layout time**: mounted at 0,
1116    /// transitioning to 20 half-way → the child is inset by ~10 (the interpolated
1117    /// padding).
1118    #[test]
1119    fn animated_padding_insets_the_child_at_layout() {
1120        use frus_core::{Primitive, Size};
1121        let red = Color::rgb(1.0, 0.0, 0.0);
1122        let build = |pad: f32| {
1123            Container::<()>::new()
1124                .width(60.0)
1125                .height(60.0)
1126                .animated_padding(pad, 0.10, Curve::Linear)
1127                .child(Container::new().width(20.0).height(20.0).color(red))
1128        };
1129        let mut rt = crate::runtime::Runtime::default();
1130        rt.advance_paddings(&build(0.0), 1.0); // montage → 0
1131        let bigger = build(20.0);
1132        rt.advance_paddings(&bigger, 0.05); // t = 0.5 → 10
1133
1134        let theme = crate::Theme::dark();
1135        let ui = crate::ui::build_ui(&bigger, Size::new(60.0, 60.0), &rt, &theme);
1136        let rect = ui
1137            .scene()
1138            .primitives()
1139            .iter()
1140            .find_map(|p| match p {
1141                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1142                _ => None,
1143            })
1144            .expect("the child's red background");
1145        assert!(
1146            (rect.x - 10.0).abs() < 1.0 && (rect.y - 10.0).abs() < 1.0,
1147            "the child is inset by ~10 by the interpolated padding: {rect:?}"
1148        );
1149    }
1150
1151    /// `alignment(Center)` positions the (20×20) child at the centre of a 100×100 box
1152    /// → its background sits at about (40, 40).
1153    #[test]
1154    fn alignment_centers_the_child() {
1155        use frus_core::{Alignment, Primitive, Size};
1156        let red = Color::rgb(1.0, 0.0, 0.0);
1157        let root: Container<()> = Container::new()
1158            .width(100.0)
1159            .height(100.0)
1160            .alignment(Alignment::CENTER)
1161            .child(Container::new().width(20.0).height(20.0).color(red));
1162        let rt = crate::runtime::Runtime::default();
1163        let theme = crate::Theme::dark();
1164        let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1165        let rect = ui
1166            .scene()
1167            .primitives()
1168            .iter()
1169            .find_map(|p| match p {
1170                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1171                _ => None,
1172            })
1173            .expect("the child's red background");
1174        assert!(
1175            (rect.x - 40.0).abs() < 1.0 && (rect.y - 40.0).abs() < 1.0,
1176            "centred: {rect:?}"
1177        );
1178    }
1179
1180    /// `alignment(BottomRight)` anchors the (20×20) child to the bottom-right corner
1181    /// of a 100×100 box → its background sits at about (80, 80).
1182    #[test]
1183    fn alignment_anchors_child_to_a_corner() {
1184        use frus_core::{Alignment, Primitive, Size};
1185        let red = Color::rgb(1.0, 0.0, 0.0);
1186        let root: Container<()> = Container::new()
1187            .width(100.0)
1188            .height(100.0)
1189            .alignment(Alignment::BOTTOM_RIGHT)
1190            .child(Container::new().width(20.0).height(20.0).color(red));
1191        let rt = crate::runtime::Runtime::default();
1192        let theme = crate::Theme::dark();
1193        let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1194        let rect = ui
1195            .scene()
1196            .primitives()
1197            .iter()
1198            .find_map(|p| match p {
1199                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1200                _ => None,
1201            })
1202            .expect("the child's red background");
1203        assert!(
1204            (rect.x - 80.0).abs() < 1.0 && (rect.y - 80.0).abs() < 1.0,
1205            "bottom-right corner: {rect:?}"
1206        );
1207    }
1208
1209    /// A **fractional** anchor (outside the nine discrete positions) places the child
1210    /// proportionally: `x = 0.5, y = -0.5` → fractions (0.75, 0.25) → in a 100×100
1211    /// box with a 20×20 child (80×80 free), the background lands at about (60, 20).
1212    /// This is what the discrete flex could not do — and what makes
1213    /// `Tween<Alignment>` visually continuous.
1214    #[test]
1215    fn fractional_alignment_places_child_proportionally() {
1216        use frus_core::{Alignment, Primitive, Size};
1217        let red = Color::rgb(1.0, 0.0, 0.0);
1218        let root: Container<()> = Container::new()
1219            .width(100.0)
1220            .height(100.0)
1221            .alignment(Alignment::new(0.5, -0.5))
1222            .child(Container::new().width(20.0).height(20.0).color(red));
1223        let rt = crate::runtime::Runtime::default();
1224        let theme = crate::Theme::dark();
1225        let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, &theme);
1226        let rect = ui
1227            .scene()
1228            .primitives()
1229            .iter()
1230            .find_map(|p| match p {
1231                Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(*rect),
1232                _ => None,
1233            })
1234            .expect("the child's red background");
1235        assert!(
1236            (rect.x - 60.0).abs() < 1.0 && (rect.y - 20.0).abs() < 1.0,
1237            "fractionnel : {rect:?}"
1238        );
1239    }
1240
1241    /// A **directional** anchor follows the reading direction: `CENTER_START` places
1242    /// the child on the **left** in LTR, on the **right** in RTL (resolved at render
1243    /// time).
1244    #[test]
1245    fn directional_alignment_flips_the_child_in_rtl() {
1246        use frus_core::{AlignmentDirectional, Primitive, Size};
1247        let red = Color::rgb(1.0, 0.0, 0.0);
1248        let rt = crate::runtime::Runtime::default();
1249        let child_x = |theme: &crate::Theme| {
1250            let root: Container<()> = Container::new()
1251                .width(100.0)
1252                .height(100.0)
1253                .alignment(AlignmentDirectional::CENTER_START)
1254                .child(Container::new().width(20.0).height(20.0).color(red));
1255            let ui = crate::ui::build_ui(&root, Size::new(100.0, 100.0), &rt, theme);
1256            ui.scene()
1257                .primitives()
1258                .iter()
1259                .find_map(|p| match p {
1260                    Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
1261                    _ => None,
1262                })
1263                .expect("the child's red background")
1264        };
1265        assert!(
1266            child_x(&crate::Theme::dark()).abs() < 1.0,
1267            "start on the left in LTR"
1268        );
1269        assert!(
1270            (child_x(&crate::Theme::dark().rtl()) - 80.0).abs() < 1.0,
1271            "start on the right in RTL"
1272        );
1273    }
1274
1275    /// `decoration(...)` applies background, radius and border as one block: the
1276    /// background paints the given color and radius, and the border reserves its
1277    /// width at layout time.
1278    /// **A border side by side takes its own room on each side**, a directional one by the
1279    /// reading direction; a circle clips as one; and each shadow said is kept (milestone
1280    /// 641).
1281    #[test]
1282    fn sides_shape_and_shadows_reach_the_box() {
1283        use frus_core::{BorderDirectional, BorderSide, BoxShape, ClipShape, TextDirection};
1284        let sided: Container<()> = Container::new().box_border(BorderDirectional {
1285            start: BorderSide::new(Color::WHITE, 6.0),
1286            top: BorderSide::new(Color::WHITE, 2.0),
1287            ..Default::default()
1288        });
1289        assert_eq!(
1290            sided.effective_padding_in(TextDirection::Ltr),
1291            Insets::new(2.0, 0.0, 0.0, 6.0)
1292        );
1293        assert_eq!(
1294            sided.effective_padding_in(TextDirection::Rtl),
1295            Insets::new(2.0, 6.0, 0.0, 0.0)
1296        );
1297        let round: Container<()> = Container::new().shape(BoxShape::Circle).clip();
1298        assert_eq!(Widget::<()>::clip_shape(&round), Some(ClipShape::Oval));
1299        let two: Container<()> = Container::new().shadow(0.0, 1.0, 2.0, Color::BLACK).shadow(
1300            0.0,
1301            4.0,
1302            8.0,
1303            Color::BLACK,
1304        );
1305        assert_eq!(two.shadows.len(), 2, "each call adds one");
1306        let none: Container<()> = Container::new().border(0.0, Color::WHITE);
1307        assert_eq!(none.border, None, "no width, no border");
1308    }
1309
1310    #[test]
1311    fn decoration_applies_composite_fields() {
1312        use frus_core::{BorderRadius, BoxDecoration, Primitive, Size};
1313        let green = Color::rgb(0.0, 1.0, 0.0);
1314        let deco = BoxDecoration {
1315            color: Some(green),
1316            radius: BorderRadius::uniform(8.0),
1317            border: Some(Border::new(2.0, Color::WHITE).into()),
1318            ..Default::default()
1319        };
1320        let root: Container<()> = Container::new().width(40.0).height(40.0).decoration(deco);
1321        // The border reserves its width in the layout padding.
1322        assert_eq!(Widget::style(&root).padding, Insets::uniform(2.0));
1323
1324        let rt = crate::runtime::Runtime::default();
1325        let theme = crate::Theme::dark();
1326        let ui = crate::ui::build_ui(&root, Size::new(40.0, 40.0), &rt, &theme);
1327        let (color, radius) = ui
1328            .scene()
1329            .primitives()
1330            .iter()
1331            .find_map(|p| match p {
1332                Primitive::Rect { color, radius, .. } if color.g > 0.5 => Some((*color, *radius)),
1333                _ => None,
1334            })
1335            .expect("the decorated green background");
1336        assert!(color.g > 0.9, "a green background: {color:?}");
1337        assert!(
1338            (radius.top_left - 8.0).abs() < 1e-3,
1339            "rayon composite : {}",
1340            radius.top_left
1341        );
1342    }
1343
1344    /// `margin(...)` reserves space **around** the box: it pushes the siblings away
1345    /// and offsets the background without growing it. In a column, a second child
1346    /// (20 tall) with a margin of 10 starts at `y = 20 (sibling) + 10 (margin)` and
1347    /// is inset by 10 on the left.
1348    #[test]
1349    fn margin_pushes_siblings_and_insets() {
1350        use frus_core::{Primitive, Size};
1351        let red = Color::rgb(1.0, 0.0, 0.0);
1352        let green = Color::rgb(0.0, 1.0, 0.0);
1353        let root = crate::Flex::<()>::column()
1354            .width(100.0)
1355            .child(Container::new().height(20.0).color(red))
1356            .child(Container::new().height(20.0).margin(10.0).color(green));
1357        let rt = crate::runtime::Runtime::default();
1358        let theme = crate::Theme::dark();
1359        let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
1360        let rect = ui
1361            .scene()
1362            .primitives()
1363            .iter()
1364            .find_map(|p| match p {
1365                Primitive::Rect { rect, color, .. } if color.g > 0.5 && color.r < 0.5 => {
1366                    Some(*rect)
1367                }
1368                _ => None,
1369            })
1370            .expect("the 2nd child's green background");
1371        assert!(
1372            (rect.y - 30.0).abs() < 0.5 && (rect.x - 10.0).abs() < 0.5,
1373            "margin: pushed to y=30, inset to x=10: {rect:?}"
1374        );
1375        assert!(
1376            (rect.height - 20.0).abs() < 0.5,
1377            "the margin does not grow the box: {rect:?}"
1378        );
1379    }
1380
1381    #[test]
1382    fn visible_border_reserves_layout_padding() {
1383        // A visible border: the layout padding reserves its width.
1384        let bordered: Container<()> = Container::new().padding(4.0).border(2.0, Color::WHITE);
1385        assert_eq!(Widget::style(&bordered).padding, Insets::uniform(6.0));
1386
1387        // With no border (or an invisible one): the padding is unchanged.
1388        let plain: Container<()> = Container::new().padding(4.0);
1389        assert_eq!(Widget::style(&plain).padding, Insets::uniform(4.0));
1390        let invisible: Container<()> = Container::new()
1391            .padding(4.0)
1392            .border(2.0, Color::TRANSPARENT);
1393        assert_eq!(Widget::style(&invisible).padding, Insets::uniform(4.0));
1394    }
1395}
1396
1397#[cfg(test)]
1398mod clip_tests {
1399    use super::*;
1400    use crate::{build_ui, Runtime, Size};
1401    use frus_core::Primitive;
1402
1403    fn layers(root: &Container<()>) -> Vec<frus_core::ClipShape> {
1404        let ui = build_ui(
1405            root,
1406            Size::new(100.0, 100.0),
1407            &Runtime::default(),
1408            &Theme::default(),
1409        );
1410        ui.scene()
1411            .primitives()
1412            .iter()
1413            .filter_map(|p| match p {
1414                Primitive::Layer { clip_shape, .. } => Some(clip_shape.clone()),
1415                _ => None,
1416            })
1417            .collect()
1418    }
1419
1420    /// Off by default, as in the reference: a decoration is painted behind a child and
1421    /// does not confine it.
1422    #[test]
1423    fn a_rounded_box_does_not_clip_unless_asked() {
1424        let plain = Container::<()>::new()
1425            .width(100.0)
1426            .height(100.0)
1427            .radius(12.0)
1428            .color(Color::rgb(1.0, 0.0, 0.0))
1429            .child(Container::<()>::new().width(100.0).height(100.0));
1430        assert!(layers(&plain).is_empty(), "no compositing layer");
1431    }
1432
1433    /// Asked, it wraps its child in a layer clipped to its own corners.
1434    #[test]
1435    fn clip_confines_the_child_to_the_corners() {
1436        let clipped = Container::<()>::new()
1437            .width(100.0)
1438            .height(100.0)
1439            .radius(12.0)
1440            .clip()
1441            .color(Color::rgb(1.0, 0.0, 0.0))
1442            .child(Container::<()>::new().width(100.0).height(100.0));
1443        let shapes = layers(&clipped);
1444        assert_eq!(shapes.len(), 1);
1445        match &shapes[0] {
1446            frus_core::ClipShape::RRect(r) => assert_eq!(r.top_left, 12.0),
1447            other => panic!("a rounded rectangle, not {other:?}"),
1448        }
1449    }
1450}
1451
1452#[cfg(test)]
1453mod foreground_tests {
1454    use super::*;
1455    use crate::{build_ui, Runtime, Size};
1456    use frus_core::{Primitive, Scene};
1457
1458    const BACK: Color = Color::rgb(1.0, 0.0, 0.0);
1459    const CHILD: Color = Color::rgb(0.0, 1.0, 0.0);
1460    const OVER: Color = Color::rgb(0.0, 0.0, 1.0);
1461
1462    fn scene(root: &Container<()>) -> Scene {
1463        build_ui(
1464            root,
1465            Size::new(100.0, 100.0),
1466            &Runtime::default(),
1467            &Theme::default(),
1468        )
1469        .scene()
1470        .clone()
1471    }
1472
1473    /// The order the three coloured boxes came out in, by their colour.
1474    fn order(root: &Container<()>) -> Vec<Color> {
1475        scene(root)
1476            .primitives()
1477            .iter()
1478            .filter_map(|p| match p {
1479                Primitive::Rect { color, .. } if *color == BACK || *color == CHILD => Some(*color),
1480                Primitive::Rect {
1481                    border_color,
1482                    border_width,
1483                    ..
1484                } if *border_width > 0.0 && *border_color == OVER => Some(OVER),
1485                _ => None,
1486            })
1487            .collect()
1488    }
1489
1490    fn card() -> Container<()> {
1491        Container::<()>::new()
1492            .width(100.0)
1493            .height(100.0)
1494            .color(BACK)
1495            .child(
1496                Container::<()>::new()
1497                    .width(100.0)
1498                    .height(100.0)
1499                    .color(CHILD),
1500            )
1501    }
1502
1503    /// Nothing is painted over a child unless a foreground asks for it.
1504    #[test]
1505    fn nothing_paints_over_the_child_by_default() {
1506        assert_eq!(order(&card()), vec![BACK, CHILD]);
1507    }
1508
1509    /// The whole point: after the child, not before it. A background could not say this.
1510    #[test]
1511    fn a_foreground_paints_after_the_child() {
1512        let card = card().foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1513        assert_eq!(order(&card), vec![BACK, CHILD, OVER]);
1514    }
1515
1516    /// It uses the container's own box, the same one the background does.
1517    #[test]
1518    fn it_covers_the_container_s_own_box() {
1519        let card = card()
1520            .padding(10.0)
1521            .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1522        let over = scene(&card)
1523            .primitives()
1524            .iter()
1525            .find_map(|p| match p {
1526                Primitive::Rect {
1527                    border_color,
1528                    border_width,
1529                    rect,
1530                    ..
1531                } if *border_width > 0.0 && *border_color == OVER => Some(*rect),
1532                _ => None,
1533            })
1534            .expect("the foreground is painted");
1535        assert_eq!((over.width, over.height), (100.0, 100.0));
1536    }
1537
1538    /// A foreground that names no radius takes the container's: a square outline over a
1539    /// rounded box is a mistake far more often than a request.
1540    #[test]
1541    fn a_silent_foreground_wears_the_container_s_corners() {
1542        let card = card()
1543            .radius(12.0)
1544            .foreground(BoxDecoration::default().border(Border::new(2.0, OVER)));
1545        assert_eq!(
1546            Widget::<()>::foreground(&card, &Theme::default())
1547                .unwrap()
1548                .radius,
1549            BorderRadius::uniform(12.0),
1550            "resolved against the container on the way out"
1551        );
1552    }
1553
1554    /// Said explicitly, the foreground's own radius stands.
1555    #[test]
1556    fn an_explicit_radius_is_left_alone() {
1557        let card = card().radius(12.0).foreground(
1558            BoxDecoration::default()
1559                .border(Border::new(2.0, OVER))
1560                .radius(4.0),
1561        );
1562        assert_eq!(
1563            Widget::<()>::foreground(&card, &Theme::default())
1564                .unwrap()
1565                .radius,
1566            BorderRadius::uniform(4.0)
1567        );
1568    }
1569}
1570
1571/// The reference's container rules (milestone 633): `container.dart:387`.
1572#[cfg(test)]
1573mod reference_tests {
1574    use super::*;
1575    use crate::{build_ui_inspected, Center, Flex, Runtime, Text};
1576
1577    fn rect_of(root: &dyn Widget<()>, size: Size, nth: usize) -> Rect {
1578        let (_, nodes) = build_ui_inspected(root, size, &Runtime::default(), &Theme::dark());
1579        nodes
1580            .iter()
1581            .filter(|n| n.name == "Container")
1582            .nth(nth)
1583            .map(|n| n.rect)
1584            .expect("a container")
1585    }
1586
1587    /// **Empty, a container takes the room its parent allows** — inside a centre, all of
1588    /// it — and **along a row shared with others it is nothing**, the reference's
1589    /// `LimitedBox(0)` in unbounded room.
1590    #[test]
1591    fn an_empty_container_takes_the_room_allowed() {
1592        let size = Size::new(300.0, 200.0);
1593        let alone = Center::new(Container::<()>::new().color(Color::rgb8(1, 2, 3)));
1594        assert_eq!(rect_of(&alone, size, 0), Rect::new(0.0, 0.0, 300.0, 200.0));
1595        let row = Flex::<()>::row()
1596            .width(300.0)
1597            .height(50.0)
1598            .child(Container::new().color(Color::rgb8(1, 2, 3)))
1599            .child(Container::new().width(10.0).height(10.0));
1600        let empty = rect_of(&row, size, 0);
1601        assert_eq!(
1602            (empty.width, empty.height),
1603            (0.0, 50.0),
1604            "nothing along, the row across"
1605        );
1606    }
1607
1608    /// **Aligning, it fills its parent and places its child inside**; otherwise it is its
1609    /// child's size.
1610    #[test]
1611    fn an_aligning_container_fills_its_parent() {
1612        let size = Size::new(300.0, 200.0);
1613        let aligned = Center::new(
1614            Container::<()>::new()
1615                .alignment(frus_core::Alignment::BOTTOM_RIGHT)
1616                .child(Container::new().width(20.0).height(10.0)),
1617        );
1618        assert_eq!(
1619            rect_of(&aligned, size, 0),
1620            Rect::new(0.0, 0.0, 300.0, 200.0)
1621        );
1622        assert_eq!(
1623            rect_of(&aligned, size, 1),
1624            Rect::new(280.0, 190.0, 20.0, 10.0)
1625        );
1626        let hugging = Center::new(Container::<()>::new().child(Text::new("Hi")));
1627        let hug = rect_of(&hugging, size, 0);
1628        assert!(hug.width < 40.0 && hug.height < 30.0, "{hug:?}");
1629    }
1630
1631    /// **Constraints**, which hold a width given with them (the reference's `tighten`).
1632    #[test]
1633    fn constraints_hold_the_box() {
1634        let size = Size::new(300.0, 200.0);
1635        let floored = Center::new(
1636            Container::<()>::new()
1637                .min_width(80.0)
1638                .min_height(30.0)
1639                .child(Text::new("Hi")),
1640        );
1641        let r = rect_of(&floored, size, 0);
1642        assert_eq!((r.width, r.height), (80.0, 30.0));
1643        let capped = Center::new(
1644            Container::<()>::new()
1645                .width(500.0)
1646                .max_width(120.0)
1647                .height(10.0),
1648        );
1649        assert_eq!(rect_of(&capped, size, 0).width, 120.0);
1650        // Empty, a ceiling caps the room it takes.
1651        let ceiling = Center::new(Container::<()>::new().max_width(50.0).max_height(40.0));
1652        let r = rect_of(&ceiling, size, 0);
1653        assert_eq!((r.width, r.height), (50.0, 40.0));
1654    }
1655
1656    /// **A transform is painted, not laid out**, about the transform alignment.
1657    #[test]
1658    fn a_transform_is_about_its_alignment() {
1659        let c = Container::<()>::new()
1660            .width(40.0)
1661            .height(20.0)
1662            .translated(5.0, 6.0)
1663            .scaled(2.0, 3.0)
1664            .rotated(0.5)
1665            .transform_alignment(frus_core::Alignment::TOP_LEFT);
1666        assert_eq!(Widget::<()>::transform_translate(&c), Some((5.0, 6.0)));
1667        assert_eq!(
1668            Widget::<()>::transform_scale(&c),
1669            Some((2.0, 3.0, frus_core::Alignment::TOP_LEFT))
1670        );
1671        assert_eq!(
1672            Widget::<()>::transform_rotate(&c),
1673            Some((0.5, frus_core::Alignment::TOP_LEFT))
1674        );
1675        let plain = Container::<()>::new().scaled(2.0, 2.0);
1676        assert_eq!(
1677            Widget::<()>::transform_scale(&plain).map(|t| t.2),
1678            Some(frus_core::Alignment::CENTER),
1679            "about the centre unless told"
1680        );
1681    }
1682}