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}