1use frus_core::{Rect, Scene, Size};
33use frus_layout::{Dimension, Style};
34
35use crate::interaction::Status;
36use crate::theme::Theme;
37use crate::widget::Widget;
38
39pub struct LayoutBuilder<Msg = crate::callback::Callback> {
41 width: Dimension,
42 height: Dimension,
43 flex_grow: f32,
44 fills: bool,
47 build: Box<dyn Fn(Size) -> Box<dyn Widget<Msg>>>,
48}
49
50impl<Msg> LayoutBuilder<Msg> {
51 pub fn new<W: Widget<Msg> + 'static>(build: impl Fn(Size) -> W + 'static) -> Self {
54 Self {
55 width: Dimension::Auto,
56 height: Dimension::Auto,
57 flex_grow: 0.0,
58 fills: false,
59 build: Box::new(move |size| Box::new(build(size)) as Box<dyn Widget<Msg>>),
60 }
61 }
62
63 pub fn width(mut self, width: f32) -> Self {
65 self.width = Dimension::Length(width);
66 self
67 }
68
69 pub fn height(mut self, height: f32) -> Self {
71 self.height = Dimension::Length(height);
72 self
73 }
74
75 pub(crate) fn filling(mut self) -> Self {
78 self.fills = true;
79 self
80 }
81
82 pub fn flex(mut self, grow: f32) -> Self {
84 self.flex_grow = grow;
85 self
86 }
87}
88
89impl<Msg> Widget<Msg> for LayoutBuilder<Msg> {
90 fn style(&self) -> Style {
91 Style {
92 width: self.width,
93 height: self.height,
94 flex_grow: self.flex_grow,
95 ..Default::default()
96 }
97 }
98
99 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
100 &[]
101 }
102
103 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
104
105 fn on_click(&self) -> Option<Msg> {
106 None
107 }
108
109 fn layout_builder(&self) -> Option<&dyn Fn(Size) -> Box<dyn Widget<Msg>>> {
110 Some(&*self.build)
111 }
112
113 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
114 if self.fills {
115 crate::widget::FillAxes::BOTH
116 } else {
117 crate::widget::FillAxes::NONE
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::{build_ui, Container, Runtime, SizeClass};
126 use frus_core::{Color, Primitive};
127
128 #[test]
129 fn receives_its_real_box() {
130 use std::cell::Cell;
131 use std::rc::Rc;
132
133 let seen = Rc::new(Cell::new(Size::new(0.0, 0.0)));
134 let probe = seen.clone();
135 let lb = LayoutBuilder::<()>::new(move |size| {
136 probe.set(size);
137 Container::<()>::new().color(Color::rgb(1.0, 0.0, 0.0))
138 })
139 .width(320.0)
140 .height(48.0);
141
142 let _ui = build_ui(
143 &lb,
144 Size::new(800.0, 600.0),
145 &Runtime::default(),
146 &Theme::default(),
147 );
148 assert_eq!(seen.get(), Size::new(320.0, 48.0));
150 }
151
152 #[test]
156 fn it_is_as_big_as_what_it_built() {
157 let lb = LayoutBuilder::<()>::new(|_| {
158 Container::<()>::new()
159 .height(60.0)
160 .color(Color::rgb(1.0, 0.0, 0.0))
161 });
162 let root = crate::Flex::<()>::column()
163 .width(200.0)
164 .height(400.0)
165 .child(lb);
166 let ui = build_ui(
167 &root,
168 Size::new(200.0, 400.0),
169 &Runtime::default(),
170 &Theme::default(),
171 );
172 let painted = ui
173 .scene()
174 .primitives()
175 .iter()
176 .find_map(|p| match p {
177 Primitive::Rect { rect, color, .. } if color.r > 0.9 && color.g < 0.1 => {
178 Some(*rect)
179 }
180 _ => None,
181 })
182 .expect("the built content is painted");
183 assert_eq!(
184 painted.height, 60.0,
185 "the box is its content's: {painted:?}"
186 );
187 }
188
189 #[test]
192 fn what_it_built_pushes_its_siblings_down() {
193 let column = crate::Flex::<()>::column()
194 .width(200.0)
195 .height(400.0)
196 .child(LayoutBuilder::<()>::new(|_| {
197 Container::<()>::new().height(60.0)
198 }))
199 .child(
200 Container::<()>::new()
201 .height(20.0)
202 .color(Color::rgb(0.0, 1.0, 0.0)),
203 );
204 let ui = build_ui(
205 &column,
206 Size::new(200.0, 400.0),
207 &Runtime::default(),
208 &Theme::default(),
209 );
210 let sibling = ui
211 .scene()
212 .primitives()
213 .iter()
214 .find_map(|p| match p {
215 Primitive::Rect { rect, color, .. } if color.g > 0.9 && color.r < 0.1 => {
216 Some(*rect)
217 }
218 _ => None,
219 })
220 .expect("the sibling is painted");
221 assert_eq!(sibling.y, 60.0, "it starts below the built content");
222 }
223
224 #[test]
227 fn a_pinned_axis_is_still_the_style_s() {
228 let lb = LayoutBuilder::<()>::new(|_| {
229 Container::<()>::new()
230 .height(60.0)
231 .color(Color::rgb(1.0, 0.0, 0.0))
232 })
233 .height(120.0);
234 let root = crate::Flex::<()>::column()
235 .width(200.0)
236 .height(400.0)
237 .child(lb)
238 .child(
239 Container::<()>::new()
240 .height(20.0)
241 .color(Color::rgb(0.0, 1.0, 0.0)),
242 );
243 let ui = build_ui(
244 &root,
245 Size::new(200.0, 400.0),
246 &Runtime::default(),
247 &Theme::default(),
248 );
249 let sibling = ui
250 .scene()
251 .primitives()
252 .iter()
253 .find_map(|p| match p {
254 Primitive::Rect { rect, color, .. } if color.g > 0.9 && color.r < 0.1 => {
255 Some(*rect)
256 }
257 _ => None,
258 })
259 .expect("the sibling is painted");
260 assert_eq!(sibling.y, 120.0, "the style's 120, not the content's 60");
261 }
262
263 #[test]
264 fn adapts_content_to_available_width() {
265 let count_tiles = |window_w: f32| {
267 let lb = LayoutBuilder::<()>::new(|size| {
268 let tiles = match SizeClass::from_width(size.width) {
269 SizeClass::Compact => 1,
270 SizeClass::Medium => 2,
271 SizeClass::Expanded => 3,
272 };
273 let mut row = crate::Flex::<()>::row();
274 for _ in 0..tiles {
275 row = row.child(
276 Container::<()>::new()
277 .width(10.0)
278 .height(10.0)
279 .color(Color::WHITE),
280 );
281 }
282 row
283 })
284 .height(20.0);
285 let root = crate::Flex::<()>::column()
288 .width(window_w)
289 .height(200.0)
290 .child(lb);
291 let ui = build_ui(
292 &root,
293 Size::new(window_w, 200.0),
294 &Runtime::default(),
295 &Theme::default(),
296 );
297 ui.scene()
298 .primitives()
299 .iter()
300 .filter(|p| matches!(p, Primitive::Rect { color, .. } if *color == Color::WHITE))
301 .count()
302 };
303 assert_eq!(count_tiles(400.0), 1);
304 assert_eq!(count_tiles(700.0), 2);
305 assert_eq!(count_tiles(1000.0), 3);
306 }
307}