Block stretch sizing fixes (#674)
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 scrollbar_size = Size {
117 width: if style.overflow.y == Overflow::Scroll { style.scrollbar_width } else { 0.0 },
118 height: if style.overflow.x == Overflow::Scroll { style.scrollbar_width } else { 0.0 },
119 };
120
121 tree.set_unrounded_layout(
122 root,
123 &Layout {
124 order: 0,
125 location: Point::ZERO,
126 size: output.size,
127 #[cfg(feature = "content_size")]
128 content_size: output.content_size,
129 scrollbar_size,
130 padding,
131 border,
132 },
133 );
134}
135
136/// Attempts to find a cached layout for the specified node and layout inputs.
137///
138/// Uses the provided closure to compute the layout (and then stores the result in the cache) if no cached layout is found.
139#[inline(always)]
140pub fn compute_cached_layout<Tree: LayoutPartialTree + ?Sized, ComputeFunction>(
141 tree: &mut Tree,
142 node: NodeId,
143 inputs: LayoutInput,
144 mut compute_uncached: ComputeFunction,
145) -> LayoutOutput
146where
147 ComputeFunction: FnMut(&mut Tree, NodeId, LayoutInput) -> LayoutOutput,
148{
149 debug_push_node!(node);
150 let LayoutInput { known_dimensions, available_space, run_mode, .. } = inputs;
151
152 // First we check if we have a cached result for the given input
153 let cache_entry = tree.get_cache_mut(node).get(known_dimensions, available_space, run_mode);
154 if let Some(cached_size_and_baselines) = cache_entry {
155 debug_log_node!(known_dimensions, inputs.parent_size, available_space, run_mode, inputs.sizing_mode);
156 debug_log!("RESULT (CACHED)", dbg:cached_size_and_baselines.size);
157 debug_pop_node!();
158 return cached_size_and_baselines;
159 }
160
161 debug_log_node!(known_dimensions, inputs.parent_size, available_space, run_mode, inputs.sizing_mode);
162
163 let computed_size_and_baselines = compute_uncached(tree, node, inputs);
164
165 // Cache result
166 tree.get_cache_mut(node).store(known_dimensions, available_space, run_mode, computed_size_and_baselines);
167
168 debug_log!("RESULT", dbg:computed_size_and_baselines.size);
169 debug_pop_node!();
170
171 computed_size_and_baselines
172}
173
174/// Rounds the calculated layout to exact pixel values
175///
176/// In order to ensure that no gaps in the layout are introduced we:
177/// - Always round based on the cumulative x/y coordinates (relative to the viewport) rather than
178/// parent-relative coordinates
179/// - Compute width/height by first rounding the top/bottom/left/right and then computing the difference
180/// rather than rounding the width/height directly
181///
182/// See <https://github.com/facebook/yoga/commit/aa5b296ac78f7a22e1aeaf4891243c6bb76488e2> for more context
183///
184/// In order to prevent innacuracies caused by rounding already-rounded values, we read from `unrounded_layout`
185/// and write to `final_layout`.
186pub fn round_layout(tree: &mut impl RoundTree, node_id: NodeId) {
187 return round_layout_inner(tree, node_id, 0.0, 0.0);
188
189 /// Recursive function to apply rounding to all descendents
190 fn round_layout_inner(tree: &mut impl RoundTree, node_id: NodeId, cumulative_x: f32, cumulative_y: f32) {
191 let unrounded_layout = *tree.get_unrounded_layout(node_id);
192 let mut layout = unrounded_layout;
193
194 let cumulative_x = cumulative_x + unrounded_layout.location.x;
195 let cumulative_y = cumulative_y + unrounded_layout.location.y;
196
197 layout.location.x = round(unrounded_layout.location.x);
198 layout.location.y = round(unrounded_layout.location.y);
199 layout.size.width = round(cumulative_x + unrounded_layout.size.width) - round(cumulative_x);
200 layout.size.height = round(cumulative_y + unrounded_layout.size.height) - round(cumulative_y);
201 layout.scrollbar_size.width = round(unrounded_layout.scrollbar_size.width);
202 layout.scrollbar_size.height = round(unrounded_layout.scrollbar_size.height);
203 layout.border.left = round(cumulative_x + unrounded_layout.border.left) - round(cumulative_x);
204 layout.border.right = round(cumulative_x + unrounded_layout.size.width)
205 - round(cumulative_x + unrounded_layout.size.width - unrounded_layout.border.right);
206 layout.border.top = round(cumulative_y + unrounded_layout.border.top) - round(cumulative_y);
207 layout.border.bottom = round(cumulative_y + unrounded_layout.size.height)
208 - round(cumulative_y + unrounded_layout.size.height - unrounded_layout.border.bottom);
209 layout.padding.left = round(cumulative_x + unrounded_layout.padding.left) - round(cumulative_x);
210 layout.padding.right = round(cumulative_x + unrounded_layout.size.width)
211 - round(cumulative_x + unrounded_layout.size.width - unrounded_layout.padding.right);
212 layout.padding.top = round(cumulative_y + unrounded_layout.padding.top) - round(cumulative_y);
213 layout.padding.bottom = round(cumulative_y + unrounded_layout.size.height)
214 - round(cumulative_y + unrounded_layout.size.height - unrounded_layout.padding.bottom);
215
216 #[cfg(feature = "content_size")]
217 round_content_size(&mut layout, unrounded_layout.content_size, cumulative_x, cumulative_y);
218
219 tree.set_final_layout(node_id, &layout);
220
221 let child_count = tree.child_count(node_id);
222 for index in 0..child_count {
223 let child = tree.get_child_id(node_id, index);
224 round_layout_inner(tree, child, cumulative_x, cumulative_y);
225 }
226 }
227
228 #[cfg(feature = "content_size")]
229 #[inline(always)]
230 /// Round content size variables.
231 /// This is split into a separate function to make it easier to feature flag.
232 fn round_content_size(
233 layout: &mut Layout,
234 unrounded_content_size: Size<f32>,
235 cumulative_x: f32,
236 cumulative_y: f32,
237 ) {
238 layout.content_size.width = round(cumulative_x + unrounded_content_size.width) - round(cumulative_x);
239 layout.content_size.height = round(cumulative_y + unrounded_content_size.height) - round(cumulative_y);
240 }
241}
242
243/// Creates a layout for this node and its children, recursively.
244/// Each hidden node has zero size and is placed at the origin
245pub fn compute_hidden_layout(tree: &mut impl LayoutPartialTree, node: NodeId) -> LayoutOutput {
246 // Clear cache and set zeroed-out layout for the node
247 tree.get_cache_mut(node).clear();
248 tree.set_unrounded_layout(node, &Layout::with_order(0));
249
250 // Perform hidden layout on all children
251 for index in 0..tree.child_count(node) {
252 let child_id = tree.get_child_id(node, index);
253 tree.compute_child_layout(child_id, LayoutInput::HIDDEN);
254 }
255
256 LayoutOutput::HIDDEN
257}
258
259#[cfg(test)]
260mod tests {
261 use super::compute_hidden_layout;
262 use crate::geometry::{Point, Size};
263 use crate::style::{Display, Style};
264 use crate::TaffyTree;
265
266 #[test]
267 fn hidden_layout_should_hide_recursively() {
268 let mut taffy: TaffyTree<()> = TaffyTree::new();
269
270 let style: Style = Style { display: Display::Flex, size: Size::from_lengths(50.0, 50.0), ..Default::default() };
271
272 let grandchild_00 = taffy.new_leaf(style.clone()).unwrap();
273 let grandchild_01 = taffy.new_leaf(style.clone()).unwrap();
274 let child_00 = taffy.new_with_children(style.clone(), &[grandchild_00, grandchild_01]).unwrap();
275
276 let grandchild_02 = taffy.new_leaf(style.clone()).unwrap();
277 let child_01 = taffy.new_with_children(style.clone(), &[grandchild_02]).unwrap();
278
279 let root = taffy
280 .new_with_children(
281 Style { display: Display::None, size: Size::from_lengths(50.0, 50.0), ..Default::default() },
282 &[child_00, child_01],
283 )
284 .unwrap();
285
286 compute_hidden_layout(&mut taffy.as_layout_tree(), root.into());
287
288 // Whatever size and display-mode the nodes had previously,
289 // all layouts should resolve to ZERO due to the root's DISPLAY::NONE
290
291 for node in [root, child_00, child_01, grandchild_00, grandchild_01, grandchild_02] {
292 let layout = taffy.layout(node.into()).unwrap();
293 assert_eq!(layout.size, Size::zero());
294 assert_eq!(layout.location, Point::zero());
295 }
296 }
297}