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