taffy/compute/
mod.rs

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