iris_ui/
layouts.rs

1use crate::LayoutEvent;
2use crate::geom::{Insets, Size};
3use crate::view::Align::{Center, End, Start};
4use crate::view::Flex::Resize;
5use crate::view::{Flex, ViewId};
6use Flex::Intrinsic;
7use log::info;
8
9pub fn layout_vbox(pass: &mut LayoutEvent) {
10    let Some(parent) = pass.scene.get_view_mut(&pass.target) else {
11        info!("view not found!");
12        return;
13    };
14    let h_flex = parent.h_flex.clone();
15    let padding = parent.padding.clone();
16    let mut available_space: Size = pass.space - parent.padding;
17
18    // get the intrinsic children
19    let fixed_kids = pass
20        .scene
21        .get_children_ids_filtered(&pass.target, |v| v.v_flex == Intrinsic);
22    // lay out the intrinsic children
23    for kid in &fixed_kids {
24        pass.layout_child(kid, available_space);
25    }
26
27    // calculate total used height
28    let kids_sum = fixed_kids.iter().fold(0, |a, id| {
29        return if let Some(view) = pass.scene.get_view_mut(id) {
30            view.bounds.size.h + a
31        } else {
32            a
33        };
34    });
35    let vert_leftover = (pass.space - padding).h - kids_sum;
36
37    // layout the flex children
38    let flex_kids = pass
39        .scene
40        .get_children_ids_filtered(&pass.target, |v| v.v_flex == Flex::Resize);
41    if flex_kids.len() > 0 {
42        let flex_space = Size {
43            w: pass.space.w - padding.left - padding.right,
44            h: vert_leftover / (flex_kids.len() as i32),
45        };
46        for kid in flex_kids {
47            pass.layout_child(&kid, flex_space);
48        }
49    }
50
51    // calculate the max width of any child
52    let mut max_width = 0;
53    for kid in pass.scene.get_children_ids(&pass.target) {
54        if let Some(kid) = pass.scene.get_view_mut(&kid) {
55            max_width = max_width.max(kid.bounds.size.w);
56        }
57    }
58    if h_flex == Intrinsic {
59        available_space.w = max_width;
60    }
61
62    // position all the children
63    let mut y = padding.top;
64    let all_kids = pass.scene.get_children_ids(&pass.target);
65    let avail_w = available_space.w;
66    for kid in all_kids {
67        if let Some(kid) = pass.scene.get_view_mut(&kid) {
68            kid.bounds.position.x = match &kid.h_align {
69                Start => 0,
70                Center => (avail_w - kid.bounds.size.w) / 2,
71                End => (avail_w - kid.bounds.size.w),
72            } + padding.left;
73            kid.bounds.position.y = y;
74            y += kid.bounds.size.h;
75        }
76    }
77    // layout self
78    if let Some(view) = pass.scene.get_view_mut(&pass.target) {
79        if view.h_flex == Resize {
80            view.bounds.size.w = pass.space.w
81        }
82        if view.h_flex == Intrinsic {
83            view.bounds.size.w = max_width + padding.left + padding.right
84        }
85        if view.v_flex == Resize {
86            view.bounds.size.h = pass.space.h
87        }
88        if view.v_flex == Intrinsic {}
89    }
90}
91
92pub fn layout_hbox(pass: &mut LayoutEvent) {
93    let Some(parent) = pass.scene.get_view_mut(&pass.target) else {
94        return;
95    };
96    let h_flex = parent.h_flex.clone();
97    let v_flex = parent.v_flex.clone();
98    // layout self
99    if v_flex == Resize {
100        parent.bounds.size.h = pass.space.h
101    }
102    if h_flex == Resize {
103        parent.bounds.size.w = pass.space.w
104    }
105
106    let padding = parent.padding.clone();
107    let mut available_space = pass.space - padding;
108
109    // get the fixed children
110    let fixed_kids = pass
111        .scene
112        .get_children_ids_filtered(&pass.target, |v| v.h_flex == Intrinsic);
113
114    // layout the fixed width children
115    for kid in &fixed_kids {
116        pass.layout_child(kid, available_space);
117    }
118
119    // calc the total width of the fixed kids
120    let kids_sum: i32 = fixed_kids
121        .iter()
122        .map(|id| pass.scene.get_view(id))
123        .flatten()
124        .fold(0, |a, v| v.bounds.size.w + a);
125    let avail_horizontal_space = (available_space - padding).h - kids_sum;
126
127    // get the flex children
128    let flex_kids = pass
129        .scene
130        .get_children_ids_filtered(&pass.target, |v| v.h_flex == Flex::Resize);
131    // if there are any flex children
132    if flex_kids.len() > 0 {
133        // split the leftover space
134        let flex_space = Size {
135            w: avail_horizontal_space / (flex_kids.len() as i32),
136            h: pass.space.h - padding.top - padding.bottom,
137        };
138        // layout the flex children
139        for kid in &flex_kids {
140            pass.layout_child(kid, flex_space);
141        }
142    }
143
144    // calculate the max height of any child
145    let mut max_height = 0;
146    for kid in pass.scene.get_children_ids(&pass.target) {
147        if let Some(kid) = pass.scene.get_view_mut(&kid) {
148            max_height = max_height.max(kid.bounds.size.h);
149        }
150    }
151
152    // now position all children
153    if v_flex == Intrinsic {
154        available_space.h = max_height;
155    }
156    let avail_h = available_space.h;
157    let mut x = padding.left;
158    for kid in pass.scene.get_children_ids(&pass.target) {
159        if let Some(kid) = pass.scene.get_view_mut(&kid) {
160            kid.bounds.position.x = x;
161            x += kid.bounds.size.w;
162            kid.bounds.position.y = match &kid.v_align {
163                Start => 0,
164                Center => (avail_h - kid.bounds.size.h) / 2,
165                End => (avail_h - kid.bounds.size.h),
166            } + padding.top;
167        }
168    }
169    if let Some(parent) = pass.scene.get_view_mut(pass.target) {
170        if parent.v_flex == Intrinsic {
171            parent.bounds.size.h = available_space.h + padding.top + padding.bottom;
172        }
173        if parent.h_flex == Intrinsic {
174            parent.bounds.size.w = x;
175        }
176    }
177}
178
179pub fn layout_std_panel(pass: &mut LayoutEvent) {
180    if let Some(view) = pass.scene.get_view_mut(&pass.target) {
181        if view.v_flex == Resize {
182            view.bounds.size.h = pass.space.h;
183        }
184        if view.h_flex == Resize {
185            view.bounds.size.w = pass.space.w;
186        }
187        let space = view.bounds.size.clone() - view.padding;
188        pass.layout_all_children(&pass.target.clone(), space);
189    }
190}
191
192pub fn layout_tabbed_panel(pass: &mut LayoutEvent) {
193    if let Some(view) = pass.scene.get_view_mut(&pass.target) {
194        // layout self
195        if view.h_flex == Resize {
196            view.bounds.size.w = pass.space.w;
197        }
198        if view.v_flex == Resize {
199            view.bounds.size.h = pass.space.h;
200        }
201
202        // layout tabs
203        let space = view.bounds.size.clone();
204        let tabs_id: ViewId = "tabs".into();
205        pass.layout_child(&tabs_id, space);
206
207        // layout content panels
208        if let Some(view) = pass.scene.get_view(&tabs_id) {
209            let insets = Insets::new(view.bounds.size.h, 0, 0, 0);
210            for kid in &pass.scene.get_children_ids(&pass.target) {
211                if kid == &tabs_id {
212                    continue;
213                }
214                pass.layout_child(kid, space - insets);
215                if let Some(view) = pass.scene.get_view_mut(kid) {
216                    view.bounds.position.y = insets.top;
217                }
218            }
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use crate::LayoutEvent;
226    use crate::geom::{Bounds, Insets, Point, Size};
227    use crate::layouts::{layout_hbox, layout_std_panel, layout_tabbed_panel, layout_vbox};
228    use crate::scene::{Scene, layout_scene};
229    use crate::test::MockDrawingContext;
230    use crate::toggle_group::layout_toggle_group;
231    use crate::view::Align::{Center, End, Start};
232    use crate::view::{Align, Flex, View, ViewId};
233    use test_log::test;
234    fn layout_button(layout: &mut LayoutEvent) {
235        if let Some(view) = layout.scene.get_view_mut(&layout.target) {
236            view.bounds.size = Size::new((view.title.len() * 10) as i32, 10) + view.padding;
237        }
238    }
239    #[test]
240    fn test_button() {
241        let button = View {
242            name: "button1".into(),
243            title: "abc".into(),
244            layout: Some(layout_button),
245            padding: Insets::new_same(10),
246            ..Default::default()
247        };
248
249        let theme = MockDrawingContext::make_mock_theme();
250        let mut scene = Scene::new();
251        scene.add_view_to_parent(button, &scene.root_id());
252        layout_scene(&mut scene, &theme);
253        // size = 3 letters x 10x10 font + 10px padding
254        assert_eq!(
255            view_bounds(&scene, &"button1".into()).size,
256            Size::new(3 * 10 + 20, 10 + 20),
257            "button size is wrong"
258        );
259    }
260
261    fn view_bounds(scene: &Scene, name: &ViewId) -> Bounds {
262        if let Some(view) = scene.get_view(name) {
263            view.bounds
264        } else {
265            Bounds::new(-99, -99, -99, -99)
266        }
267    }
268
269    #[test]
270    fn test_vbox() {
271        let mut scene = Scene::new();
272        let parent_id: ViewId = "parent".into();
273        let parent_view = View {
274            name: parent_id.clone(),
275            title: "parent".into(),
276            padding: Insets::new_same(10),
277            bounds: Bounds {
278                position: Point::new(-99, -99),
279                size: Size::new(100, 100),
280            },
281            h_flex: Flex::Resize,
282            v_flex: Flex::Resize,
283            h_align: Start,
284            v_align: Start,
285            layout: Some(layout_vbox),
286            ..Default::default()
287        };
288
289        let child1_id: ViewId = "child1".into();
290        scene.add_view_to_parent(
291            View {
292                name: child1_id.clone(),
293                title: "ch1".into(),
294                h_align: Align::Start,
295                layout: Some(layout_button),
296                ..Default::default()
297            },
298            &parent_id,
299        );
300
301        let child2_id: ViewId = "child2".into();
302        scene.add_view_to_parent(
303            View {
304                name: child2_id.clone(),
305                title: "ch2".into(),
306                h_align: Align::Center,
307                layout: Some(layout_button),
308                ..Default::default()
309            },
310            &parent_id,
311        );
312
313        let child3_id: ViewId = "child3".into();
314        scene.add_view_to_parent(
315            View {
316                name: child3_id.clone(),
317                title: "ch3".into(),
318                h_align: Align::End,
319                layout: Some(layout_button),
320                ..Default::default()
321            },
322            &parent_id,
323        );
324
325        let child4_id: ViewId = "child4".into();
326        scene.add_view_to_parent(
327            View {
328                name: child4_id.clone(),
329                title: "ch4".into(),
330                h_flex: Flex::Resize,
331                v_flex: Flex::Resize,
332                layout: Some(layout_std_panel),
333                ..Default::default()
334            },
335            &parent_id,
336        );
337
338        scene.add_view_to_parent(parent_view, &scene.root_id());
339
340        let theme = MockDrawingContext::make_mock_theme();
341        layout_scene(&mut scene, &theme);
342        scene.dump();
343        if let Some(view) = scene.get_view_mut(&parent_id) {
344            assert_eq!(view.name, parent_id);
345            // confirm position wasn't modified at all
346            assert_eq!(view.bounds.position, Point::new(-99, -99));
347            // size = scene size of 200x200
348            assert_eq!(view.bounds.size, Size::new(200, 200));
349            // left align
350            if let Some(view) = scene.get_view(&child1_id) {
351                assert_eq!(view.bounds.position, Point::new(10, 10));
352                assert_eq!(view.bounds.size, Size::new(30, 10));
353            }
354            // center align
355            if let Some(view) = scene.get_view(&child2_id) {
356                assert_eq!(view.bounds.position, Point::new(10 + (180 - 30) / 2, 20));
357                assert_eq!(view.bounds.size, Size::new(30, 10));
358            }
359            // right align
360            if let Some(view) = scene.get_view(&child3_id) {
361                assert_eq!(view.bounds.position, Point::new(10 + (180 - 30), 30));
362                assert_eq!(view.bounds.size, Size::new(30, 10));
363            }
364            // should fill rest of the space
365            assert!(scene.has_view(&child4_id));
366            if let Some(view) = scene.get_view(&child4_id) {
367                assert_eq!(view.bounds.position, Point::new(10, 40));
368                assert_eq!(view.bounds.size, Size::new(180, 180 - 30));
369            }
370        }
371    }
372
373    #[test]
374    fn test_complex_tabbed_panels() {
375        let mut scene = Scene::new();
376        // tab panel test
377        let tabbed_panel: ViewId = "tabbed_panel_id".into();
378        let tabs: ViewId = "tabs".into();
379        {
380            let mut tabbed_panel_view: View = View {
381                name: tabbed_panel.clone(),
382                ..Default::default()
383            };
384            tabbed_panel_view.h_flex = Flex::Resize;
385            tabbed_panel_view.v_flex = Flex::Resize;
386            tabbed_panel_view.layout = Some(layout_tabbed_panel);
387            scene.add_view_to_parent(tabbed_panel_view, &scene.root_id());
388
389            // tab panel tabs has intrisic height but flex width
390            let mut tabbed_panel_tabs: View = View {
391                name: tabs.clone(),
392                ..Default::default()
393            };
394            tabbed_panel_tabs.h_flex = Flex::Resize;
395            tabbed_panel_tabs.v_flex = Flex::Intrinsic;
396            tabbed_panel_tabs.layout = Some(layout_toggle_group);
397            scene.add_view_to_parent(tabbed_panel_tabs, &tabbed_panel);
398        }
399
400        // has three tabs contents
401        // tab panel contents are panels that grow to fill space
402        // first tab panel is an hbox with three buttons vertically centered and left aligned
403        let tab1: ViewId = "tab1".into();
404        {
405            let mut view = make_standard_view(&tab1);
406            view.h_flex = Flex::Resize;
407            view.v_flex = Flex::Resize;
408            view.layout = Some(layout_hbox);
409            scene.add_view_to_parent(view, &tabbed_panel);
410
411            let b1: ViewId = "tab1_button1".into();
412            {
413                let mut button = make_standard_view(&b1);
414                button.title = "abc".into();
415                button.v_align = Start;
416                button.layout = Some(layout_button);
417                scene.add_view_to_parent(button, &tab1);
418            }
419
420            let b2: ViewId = "tab1_button2".into();
421            {
422                let mut button = make_standard_view(&b2);
423                button.v_align = Center;
424                button.layout = Some(layout_button);
425                scene.add_view_to_parent(button, &tab1);
426            }
427
428            let b3: ViewId = "tab1_button3".into();
429            {
430                let mut button = make_standard_view(&b3);
431                button.v_align = End;
432                button.layout = Some(layout_button);
433                scene.add_view_to_parent(button, &tab1);
434            }
435        }
436
437        // second tab panel is a vbox with three buttons horizontally centered
438        // and the first one takes all vertical space and horizontal space
439        let tab2: ViewId = "tab2".into();
440        {
441            let mut view = make_standard_view(&tab2);
442            view.h_flex = Flex::Resize;
443            view.v_flex = Flex::Resize;
444            view.layout = Some(layout_vbox);
445            scene.add_view_to_parent(view, &tabbed_panel);
446
447            let b1: ViewId = "tab2_button1".into();
448            {
449                let mut b1_view = make_standard_view(&b1);
450                b1_view.h_align = Start;
451                b1_view.h_flex = Flex::Resize;
452                b1_view.v_flex = Flex::Resize;
453                b1_view.title = "b11".into();
454                b1_view.layout = Some(layout_std_panel);
455                scene.add_view_to_parent(b1_view, &tab2);
456            }
457
458            let b2: ViewId = "tab2_button2".into();
459            {
460                let mut b2_view = make_standard_view(&b2);
461                b2_view.h_align = End;
462                b2_view.h_flex = Flex::Intrinsic;
463                b2_view.v_flex = Flex::Intrinsic;
464                b2_view.title = "b11".into();
465                b2_view.layout = Some(layout_button);
466                scene.add_view_to_parent(b2_view, &tab2);
467            }
468        }
469
470        // third tab panel lets children be absolutely positioned and sizes self to the center with a fixed width and height of 100
471        let tab3: ViewId = "tab3".into();
472        {
473            let mut view = make_standard_view(&tab3);
474            view.h_flex = Flex::Resize;
475            view.v_flex = Flex::Resize;
476            view.layout = Some(layout_std_panel);
477            scene.add_view_to_parent(view, &tabbed_panel);
478        }
479
480        // tab panel is 200 x 200px to fill the root
481        let scene_size = Size::new(200, 200);
482
483        let theme = MockDrawingContext::make_mock_theme();
484        layout_scene(&mut scene, &theme);
485        scene.dump();
486        assert_eq!(
487            scene.get_view_bounds(&scene.root_id()),
488            Some(Bounds::new(0, 0, 200, 200))
489        );
490        assert_eq!(
491            scene.get_view_bounds(&tabbed_panel),
492            Some(Bounds::new(0, 0, 200, 200))
493        );
494        assert_eq!(
495            scene.get_view_bounds(&tabs),
496            Some(Bounds::new(0, 0, 200, 20))
497        );
498        assert_eq!(
499            scene.get_view_bounds(&tab1),
500            Some(Bounds::new(0, 20, 200, 180))
501        );
502        assert_eq!(
503            scene.get_view_bounds(&tab2),
504            Some(Bounds::new(0, 20, 200, 180))
505        );
506        assert_eq!(
507            scene.get_view_bounds(&tab3),
508            Some(Bounds::new(0, 20, 200, 180))
509        );
510
511        assert_eq!(
512            scene.get_view_bounds(&"tab1_button1".into()),
513            Some(Bounds::new(0, 0, 30, 10))
514        );
515        assert_eq!(
516            scene.get_view_bounds(&"tab2_button1".into()),
517            Some(Bounds::new(0, 0, 200, 170))
518        );
519        assert_eq!(
520            scene.get_view_bounds(&"tab2_button2".into()),
521            Some(Bounds::new(170, 170, 30, 10))
522        );
523    }
524
525    fn make_standard_view(name: &ViewId) -> View {
526        View {
527            name: name.clone(),
528            ..Default::default()
529        }
530    }
531}