gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1061HTML 997Markdown 12TOML 83120 Sutichaykuna

⎇ 0.5.x src/compute/mod.rs Rust · 297 siq'ikuna · 15.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

Block stretch sizing fixes (#674) 9651a18b7 · Nico Burns ·

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}