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}