feather_ui/layout/
list.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use super::{
5    Concrete, Desc, Layout, Renderable, Staged, base, check_unsized_abs, map_unsized_area,
6    nuetralize_unsized,
7};
8use crate::{PxDim, PxPoint, PxRect, RowDirection, SourceID, rtree};
9use std::rc::Rc;
10
11pub trait Prop: base::Area + base::Limits + base::Direction {}
12
13crate::gen_from_to_dyn!(Prop);
14
15pub trait Child: base::RLimits + base::Margin + base::Order {}
16
17crate::gen_from_to_dyn!(Child);
18
19impl Desc for dyn Prop {
20    type Props = dyn Prop;
21    type Child = dyn Child;
22    // TODO: Make a sorted im::Vector that uses base::Order to order inserted children.
23    type Children = im::Vector<Option<Box<dyn Layout<Self::Child>>>>;
24
25    fn stage<'a>(
26        props: &Self::Props,
27        outer_area: PxRect,
28        outer_limits: crate::PxLimits,
29        children: &Self::Children,
30        id: std::sync::Weak<SourceID>,
31        renderable: Option<Rc<dyn Renderable>>,
32        window: &mut crate::component::window::WindowState,
33    ) -> Box<dyn Staged + 'a> {
34        use super::Swappable;
35        // TODO: make insertion efficient by creating a RRB tree of list layout subnodes, in a similar manner to the r-tree nodes.
36
37        let limits = outer_limits + props.limits().resolve(window.dpi);
38        let myarea = props.area().resolve(window.dpi);
39        //let (unsized_x, unsized_y) = super::check_unsized(*myarea);
40        let dir = props.direction();
41        // For the purpose of calculating size, we only care about which axis we're distributing along
42        let xaxis = match dir {
43            RowDirection::LeftToRight | RowDirection::RightToLeft => true,
44            RowDirection::TopToBottom | RowDirection::BottomToTop => false,
45        };
46
47        // Even if both axis are sized, we have to precalculate the areas and margins anyway.
48        let inner_dim = super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits);
49        let outer_safe = nuetralize_unsized(outer_area);
50        // The inner_dim must preserve whether an axis is unsized, but the actual limits must be respected regardless.
51        let (main_limit, _) = inner_dim.min(limits.max()).swap_axis(xaxis);
52
53        // This should eventually be a persistent fold
54        let mut aux_margins: im::Vector<f32> = im::Vector::new();
55        let mut areas: im::Vector<Option<(PxRect, f32)>> = im::Vector::new();
56
57        let area = {
58            let mut cur = PxPoint::zero();
59            let mut max_main = 0.0;
60            let mut max_aux: f32 = 0.0;
61            let mut prev_margin = f32::NAN;
62            let mut aux_margin: f32 = 0.0;
63            let mut aux_margin_bottom = f32::NAN;
64            let mut prev_aux_margin = f32::NAN;
65            let inner_area = PxRect::from(inner_dim);
66
67            for child in children.iter() {
68                let child_props = child.as_ref().unwrap().get_props();
69                let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits());
70                let child_margin = child_props
71                    .margin()
72                    .resolve(window.dpi)
73                    .to_perimeter(outer_safe);
74
75                let stage = child
76                    .as_ref()
77                    .unwrap()
78                    .stage(inner_area, child_limit, window);
79                let area = stage.get_area();
80
81                let (margin_main, child_margin_aux) = child_margin.topleft().swap_axis(xaxis);
82                let (main, aux) = area.dim().swap_axis(xaxis);
83                let mut margin = super::merge_margin(prev_margin, margin_main);
84                // Have to add the margin here before we zero it
85                areas.push_back(Some((area, margin)));
86
87                if !prev_margin.is_nan() && cur.x + main + margin > main_limit {
88                    max_main = cur.x.max(max_main);
89                    cur.x = 0.0;
90                    let aux_merge = super::merge_margin(prev_aux_margin, aux_margin);
91                    aux_margins.push_back(aux_merge);
92                    cur.y += max_aux + aux_merge;
93                    max_aux = 0.0;
94                    margin = 0.0;
95                    aux_margin = 0.0;
96                    prev_aux_margin = aux_margin_bottom;
97                    aux_margin_bottom = f32::NAN;
98                }
99
100                cur.x += main + margin;
101                aux_margin = aux_margin.max(child_margin_aux);
102                max_aux = max_aux.max(aux);
103
104                let (margin, child_margin_aux) = child_margin.bottomright().swap_axis(xaxis);
105                prev_margin = margin;
106                aux_margin_bottom = aux_margin_bottom.max(child_margin_aux);
107            }
108
109            // Final bounds calculations
110            max_main = cur.x.max(max_main);
111            let aux_merge = super::merge_margin(prev_aux_margin, aux_margin);
112            aux_margins.push_back(aux_merge);
113            cur.y += max_aux + aux_margin;
114            let (bounds_x, bounds_y) = super::swap_pair(xaxis, (max_main, cur.y));
115            map_unsized_area(myarea, PxDim::new(bounds_x, bounds_y))
116        };
117
118        // No need to cap this because unsized axis have now been resolved
119        let evaluated_area = super::limit_area(area * outer_safe, limits);
120
121        let mut staging: im::Vector<Option<Box<dyn Staged>>> = im::Vector::new();
122        let mut nodes: im::Vector<Option<Rc<rtree::Node>>> = im::Vector::new();
123
124        // If our parent is asking for a size estimation along the expansion axis, no need to layout the children
125        // TODO: Double check this assumption is true
126        let (unsized_x, unsized_y) = check_unsized_abs(outer_area.bottomright());
127        if (unsized_x && xaxis) || (unsized_y && !xaxis) {
128            return Box::new(Concrete {
129                area: evaluated_area,
130                renderable: None,
131                rtree: rtree::Node::new(evaluated_area.to_untyped(), None, nodes, id, window),
132                children: staging,
133                layer: None,
134            });
135        }
136
137        let evaluated_dim = evaluated_area.dim();
138        let mut cur = match dir {
139            RowDirection::LeftToRight | RowDirection::TopToBottom => PxPoint::zero(),
140            RowDirection::RightToLeft => PxPoint::new(evaluated_dim.width, 0.0),
141            RowDirection::BottomToTop => PxPoint::new(0.0, evaluated_dim.height),
142        };
143        let mut maxaux: f32 = 0.0;
144        aux_margins.pop_front();
145        let mut aux_margin = aux_margins.pop_front().unwrap_or_default();
146
147        for (i, child) in children.iter().enumerate() {
148            let child = child.as_ref().unwrap();
149            let (area, margin) = areas[i].unwrap();
150            let dim = area.dim();
151            let (_, aux) = dim.swap_axis(xaxis);
152
153            match dir {
154                RowDirection::RightToLeft => {
155                    if cur.x - dim.width - margin < 0.0 {
156                        cur.y += maxaux + aux_margin;
157                        aux_margin = aux_margins.pop_front().unwrap_or_default();
158                        maxaux = 0.0;
159                        cur.x = evaluated_dim.width - dim.width;
160                    } else {
161                        cur.x -= dim.width + margin
162                    }
163                }
164                RowDirection::BottomToTop => {
165                    if cur.y - dim.height - margin < 0.0 {
166                        cur.x += maxaux + aux_margin;
167                        aux_margin = aux_margins.pop_front().unwrap_or_default();
168                        maxaux = 0.0;
169                        cur.y = evaluated_dim.height - dim.height;
170                    } else {
171                        cur.y -= dim.height + margin
172                    }
173                }
174                RowDirection::LeftToRight => {
175                    if cur.x + dim.width + margin > evaluated_dim.width {
176                        cur.y += maxaux + aux_margin;
177                        aux_margin = aux_margins.pop_front().unwrap_or_default();
178                        maxaux = 0.0;
179                        cur.x = 0.0;
180                    } else {
181                        cur.x += margin;
182                    }
183                }
184                RowDirection::TopToBottom => {
185                    if cur.y + dim.height + margin > evaluated_dim.height {
186                        cur.x += maxaux + aux_margin;
187                        aux_margin = aux_margins.pop_front().unwrap_or_default();
188                        maxaux = 0.0;
189                        cur.y = 0.0;
190                    } else {
191                        cur.y += margin;
192                    }
193                }
194            };
195
196            let child_area = area + cur;
197
198            match dir {
199                RowDirection::LeftToRight => cur.x += dim.width,
200                RowDirection::TopToBottom => cur.y += dim.height,
201                _ => (),
202            };
203            maxaux = maxaux.max(aux);
204
205            let child_limit = *child.get_props().rlimits() * evaluated_dim;
206
207            let stage = child.stage(child_area, child_limit, window);
208            if let Some(node) = stage.get_rtree().upgrade() {
209                nodes.push_back(Some(node));
210            }
211            staging.push_back(Some(stage));
212        }
213
214        Box::new(Concrete {
215            area: evaluated_area,
216            renderable,
217            rtree: rtree::Node::new(evaluated_area.to_untyped(), None, nodes, id, window),
218            children: staging,
219            layer: None,
220        })
221    }
222}