feather_ui/layout/
grid.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, map_unsized_area,
6    nuetralize_unsized,
7};
8use crate::{DPoint, DValue, PxDim, PxRect, RowDirection, SourceID, UNSIZED_AXIS, rtree};
9use std::rc::Rc;
10
11// TODO: use sparse vectors here? Does that even make sense if rows require a
12// default size of some kind?
13pub trait Prop: base::Area + base::Limits + base::Anchor + base::Padding + base::Direction {
14    fn rows(&self) -> &[DValue];
15    fn columns(&self) -> &[DValue];
16    fn spacing(&self) -> DPoint; // Spacing is specified as (row, column)
17}
18
19crate::gen_from_to_dyn!(Prop);
20
21pub trait Child: base::RLimits {
22    /// (Column, Row) coordinate of the item
23    fn coord(&self) -> (usize, usize);
24    /// (Column, Row) span of the item, lets items span across multiple rows or
25    /// columns. Minimum is (1,1), and the layout won't save you if you tell
26    /// it to overlap items.
27    fn span(&self) -> (usize, usize);
28}
29
30crate::gen_from_to_dyn!(Child);
31
32fn swap_coord((x, y): (usize, usize), (w, h): (usize, usize), dir: RowDirection) -> (usize, usize) {
33    match dir {
34        RowDirection::LeftToRight => (x, y),
35        RowDirection::RightToLeft => (w - 1 - x, y),
36        RowDirection::BottomToTop => (x, h - 1 - y),
37        RowDirection::TopToBottom => (w - 1 - x, h - 1 - y), /* TODO: This is confusing, but it's
38                                                              * not clear how to handle this
39                                                              * without being verbose or
40                                                              * confusing. */
41    }
42}
43
44impl Desc for dyn Prop {
45    type Props = dyn Prop;
46    type Child = dyn Child;
47    type Children = im::Vector<Option<Box<dyn Layout<Self::Child>>>>;
48
49    fn stage<'a>(
50        props: &Self::Props,
51        outer_area: crate::PxRect,
52        outer_limits: crate::PxLimits,
53        children: &Self::Children,
54        id: std::sync::Weak<SourceID>,
55        renderable: Option<Rc<dyn Renderable>>,
56        window: &mut crate::component::window::WindowState,
57    ) -> Box<dyn Staged + 'a> {
58        let mut limits = outer_limits + props.limits().resolve(window.dpi);
59        let myarea = props.area().resolve(window.dpi);
60        let (unsized_x, unsized_y) = check_unsized(myarea);
61        let padding = props.padding().as_perimeter(window.dpi);
62        let allpadding = padding.topleft() + padding.bottomright();
63        let minmax = limits.v.as_array_mut();
64        if unsized_x {
65            minmax[2] -= allpadding.width;
66            minmax[0] -= allpadding.width;
67        }
68        if unsized_y {
69            minmax[3] -= allpadding.height;
70            minmax[1] -= allpadding.height;
71        }
72
73        let outer_safe = nuetralize_unsized(outer_area);
74        let inner_dim = super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits)
75            - padding.topleft()
76            - padding.bottomright();
77
78        //let (outer_column, outer_row) = ;
79
80        let spacing = props.spacing().resolve(window.dpi) * outer_safe.dim();
81        let nrows = props.rows().len();
82        let ncolumns = props.columns().len();
83
84        let mut staging: im::Vector<Option<Box<dyn Staged>>> = im::Vector::new();
85        let mut nodes: im::Vector<Option<Rc<rtree::Node>>> = im::Vector::new();
86
87        let evaluated_area =
88            crate::util::alloca_array::<f32, PxRect>((nrows + ncolumns) * 2, |x| {
89                let (resolved, sizes) = x.split_at_mut(nrows + ncolumns);
90                {
91                    let (rows, columns) = resolved.split_at_mut(nrows);
92
93                    // Fill our max calculation rows with NANs (this ensures max()/min() behave
94                    // properly)
95                    sizes.fill(f32::NAN);
96
97                    let (maxrows, maxcolumns) = sizes.split_at_mut(nrows);
98
99                    // First we precalculate all row/column sizes that we can (if an outer axis is
100                    // unsized, relative sizes are set to 0)
101                    for (i, row) in props.rows().iter().enumerate() {
102                        rows[i] = row.resolve(window.dpi.height).resolve(inner_dim.height);
103                    }
104                    for (i, column) in props.columns().iter().enumerate() {
105                        columns[i] = column.resolve(window.dpi.width).resolve(inner_dim.width);
106                    }
107
108                    // Then we go through all child elements so we can precalculate the maximum area
109                    // of all rows and columns
110                    for child in children.iter() {
111                        let child_props = child.as_ref().unwrap().get_props();
112                        let child_limit =
113                            super::apply_limit(inner_dim, limits, *child_props.rlimits());
114                        let (column, row) =
115                            swap_coord(child_props.coord(), (ncolumns, nrows), props.direction());
116
117                        if rows[row] == UNSIZED_AXIS || columns[column] == UNSIZED_AXIS {
118                            let (w, h) = (columns[column], rows[row]);
119                            let child_area = PxRect::new(0.0, 0.0, w, h);
120
121                            let stage =
122                                child
123                                    .as_ref()
124                                    .unwrap()
125                                    .stage(child_area, child_limit, window);
126                            let area = stage.get_area();
127                            maxrows[row] = maxrows[row].max(area.dim().height);
128                            maxcolumns[column] = maxcolumns[column].max(area.dim().width);
129                        }
130                    }
131                }
132
133                // Copy back our resolved row or column to any unsized ones
134                for (i, size) in sizes.iter().enumerate() {
135                    if resolved[i] == UNSIZED_AXIS {
136                        resolved[i] = if size.is_nan() { 0.0 } else { *size };
137                    }
138                }
139                let (rows, columns) = resolved.split_at_mut(nrows);
140                let (x_used, y_used) = (
141                    columns.iter().fold(0.0, |x, y| x + y)
142                        + (spacing.y * ncolumns.saturating_sub(1) as f32),
143                    rows.iter().fold(0.0, |x, y| x + y)
144                        + (spacing.x * nrows.saturating_sub(1) as f32),
145                );
146                let area = map_unsized_area(myarea, PxDim::new(x_used, y_used));
147
148                // Calculate the offset to each row or column, without overwriting the size we
149                // stored in resolved
150                let (row_offsets, column_offsets) = sizes.split_at_mut(nrows);
151                let mut offset = 0.0;
152
153                for (i, row) in rows.iter().enumerate() {
154                    row_offsets[i] = offset;
155                    offset += row + spacing.x;
156                }
157
158                offset = 0.0;
159                for (i, column) in columns.iter().enumerate() {
160                    column_offsets[i] = offset;
161                    offset += column + spacing.y;
162                }
163
164                for child in children.iter() {
165                    let child_props = child.as_ref().unwrap().get_props();
166                    let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits());
167                    let (column, row) =
168                        swap_coord(child_props.coord(), (ncolumns, nrows), props.direction());
169
170                    let (x, y) = (column_offsets[column], row_offsets[row]);
171                    let (w, h) = (columns[column], rows[row]);
172                    let child_area = PxRect::new(x, y, x + w, y + h);
173
174                    let stage = child
175                        .as_ref()
176                        .unwrap()
177                        .stage(child_area, child_limit, window);
178                    if let Some(node) = stage.get_rtree().upgrade() {
179                        nodes.push_back(Some(node));
180                    }
181                    staging.push_back(Some(stage));
182                }
183
184                // No need to cap this because unsized axis have now been resolved
185                let evaluated_area = super::limit_area(area * outer_safe, limits) + padding;
186
187                let anchor = props.anchor().resolve(window.dpi) * evaluated_area.dim();
188                evaluated_area - anchor
189            });
190
191        debug_assert!(evaluated_area.v.is_finite().all());
192        Box::new(Concrete {
193            area: evaluated_area,
194            renderable,
195            rtree: rtree::Node::new(evaluated_area.to_untyped(), None, nodes, id, window),
196            children: staging,
197            layer: None,
198        })
199    }
200}