feather_ui/layout/
fixed.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, check_unsized_abs,
6    map_unsized_area,
7};
8use crate::{PxDim, PxRect, rtree};
9use std::rc::Rc;
10
11pub trait Prop: base::Area + base::Anchor + base::Limits + base::ZIndex {}
12
13crate::gen_from_to_dyn!(Prop);
14
15pub trait Child: base::RLimits {}
16
17crate::gen_from_to_dyn!(Child);
18
19impl Prop for crate::DRect {}
20impl Child for crate::DRect {}
21
22impl Desc for dyn Prop {
23    type Props = dyn Prop;
24    type Child = dyn Child;
25    type Children = im::Vector<Option<Box<dyn Layout<Self::Child>>>>;
26
27    fn stage<'a>(
28        props: &Self::Props,
29        outer_area: PxRect,
30        outer_limits: crate::PxLimits,
31        children: &Self::Children,
32        id: std::sync::Weak<crate::SourceID>,
33        renderable: Option<Rc<dyn Renderable>>,
34        window: &mut crate::component::window::WindowState,
35    ) -> Box<dyn Staged + 'a> {
36        // If we have an unsized outer_area, any sized object with relative dimensions
37        // must evaluate to 0 (or to the minimum limited size). An
38        // unsized object can never have relative dimensions, as that creates a logic
39        // loop - instead it can only have a single relative anchor.
40        // If both axes are sized, then all limits are applied as if outer_area was
41        // unsized, and children calculations are skipped.
42        //
43        // If we have an unsized outer_area and an unsized myarea.rel, then limits are
44        // applied as if outer_area was unsized, and furthermore,
45        // they are reduced by myarea.abs.bottomright(), because that will be added on
46        // to the total area later, which will still be subject to size
47        // limits, so we must anticipate this when calculating how much size the
48        // children will have available to them. This forces limits to be
49        // true infinite numbers, so we can subtract finite amounts and still have
50        // infinity. We can't use infinity anywhere else, because infinity times
51        // zero is NaN, so we cap certain calculations at f32::MAX
52        //
53        // If outer_area is sized and myarea.rel is zero or nonzero, all limits are
54        // applied normally and child calculations are skipped. If outer_area is
55        // sized and myarea.rel is unsized, limits are applied normally, but are once
56        // again reduced by myarea.abs.bottomright() to account for how the area
57        // calculations will interact with the limits later on.
58
59        let limits = outer_limits + props.limits().resolve(window.dpi);
60        let myarea = props.area().resolve(window.dpi);
61        let (unsized_x, unsized_y) = check_unsized(myarea);
62
63        // Check if any axis is unsized in a way that requires us to calculate baseline
64        // child sizes
65        let evaluated_area = if unsized_x || unsized_y {
66            // When an axis is unsized, we don't apply any limits to it, so we don't have to
67            // worry about cases where the full evaluated area would invalidate
68            // the limit.
69            let inner_dim = super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits);
70            let inner_area = PxRect::from(inner_dim);
71            // The area we pass to children must be independent of our own area, so it
72            // starts at 0,0
73            let mut bottomright = PxDim::zero();
74
75            for child in children.iter() {
76                let child_props = child.as_ref().unwrap().get_props();
77                let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits());
78
79                let stage = child
80                    .as_ref()
81                    .unwrap()
82                    .stage(inner_area, child_limit, window);
83                bottomright = bottomright.max(stage.get_area().bottomright().to_vector().to_size());
84            }
85
86            let area = map_unsized_area(myarea, bottomright);
87
88            // No need to cap this because unsized axis have now been resolved
89            super::limit_area(area * crate::layout::nuetralize_unsized(outer_area), limits)
90        } else {
91            // If outer_area is unsized here, we nuetralize it when evaluating the relative
92            // coordinates.
93            super::limit_area(
94                myarea * crate::layout::nuetralize_unsized(outer_area),
95                limits,
96            )
97        };
98
99        let mut staging: im::Vector<Option<Box<dyn Staged>>> = im::Vector::new();
100        let mut nodes: im::Vector<Option<Rc<rtree::Node>>> = im::Vector::new();
101
102        // If our parent just wants a size estimate, no need to layout children or
103        // render anything
104        let (unsized_x, unsized_y) = check_unsized_abs(outer_area.bottomright());
105        if unsized_x || unsized_y {
106            return Box::new(Concrete::new(
107                None,
108                evaluated_area,
109                rtree::Node::new(
110                    evaluated_area.to_untyped(),
111                    Some(props.zindex()),
112                    nodes,
113                    id,
114                    window,
115                ),
116                staging,
117            ));
118        }
119
120        // We had to evaluate the full area first because our final area calculation can
121        // change the dimensions in unsized cases. Thus, we calculate the final
122        // inner_area for the children from this evaluated area.
123        let evaluated_dim = evaluated_area.dim();
124
125        let inner_area = PxRect::from(evaluated_dim);
126
127        for child in children.iter() {
128            let child_props = child.as_ref().unwrap().get_props();
129            let child_limit = *child_props.rlimits() * evaluated_dim;
130
131            let stage = child
132                .as_ref()
133                .unwrap()
134                .stage(inner_area, child_limit, window);
135            if let Some(node) = stage.get_rtree().upgrade() {
136                nodes.push_back(Some(node));
137            }
138            staging.push_back(Some(stage));
139        }
140
141        // TODO: It isn't clear if the simple layout should attempt to handle children
142        // changing their estimated sizes after the initial estimate. If we were
143        // to handle this, we would need to recalculate the unsized
144        // axis with the new child results here, and repeat until it stops changing (we
145        // find the fixed point). Because the performance implications are
146        // unclear, this might need to be relagated to a special layout.
147
148        // Calculate the anchor using the final evaluated dimensions, after all unsized
149        // axis and limits are calculated. However, we can only apply the anchor
150        // if the parent isn't unsized on that axis.
151        let mut anchor = props.anchor().resolve(window.dpi) * evaluated_dim;
152        let (unsized_outer_x, unsized_outer_y) =
153            crate::layout::check_unsized_abs(outer_area.bottomright());
154        if unsized_outer_x {
155            anchor.x = 0.0;
156        }
157        if unsized_outer_y {
158            anchor.y = 0.0;
159        }
160        let evaluated_area = evaluated_area - anchor;
161
162        Box::new(Concrete::new(
163            renderable,
164            evaluated_area,
165            rtree::Node::new(
166                evaluated_area.to_untyped(),
167                Some(props.zindex()),
168                nodes,
169                id,
170                window,
171            ),
172            staging,
173        ))
174    }
175}