Skip to main content

frus_widgets/
fittedbox.rs

1//! [`FittedBox`]: scales its child to **fit** its box according to a [`BoxFit`] —
2//! and, unlike [`crate::Transform`], the scale follows from the **layout** (the
3//! size of the box) instead of being set by hand.
4
5use frus_core::{BoxFit, Rect, Scene};
6use frus_layout::{Dimension, Style};
7
8use crate::interaction::Status;
9use crate::theme::Theme;
10use crate::widget::Widget;
11
12/// Scales its child to **fit** its own box according to a [`BoxFit`] (like CSS
13/// `object-fit`), then centres it. The child is measured at its **natural** size;
14/// the scale factor follows from that — hence the effect on layout (unlike
15/// `Transform`, where the scale is set by hand).
16///
17/// Ideal for making intrinsically sized content (text, an icon, a drawing) **fill**
18/// or **fit** a given frame with no manual arithmetic. The box needs a size (as
19/// [`crate::SingleChildScrollView`] does): a fixed `width`/`height`, or `flex`.
20///
21/// ```ignore
22/// FittedBox::new(BoxFit::Contain).width(120.0).height(40.0).child(Text::new("Big"))
23/// ```
24pub struct FittedBox<Msg = crate::callback::Callback> {
25    fit: BoxFit,
26    width: Dimension,
27    height: Dimension,
28    flex_grow: f32,
29    children: Vec<Box<dyn Widget<Msg>>>,
30}
31
32impl<Msg> FittedBox<Msg> {
33    /// Fits the child according to `fit` (the usual default: [`BoxFit::Contain`]).
34    pub fn new(fit: BoxFit) -> Self {
35        Self {
36            fit,
37            width: Dimension::Auto,
38            height: Dimension::Auto,
39            flex_grow: 0.0,
40            children: Vec::new(),
41        }
42    }
43
44    /// Fixed box width, in logical pixels.
45    pub fn width(mut self, width: f32) -> Self {
46        self.width = Dimension::Length(width);
47        self
48    }
49
50    /// Fixed box height, in logical pixels.
51    pub fn height(mut self, height: f32) -> Self {
52        self.height = Dimension::Length(height);
53        self
54    }
55
56    /// Flex growth factor along the parent's main axis.
57    pub fn flex(mut self, grow: f32) -> Self {
58        self.flex_grow = grow;
59        self
60    }
61
62    /// Sets the fitted child.
63    pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
64        self.children.clear();
65        self.children.push(Box::new(child));
66        self
67    }
68}
69
70impl<Msg: Clone> Widget<Msg> for FittedBox<Msg> {
71    fn style(&self) -> Style {
72        Style {
73            width: self.width,
74            height: self.height,
75            flex_grow: self.flex_grow,
76            ..Default::default()
77        }
78    }
79
80    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
81        &self.children
82    }
83
84    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
85        // A pure fitting widget: no decoration of its own.
86    }
87
88    fn on_click(&self) -> Option<Msg> {
89        None
90    }
91
92    fn fitted(&self) -> Option<BoxFit> {
93        Some(self.fit)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::{Container, Flex};
101    use frus_core::{Color, Primitive, Size};
102
103    /// `BoxFit::Fill` stretches the child to **fill** the box: the layer's matrix
104    /// carries the per-axis scale (200/40 = 5 in x, 100/20 = 5 in y here → square: 5,5).
105    #[test]
106    fn fill_scales_child_to_the_box() {
107        let root = Flex::<()>::column().width(200.0).child(
108            FittedBox::new(BoxFit::Fill)
109                .width(200.0)
110                .height(100.0)
111                .child(
112                    Container::new()
113                        .width(40.0)
114                        .height(20.0)
115                        .color(Color::rgb(0.3, 0.3, 0.3)),
116                ),
117        );
118        let rt = crate::runtime::Runtime::default();
119        let theme = crate::Theme::dark();
120        let ui = crate::ui::build_ui(&root, Size::new(200.0, 400.0), &rt, &theme);
121        let m = ui
122            .scene()
123            .primitives()
124            .iter()
125            .find_map(|p| match p {
126                Primitive::Layer {
127                    transform: Some(t), ..
128                } => Some(t.affine),
129                _ => None,
130            })
131            .expect("a fitted layer");
132        assert!(
133            (m.m[0] - 5.0).abs() < 1e-2 && (m.m[3] - 5.0).abs() < 1e-2,
134            "Fill scale 5×5: {:?}",
135            m.m
136        );
137    }
138
139    /// `BoxFit::Contain` preserves the aspect ratio: the smallest factor that fits.
140    /// A 40×20 child in 200×100 → min(5, 5) = 5 (square), and stays centred.
141    #[test]
142    fn contain_preserves_aspect() {
143        let root = Flex::<()>::column().width(200.0).child(
144            FittedBox::new(BoxFit::Contain)
145                .width(200.0)
146                .height(100.0)
147                .child(
148                    Container::new()
149                        .width(40.0)
150                        .height(40.0)
151                        .color(Color::rgb(0.3, 0.3, 0.3)),
152                ),
153        );
154        let rt = crate::runtime::Runtime::default();
155        let theme = crate::Theme::dark();
156        let ui = crate::ui::build_ui(&root, Size::new(200.0, 400.0), &rt, &theme);
157        let m = ui
158            .scene()
159            .primitives()
160            .iter()
161            .find_map(|p| match p {
162                Primitive::Layer {
163                    transform: Some(t), ..
164                } => Some(t.affine),
165                _ => None,
166            })
167            .expect("a fitted layer");
168        // A square 40×40 child in 200×100 → min(5, 2.5) = 2.5, uniform.
169        assert!(
170            (m.m[0] - 2.5).abs() < 1e-2 && (m.m[3] - 2.5).abs() < 1e-2,
171            "Contain scale 2.5×2.5: {:?}",
172            m.m
173        );
174    }
175}