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