gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1090HTML 1022Markdown 12TOML 84418 Definitions

⎇ 477ff4aff498cbae4cdfbf0cda1ee5b04c784f08 src/compute/mod.rs Rust · 300 lines · 15.3 KB History Who touched what raw

Output computed margins in Layout (#678) 41fe221c1 · 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 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}