Output computed margins in Layout (#678)
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//!
24pub(crate) mod common;
25pub(crate) mod leaf;
26
27#[cfg(feature = "block_layout")]
28pub(crate) mod block;
29
30#[cfg(feature = "flexbox")]
31pub(crate) mod flexbox;
32
33#[cfg(feature = "grid")]
34pub(crate) mod grid;
35
36pub use leaf::compute_leaf_layout;
37
38#[cfg(feature = "block_layout")]
39pub use self::block::compute_block_layout;
40
41#[cfg(feature = "flexbox")]
42pub use self::flexbox::compute_flexbox_layout;
43
44#[cfg(feature = "grid")]
45pub use self::grid::compute_grid_layout;
46
47use crate::geometry::{Line, Point, Size};
48use crate::style::{AvailableSpace, Overflow};
49use crate::tree::{
50 Layout, LayoutInput, LayoutOutput, LayoutPartialTree, LayoutPartialTreeExt, NodeId, RoundTree, SizingMode,
51};
52use crate::util::debug::{debug_log, debug_log_node, debug_pop_node, debug_push_node};
53use crate::util::sys::round;
54use crate::util::ResolveOrZero;
55use crate::{Display, MaybeMath, MaybeResolve};
56
57/// Compute layout for the root node in the tree
58pub 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_style(root);
65
66 if style.display == Display::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 min_size = style.min_size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio);
71 let max_size = style.max_size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio);
72 let padding = style.padding.resolve_or_zero(parent_size.width);
73 let border = style.border.resolve_or_zero(parent_size.width);
74 let padding_border_size = (padding + border).sum_axes();
75 let clamped_style_size = style
76 .size
77 .maybe_resolve(parent_size)
78 .maybe_apply_aspect_ratio(aspect_ratio)
79 .maybe_clamp(min_size, max_size);
80
81 // If both min and max in a given axis are set and max <= min then this determines the size in that axis
82 let min_max_definite_size = min_size.zip_map(max_size, |min, max| match (min, max) {
83 (Some(min), Some(max)) if max <= min => Some(min),
84 _ => None,
85 });
86
87 // Block nodes automatically stretch fit their width to fit available space if available space is definite
88 let available_space_based_size = Size {
89 width: available_space.width.into_option().maybe_sub(margin.horizontal_axis_sum()),
90 height: None,
91 };
92
93 let styled_based_known_dimensions = known_dimensions
94 .or(min_max_definite_size)
95 .or(clamped_style_size)
96 .or(available_space_based_size)
97 .maybe_max(padding_border_size);
98
99 known_dimensions = styled_based_known_dimensions;
100 }
101 }
102
103 // Recursively compute node layout
104 let output = tree.perform_child_layout(
105 root,
106 known_dimensions,
107 available_space.into_options(),
108 available_space,
109 SizingMode::InherentSize,
110 Line::FALSE,
111 );
112
113 let style = tree.get_style(root);
114 let padding = style.padding.resolve_or_zero(available_space.width.into_option());
115 let border = style.border.resolve_or_zero(available_space.width.into_option());
116 let margin = style.margin.resolve_or_zero(available_space.width.into_option());
117 let scrollbar_size = Size {
118 width: if style.overflow.y == Overflow::Scroll { style.scrollbar_width } else { 0.0 },
119 height: if style.overflow.x == Overflow::Scroll { style.scrollbar_width } else { 0.0 },
120 };
121
122 tree.set_unrounded_layout(
123 root,
124 &Layout {
125 order: 0,
126 location: Point::ZERO,
127 size: output.size,
128 #[cfg(feature = "content_size")]
129 content_size: output.content_size,
130 scrollbar_size,
131 padding,
132 border,
133 // TODO: support auto margins for root node?
134 margin,
135 },
136 );
137}
138
139/// Attempts to find a cached layout for the specified node and layout inputs.
140///
141/// Uses the provided closure to compute the layout (and then stores the result in the cache) if no cached layout is found.
142#[inline(always)]
143pub fn compute_cached_layout<Tree: LayoutPartialTree + ?Sized, ComputeFunction>(
144 tree: &mut Tree,
145 node: NodeId,
146 inputs: LayoutInput,
147 mut compute_uncached: ComputeFunction,
148) -> LayoutOutput
149where
150 ComputeFunction: FnMut(&mut Tree, NodeId, LayoutInput) -> LayoutOutput,
151{
152 debug_push_node!(node);
153 let LayoutInput { known_dimensions, available_space, run_mode, .. } = inputs;
154
155 // First we check if we have a cached result for the given input
156 let cache_entry = tree.get_cache_mut(node).get(known_dimensions, available_space, run_mode);
157 if let Some(cached_size_and_baselines) = cache_entry {
158 debug_log_node!(known_dimensions, inputs.parent_size, available_space, run_mode, inputs.sizing_mode);
159 debug_log!("RESULT (CACHED)", dbg:cached_size_and_baselines.size);
160 debug_pop_node!();
161 return cached_size_and_baselines;
162 }
163
164 debug_log_node!(known_dimensions, inputs.parent_size, available_space, run_mode, inputs.sizing_mode);
165
166 let computed_size_and_baselines = compute_uncached(tree, node, inputs);
167
168 // Cache result
169 tree.get_cache_mut(node).store(known_dimensions, available_space, run_mode, computed_size_and_baselines);
170
171 debug_log!("RESULT", dbg:computed_size_and_baselines.size);
172 debug_pop_node!();
173
174 computed_size_and_baselines
175}
176
177/// Rounds the calculated layout to exact pixel values
178///
179/// In order to ensure that no gaps in the layout are introduced we:
180/// - Always round based on the cumulative x/y coordinates (relative to the viewport) rather than
181/// parent-relative coordinates
182/// - Compute width/height by first rounding the top/bottom/left/right and then computing the difference
183/// rather than rounding the width/height directly
184///
185/// See <https://github.com/facebook/yoga/commit/aa5b296ac78f7a22e1aeaf4891243c6bb76488e2> for more context
186///
187/// In order to prevent innacuracies caused by rounding already-rounded values, we read from `unrounded_layout`
188/// and write to `final_layout`.
189pub fn round_layout(tree: &mut impl RoundTree, node_id: NodeId) {
190 return round_layout_inner(tree, node_id, 0.0, 0.0);
191
192 /// Recursive function to apply rounding to all descendents
193 fn round_layout_inner(tree: &mut impl RoundTree, node_id: NodeId, cumulative_x: f32, cumulative_y: f32) {
194 let unrounded_layout = *tree.get_unrounded_layout(node_id);
195 let mut layout = unrounded_layout;
196
197 let cumulative_x = cumulative_x + unrounded_layout.location.x;
198 let cumulative_y = cumulative_y + unrounded_layout.location.y;
199
200 layout.location.x = round(unrounded_layout.location.x);
201 layout.location.y = round(unrounded_layout.location.y);
202 layout.size.width = round(cumulative_x + unrounded_layout.size.width) - round(cumulative_x);
203 layout.size.height = round(cumulative_y + unrounded_layout.size.height) - round(cumulative_y);
204 layout.scrollbar_size.width = round(unrounded_layout.scrollbar_size.width);
205 layout.scrollbar_size.height = round(unrounded_layout.scrollbar_size.height);
206 layout.border.left = round(cumulative_x + unrounded_layout.border.left) - round(cumulative_x);
207 layout.border.right = round(cumulative_x + unrounded_layout.size.width)
208 - round(cumulative_x + unrounded_layout.size.width - unrounded_layout.border.right);
209 layout.border.top = round(cumulative_y + unrounded_layout.border.top) - round(cumulative_y);
210 layout.border.bottom = round(cumulative_y + unrounded_layout.size.height)
211 - round(cumulative_y + unrounded_layout.size.height - unrounded_layout.border.bottom);
212 layout.padding.left = round(cumulative_x + unrounded_layout.padding.left) - round(cumulative_x);
213 layout.padding.right = round(cumulative_x + unrounded_layout.size.width)
214 - round(cumulative_x + unrounded_layout.size.width - unrounded_layout.padding.right);
215 layout.padding.top = round(cumulative_y + unrounded_layout.padding.top) - round(cumulative_y);
216 layout.padding.bottom = round(cumulative_y + unrounded_layout.size.height)
217 - round(cumulative_y + unrounded_layout.size.height - unrounded_layout.padding.bottom);
218
219 #[cfg(feature = "content_size")]
220 round_content_size(&mut layout, unrounded_layout.content_size, cumulative_x, cumulative_y);
221
222 tree.set_final_layout(node_id, &layout);
223
224 let child_count = tree.child_count(node_id);
225 for index in 0..child_count {
226 let child = tree.get_child_id(node_id, index);
227 round_layout_inner(tree, child, cumulative_x, cumulative_y);
228 }
229 }
230
231 #[cfg(feature = "content_size")]
232 #[inline(always)]
233 /// Round content size variables.
234 /// This is split into a separate function to make it easier to feature flag.
235 fn round_content_size(
236 layout: &mut Layout,
237 unrounded_content_size: Size<f32>,
238 cumulative_x: f32,
239 cumulative_y: f32,
240 ) {
241 layout.content_size.width = round(cumulative_x + unrounded_content_size.width) - round(cumulative_x);
242 layout.content_size.height = round(cumulative_y + unrounded_content_size.height) - round(cumulative_y);
243 }
244}
245
246/// Creates a layout for this node and its children, recursively.
247/// Each hidden node has zero size and is placed at the origin
248pub fn compute_hidden_layout(tree: &mut impl LayoutPartialTree, node: NodeId) -> LayoutOutput {
249 // Clear cache and set zeroed-out layout for the node
250 tree.get_cache_mut(node).clear();
251 tree.set_unrounded_layout(node, &Layout::with_order(0));
252
253 // Perform hidden layout on all children
254 for index in 0..tree.child_count(node) {
255 let child_id = tree.get_child_id(node, index);
256 tree.compute_child_layout(child_id, LayoutInput::HIDDEN);
257 }
258
259 LayoutOutput::HIDDEN
260}
261
262#[cfg(test)]
263mod tests {
264 use super::compute_hidden_layout;
265 use crate::geometry::{Point, Size};
266 use crate::style::{Display, Style};
267 use crate::TaffyTree;
268
269 #[test]
270 fn hidden_layout_should_hide_recursively() {
271 let mut taffy: TaffyTree<()> = TaffyTree::new();
272
273 let style: Style = Style { display: Display::Flex, size: Size::from_lengths(50.0, 50.0), ..Default::default() };
274
275 let grandchild_00 = taffy.new_leaf(style.clone()).unwrap();
276 let grandchild_01 = taffy.new_leaf(style.clone()).unwrap();
277 let child_00 = taffy.new_with_children(style.clone(), &[grandchild_00, grandchild_01]).unwrap();
278
279 let grandchild_02 = taffy.new_leaf(style.clone()).unwrap();
280 let child_01 = taffy.new_with_children(style.clone(), &[grandchild_02]).unwrap();
281
282 let root = taffy
283 .new_with_children(
284 Style { display: Display::None, size: Size::from_lengths(50.0, 50.0), ..Default::default() },
285 &[child_00, child_01],
286 )
287 .unwrap();
288
289 compute_hidden_layout(&mut taffy.as_layout_tree(), root.into());
290
291 // Whatever size and display-mode the nodes had previously,
292 // all layouts should resolve to ZERO due to the root's DISPLAY::NONE
293
294 for node in [root, child_00, child_01, grandchild_00, grandchild_01, grandchild_02] {
295 let layout = taffy.layout(node.into()).unwrap();
296 assert_eq!(layout.size, Size::zero());
297 assert_eq!(layout.location, Point::zero());
298 }
299 }
300}