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::{BoxSizing, 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        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)]
156pub 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
162where
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`.
202pub 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
261pub 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")]
277pub mod detailed_info {
278    #[cfg(feature = "grid")]
279    pub use super::grid::DetailedGridInfo;
280}
281
282#[cfg(test)]
283mod 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]
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}