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