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