1 //! Low-level access to the layout algorithms themselves. For a higher-level API, see the [`TaffyTree`](crate::TaffyTree) struct.
2 //!
3 //! ### Layout functions
4 //!
5 //! The layout functions all take an [`&mut impl LayoutPartialTree`](crate::LayoutPartialTree) parameter, which represents a single container node and it's direct children.
6 //!
7 //! | Function | Purpose |
8 //! | --- | --- |
9 //! | [`compute_flexbox_layout`] | Layout a Flexbox container and it's direct children |
10 //! | [`compute_grid_layout`] | Layout a CSS Grid container and it's direct children |
11 //! | [`compute_block_layout`] | Layout a Block container and it's direct children |
12 //! | [`compute_leaf_layout`] | Applies common properties like padding/border/aspect-ratio to a node before deferring to a passed closure to determine it's size. Can be applied to nodes like text or image nodes. |
13 //! | [`compute_root_layout`] | Layout the root node of a tree (regardless of it's layout mode). This function is typically called once to begin a layout run. | |
14 //! | [`compute_hidden_layout`] | Mark a node as hidden during layout (like `Display::None`) |
15 //! | [`compute_cached_layout`] | Attempts to find a cached layout for the specified node and layout inputs. Uses the provided closure to compute the layout (and then stores the result in the cache) if no cached layout is found. |
16 //!
17 //! ### Other functions
18 //!
19 //! | Function | Requires | Purpose |
20 //! | --- | --- | --- |
21 //! | [`round_layout`] | [`RoundTree`] | Round a tree of float-valued layouts to integer pixels |
22 //! | [`print_tree`](crate::print_tree) | [`PrintTree`](crate::PrintTree) | Print a debug representation of a node tree and it's computed layout |
23 //!
24 pub(crate) mod common;
25 pub(crate) mod leaf;
26
27 #[cfg(feature = "block_layout")]
28 pub(crate) mod block;
29
30 #[cfg(feature = "flexbox")]
31 pub(crate) mod flexbox;
32
33 #[cfg(feature = "grid")]
34 pub(crate) mod grid;
35
36 pub use leaf::compute_leaf_layout;
37
38 #[cfg(feature = "block_layout")]
39 pub use self::block::compute_block_layout;
40
41 #[cfg(feature = "flexbox")]
42 pub use self::flexbox::compute_flexbox_layout;
43
44 #[cfg(feature = "grid")]
45 pub use self::grid::compute_grid_layout;
46
47 use crate::geometry::{Line, Point, Size};
48 use crate::style::{AvailableSpace, CoreStyle, Overflow};
49 use crate::tree::{
50 Layout, LayoutInput, LayoutOutput, LayoutPartialTree, LayoutPartialTreeExt, NodeId, RoundTree, SizingMode,
51 };
52 use crate::util::debug::{debug_log, debug_log_node, debug_pop_node, debug_push_node};
53 use crate::util::sys::round;
54 use crate::util::ResolveOrZero;
55 use crate::{BoxSizing, CacheTree, MaybeMath, MaybeResolve};
56
57 /// Compute layout for the root node in the tree
compute_root_layout(tree: &mut impl LayoutPartialTree, root: NodeId, available_space: Size<AvailableSpace>)58 pub fn compute_root_layout(tree: &mut impl LayoutPartialTree, root: NodeId, available_space: Size<AvailableSpace>) {
59 let mut known_dimensions = Size::NONE;
60
61 #[cfg(feature = "block_layout")]
62 {
63 let parent_size = available_space.into_options();
64 let style = tree.get_core_container_style(root);
65
66 if style.is_block() {
67 // Pull these out earlier to avoid borrowing issues
68 let aspect_ratio = style.aspect_ratio();
69 let margin = style.margin().resolve_or_zero(parent_size.width);
70 let padding = style.padding().resolve_or_zero(parent_size.width);
71 let border = style.border().resolve_or_zero(parent_size.width);
72 let padding_border_size = (padding + border).sum_axes();
73 let box_sizing_adjustment =
74 if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO };
75
76 let min_size = style
77 .min_size()
78 .maybe_resolve(parent_size)
79 .maybe_apply_aspect_ratio(aspect_ratio)
80 .maybe_add(box_sizing_adjustment);
81 let max_size = style
82 .max_size()
83 .maybe_resolve(parent_size)
84 .maybe_apply_aspect_ratio(aspect_ratio)
85 .maybe_add(box_sizing_adjustment);
86 let clamped_style_size = style
87 .size()
88 .maybe_resolve(parent_size)
89 .maybe_apply_aspect_ratio(aspect_ratio)
90 .maybe_add(box_sizing_adjustment)
91 .maybe_clamp(min_size, max_size);
92
93 // If both min and max in a given axis are set and max <= min then this determines the size in that axis
94 let min_max_definite_size = min_size.zip_map(max_size, |min, max| match (min, max) {
95 (Some(min), Some(max)) if max <= min => Some(min),
96 _ => None,
97 });
98
99 // Block nodes automatically stretch fit their width to fit available space if available space is definite
100 let available_space_based_size = Size {
101 width: available_space.width.into_option().maybe_sub(margin.horizontal_axis_sum()),
102 height: None,
103 };
104
105 let styled_based_known_dimensions = known_dimensions
106 .or(min_max_definite_size)
107 .or(clamped_style_size)
108 .or(available_space_based_size)
109 .maybe_max(padding_border_size);
110
111 known_dimensions = styled_based_known_dimensions;
112 }
113 }
114
115 // Recursively compute node layout
116 let output = tree.perform_child_layout(
117 root,
118 known_dimensions,
119 available_space.into_options(),
120 available_space,
121 SizingMode::InherentSize,
122 Line::FALSE,
123 );
124
125 let style = tree.get_core_container_style(root);
126 let padding = style.padding().resolve_or_zero(available_space.width.into_option());
127 let border = style.border().resolve_or_zero(available_space.width.into_option());
128 let margin = style.margin().resolve_or_zero(available_space.width.into_option());
129 let scrollbar_size = Size {
130 width: if style.overflow().y == Overflow::Scroll { style.scrollbar_width() } else { 0.0 },
131 height: if style.overflow().x == Overflow::Scroll { style.scrollbar_width() } else { 0.0 },
132 };
133 drop(style);
134
135 tree.set_unrounded_layout(
136 root,
137 &Layout {
138 order: 0,
139 location: Point::ZERO,
140 size: output.size,
141 #[cfg(feature = "content_size")]
142 content_size: output.content_size,
143 scrollbar_size,
144 padding,
145 border,
146 // TODO: support auto margins for root node?
147 margin,
148 },
149 );
150 }
151
152 /// Attempts to find a cached layout for the specified node and layout inputs.
153 ///
154 /// Uses the provided closure to compute the layout (and then stores the result in the cache) if no cached layout is found.
155 #[inline(always)]
compute_cached_layout<Tree: CacheTree + ?Sized, ComputeFunction>( tree: &mut Tree, node: NodeId, inputs: LayoutInput, mut compute_uncached: ComputeFunction, ) -> LayoutOutput where ComputeFunction: FnMut(&mut Tree, NodeId, LayoutInput) -> LayoutOutput,156 pub fn compute_cached_layout<Tree: CacheTree + ?Sized, ComputeFunction>(
157 tree: &mut Tree,
158 node: NodeId,
159 inputs: LayoutInput,
160 mut compute_uncached: ComputeFunction,
161 ) -> LayoutOutput
162 where
163 ComputeFunction: FnMut(&mut Tree, NodeId, LayoutInput) -> LayoutOutput,
164 {
165 debug_push_node!(node);
166 let LayoutInput { known_dimensions, available_space, run_mode, .. } = inputs;
167
168 // First we check if we have a cached result for the given input
169 let cache_entry = tree.cache_get(node, known_dimensions, available_space, run_mode);
170 if let Some(cached_size_and_baselines) = cache_entry {
171 debug_log_node!(known_dimensions, inputs.parent_size, available_space, run_mode, inputs.sizing_mode);
172 debug_log!("RESULT (CACHED)", dbg:cached_size_and_baselines.size);
173 debug_pop_node!();
174 return cached_size_and_baselines;
175 }
176
177 debug_log_node!(known_dimensions, inputs.parent_size, available_space, run_mode, inputs.sizing_mode);
178
179 let computed_size_and_baselines = compute_uncached(tree, node, inputs);
180
181 // Cache result
182 tree.cache_store(node, known_dimensions, available_space, run_mode, computed_size_and_baselines);
183
184 debug_log!("RESULT", dbg:computed_size_and_baselines.size);
185 debug_pop_node!();
186
187 computed_size_and_baselines
188 }
189
190 /// Rounds the calculated layout to exact pixel values
191 ///
192 /// In order to ensure that no gaps in the layout are introduced we:
193 /// - Always round based on the cumulative x/y coordinates (relative to the viewport) rather than
194 /// parent-relative coordinates
195 /// - Compute width/height by first rounding the top/bottom/left/right and then computing the difference
196 /// rather than rounding the width/height directly
197 ///
198 /// See <https://github.com/facebook/yoga/commit/aa5b296ac78f7a22e1aeaf4891243c6bb76488e2> for more context
199 ///
200 /// In order to prevent innacuracies caused by rounding already-rounded values, we read from `unrounded_layout`
201 /// and write to `final_layout`.
round_layout(tree: &mut impl RoundTree, node_id: NodeId)202 pub fn round_layout(tree: &mut impl RoundTree, node_id: NodeId) {
203 return round_layout_inner(tree, node_id, 0.0, 0.0);
204
205 /// Recursive function to apply rounding to all descendents
206 fn round_layout_inner(tree: &mut impl RoundTree, node_id: NodeId, cumulative_x: f32, cumulative_y: f32) {
207 let unrounded_layout = *tree.get_unrounded_layout(node_id);
208 let mut layout = unrounded_layout;
209
210 let cumulative_x = cumulative_x + unrounded_layout.location.x;
211 let cumulative_y = cumulative_y + unrounded_layout.location.y;
212
213 layout.location.x = round(unrounded_layout.location.x);
214 layout.location.y = round(unrounded_layout.location.y);
215 layout.size.width = round(cumulative_x + unrounded_layout.size.width) - round(cumulative_x);
216 layout.size.height = round(cumulative_y + unrounded_layout.size.height) - round(cumulative_y);
217 layout.scrollbar_size.width = round(unrounded_layout.scrollbar_size.width);
218 layout.scrollbar_size.height = round(unrounded_layout.scrollbar_size.height);
219 layout.border.left = round(cumulative_x + unrounded_layout.border.left) - round(cumulative_x);
220 layout.border.right = round(cumulative_x + unrounded_layout.size.width)
221 - round(cumulative_x + unrounded_layout.size.width - unrounded_layout.border.right);
222 layout.border.top = round(cumulative_y + unrounded_layout.border.top) - round(cumulative_y);
223 layout.border.bottom = round(cumulative_y + unrounded_layout.size.height)
224 - round(cumulative_y + unrounded_layout.size.height - unrounded_layout.border.bottom);
225 layout.padding.left = round(cumulative_x + unrounded_layout.padding.left) - round(cumulative_x);
226 layout.padding.right = round(cumulative_x + unrounded_layout.size.width)
227 - round(cumulative_x + unrounded_layout.size.width - unrounded_layout.padding.right);
228 layout.padding.top = round(cumulative_y + unrounded_layout.padding.top) - round(cumulative_y);
229 layout.padding.bottom = round(cumulative_y + unrounded_layout.size.height)
230 - round(cumulative_y + unrounded_layout.size.height - unrounded_layout.padding.bottom);
231
232 #[cfg(feature = "content_size")]
233 round_content_size(&mut layout, unrounded_layout.content_size, cumulative_x, cumulative_y);
234
235 tree.set_final_layout(node_id, &layout);
236
237 let child_count = tree.child_count(node_id);
238 for index in 0..child_count {
239 let child = tree.get_child_id(node_id, index);
240 round_layout_inner(tree, child, cumulative_x, cumulative_y);
241 }
242 }
243
244 #[cfg(feature = "content_size")]
245 #[inline(always)]
246 /// Round content size variables.
247 /// This is split into a separate function to make it easier to feature flag.
248 fn round_content_size(
249 layout: &mut Layout,
250 unrounded_content_size: Size<f32>,
251 cumulative_x: f32,
252 cumulative_y: f32,
253 ) {
254 layout.content_size.width = round(cumulative_x + unrounded_content_size.width) - round(cumulative_x);
255 layout.content_size.height = round(cumulative_y + unrounded_content_size.height) - round(cumulative_y);
256 }
257 }
258
259 /// Creates a layout for this node and its children, recursively.
260 /// Each hidden node has zero size and is placed at the origin
compute_hidden_layout(tree: &mut (impl LayoutPartialTree + CacheTree), node: NodeId) -> LayoutOutput261 pub fn compute_hidden_layout(tree: &mut (impl LayoutPartialTree + CacheTree), node: NodeId) -> LayoutOutput {
262 // Clear cache and set zeroed-out layout for the node
263 tree.cache_clear(node);
264 tree.set_unrounded_layout(node, &Layout::with_order(0));
265
266 // Perform hidden layout on all children
267 for index in 0..tree.child_count(node) {
268 let child_id = tree.get_child_id(node, index);
269 tree.compute_child_layout(child_id, LayoutInput::HIDDEN);
270 }
271
272 LayoutOutput::HIDDEN
273 }
274
275 /// A module for unified re-exports of detailed layout info structs, used by low level API
276 #[cfg(feature = "detailed_layout_info")]
277 pub mod detailed_info {
278 #[cfg(feature = "grid")]
279 pub use super::grid::DetailedGridInfo;
280 }
281
282 #[cfg(test)]
283 mod tests {
284 use super::compute_hidden_layout;
285 use crate::geometry::{Point, Size};
286 use crate::style::{Display, Style};
287 use crate::TaffyTree;
288
289 #[test]
hidden_layout_should_hide_recursively()290 fn hidden_layout_should_hide_recursively() {
291 let mut taffy: TaffyTree<()> = TaffyTree::new();
292
293 let style: Style = Style { display: Display::Flex, size: Size::from_lengths(50.0, 50.0), ..Default::default() };
294
295 let grandchild_00 = taffy.new_leaf(style.clone()).unwrap();
296 let grandchild_01 = taffy.new_leaf(style.clone()).unwrap();
297 let child_00 = taffy.new_with_children(style.clone(), &[grandchild_00, grandchild_01]).unwrap();
298
299 let grandchild_02 = taffy.new_leaf(style.clone()).unwrap();
300 let child_01 = taffy.new_with_children(style.clone(), &[grandchild_02]).unwrap();
301
302 let root = taffy
303 .new_with_children(
304 Style { display: Display::None, size: Size::from_lengths(50.0, 50.0), ..Default::default() },
305 &[child_00, child_01],
306 )
307 .unwrap();
308
309 compute_hidden_layout(&mut taffy.as_layout_tree(), root);
310
311 // Whatever size and display-mode the nodes had previously,
312 // all layouts should resolve to ZERO due to the root's DISPLAY::NONE
313
314 for node in [root, child_00, child_01, grandchild_00, grandchild_01, grandchild_02] {
315 let layout = taffy.layout(node).unwrap();
316 assert_eq!(layout.size, Size::zero());
317 assert_eq!(layout.location, Point::zero());
318 }
319 }
320 }
321