Fix rounding accumulation bug (#521)
1//! Computation specific for the default `Taffy` tree implementation
2
3use crate::compute::{leaf, LayoutAlgorithm};
4use crate::geometry::{Line, Point, Size};
5use crate::style::{AvailableSpace, Display};
6use crate::tree::{Layout, LayoutTree, NodeId, RunMode, SizeBaselinesAndMargins, SizingMode, Taffy, TaffyError};
7use crate::util::sys::round;
8
9#[cfg(feature = "block_layout")]
10use crate::compute::BlockAlgorithm;
11
12#[cfg(feature = "flexbox")]
13use crate::compute::FlexboxAlgorithm;
14
15#[cfg(feature = "grid")]
16use crate::compute::CssGridAlgorithm;
17
18#[cfg(any(feature = "debug", feature = "profile"))]
19use crate::util::debug::NODE_LOGGER;
20
21#[cfg(feature = "debug")]
22fn debug_log_node(
23 known_dimensions: Size<Option<f32>>,
24 parent_size: Size<Option<f32>>,
25 available_space: Size<AvailableSpace>,
26 run_mode: RunMode,
27 sizing_mode: SizingMode,
28) {
29 NODE_LOGGER.debug_log(run_mode);
30 NODE_LOGGER.labelled_debug_log("sizing_mode", sizing_mode);
31 NODE_LOGGER.labelled_debug_log("known_dimensions", known_dimensions);
32 NODE_LOGGER.labelled_debug_log("parent_size", parent_size);
33 NODE_LOGGER.labelled_debug_log("available_space", available_space);
34}
35
36/// Updates the stored layout of the provided `node` and its children
37pub(crate) fn compute_layout(
38 taffy: &mut Taffy,
39 root: NodeId,
40 available_space: Size<AvailableSpace>,
41) -> Result<(), TaffyError> {
42 taffy.is_layouting = true;
43
44 // Recursively compute node layout
45 let size_and_baselines = perform_node_layout(
46 taffy,
47 root,
48 Size::NONE,
49 available_space.into_options(),
50 available_space,
51 SizingMode::InherentSize,
52 Line::FALSE,
53 );
54
55 let layout = Layout { order: 0, size: size_and_baselines.size, location: Point::ZERO };
56 *taffy.layout_mut(root) = layout;
57
58 // If rounding is enabled, recursively round the layout's of this node and all children
59 if taffy.config.use_rounding {
60 round_layout(taffy, root, 0.0, 0.0);
61 }
62
63 taffy.is_layouting = false;
64
65 Ok(())
66}
67
68/// Perform full layout on a node. Chooses which algorithm to use based on the `display` property.
69pub(crate) fn perform_node_layout(
70 tree: &mut Taffy,
71 node: NodeId,
72 known_dimensions: Size<Option<f32>>,
73 parent_size: Size<Option<f32>>,
74 available_space: Size<AvailableSpace>,
75 sizing_mode: SizingMode,
76 vertical_margins_are_collapsible: Line<bool>,
77) -> SizeBaselinesAndMargins {
78 compute_node_layout(
79 tree,
80 node,
81 known_dimensions,
82 parent_size,
83 available_space,
84 RunMode::PerformLayout,
85 sizing_mode,
86 vertical_margins_are_collapsible,
87 )
88}
89
90/// Measure a node's size. Chooses which algorithm to use based on the `display` property.
91pub(crate) fn measure_node_size(
92 tree: &mut Taffy,
93 node: NodeId,
94 known_dimensions: Size<Option<f32>>,
95 parent_size: Size<Option<f32>>,
96 available_space: Size<AvailableSpace>,
97 sizing_mode: SizingMode,
98 vertical_margins_are_collapsible: Line<bool>,
99) -> Size<f32> {
100 compute_node_layout(
101 tree,
102 node,
103 known_dimensions,
104 parent_size,
105 available_space,
106 RunMode::ComputeSize,
107 sizing_mode,
108 vertical_margins_are_collapsible,
109 )
110 .size
111}
112
113/// Updates the stored layout of the provided `node` and its children
114#[allow(clippy::too_many_arguments)]
115fn compute_node_layout(
116 tree: &mut Taffy,
117 node: NodeId,
118 known_dimensions: Size<Option<f32>>,
119 parent_size: Size<Option<f32>>,
120 available_space: Size<AvailableSpace>,
121 run_mode: RunMode,
122 sizing_mode: SizingMode,
123 vertical_margins_are_collapsible: Line<bool>,
124) -> SizeBaselinesAndMargins {
125 #[cfg(any(feature = "debug", feature = "profile"))]
126 NODE_LOGGER.push_node(node);
127 #[cfg(feature = "debug")]
128 println!();
129
130 let node_key = node.into();
131 let has_children = !tree.children[node_key].is_empty();
132
133 // First we check if we have a cached result for the given input
134 let cache_run_mode = if !has_children { RunMode::PerformLayout } else { run_mode };
135 if let Some(cached_size_and_baselines) =
136 tree.nodes[node_key].cache.get(known_dimensions, available_space, cache_run_mode)
137 {
138 #[cfg(feature = "debug")]
139 NODE_LOGGER.labelled_debug_log("CACHE", cached_size_and_baselines.size);
140 #[cfg(feature = "debug")]
141 debug_log_node(known_dimensions, parent_size, available_space, run_mode, sizing_mode);
142 #[cfg(any(feature = "debug", feature = "profile"))]
143 NODE_LOGGER.pop_node();
144
145 return cached_size_and_baselines;
146 }
147
148 #[cfg(feature = "debug")]
149 debug_log_node(known_dimensions, parent_size, available_space, run_mode, sizing_mode);
150
151 /// Inlined function generic over the LayoutAlgorithm to reduce code duplication
152 #[inline(always)]
153 fn perform_computations<Algorithm: LayoutAlgorithm>(
154 tree: &mut impl LayoutTree,
155 node: NodeId,
156 known_dimensions: Size<Option<f32>>,
157 parent_size: Size<Option<f32>>,
158 available_space: Size<AvailableSpace>,
159 run_mode: RunMode,
160 sizing_mode: SizingMode,
161 vertical_margins_are_collapsible: Line<bool>,
162 ) -> SizeBaselinesAndMargins {
163 #[cfg(feature = "debug")]
164 NODE_LOGGER.log(Algorithm::NAME);
165
166 match run_mode {
167 RunMode::PerformLayout => Algorithm::perform_layout(
168 tree,
169 node,
170 known_dimensions,
171 parent_size,
172 available_space,
173 sizing_mode,
174 vertical_margins_are_collapsible,
175 ),
176 RunMode::ComputeSize => Algorithm::measure_size(
177 tree,
178 node,
179 known_dimensions,
180 parent_size,
181 available_space,
182 sizing_mode,
183 vertical_margins_are_collapsible,
184 )
185 .into(),
186 }
187 }
188
189 let display_mode = tree.nodes[node_key].style.display;
190 let computed_size_and_baselines = match (display_mode, has_children) {
191 (Display::None, _) => {
192 perform_taffy_tree_hidden_layout(tree, node);
193 SizeBaselinesAndMargins::HIDDEN
194 }
195 #[cfg(feature = "block_layout")]
196 (Display::Block, true) => perform_computations::<BlockAlgorithm>(
197 tree,
198 node,
199 known_dimensions,
200 parent_size,
201 available_space,
202 run_mode,
203 sizing_mode,
204 vertical_margins_are_collapsible,
205 ),
206 #[cfg(feature = "flexbox")]
207 (Display::Flex, true) => perform_computations::<FlexboxAlgorithm>(
208 tree,
209 node,
210 known_dimensions,
211 parent_size,
212 available_space,
213 run_mode,
214 sizing_mode,
215 vertical_margins_are_collapsible,
216 ),
217 #[cfg(feature = "grid")]
218 (Display::Grid, true) => perform_computations::<CssGridAlgorithm>(
219 tree,
220 node,
221 known_dimensions,
222 parent_size,
223 available_space,
224 run_mode,
225 sizing_mode,
226 vertical_margins_are_collapsible,
227 ),
228 (_, false) => match run_mode {
229 RunMode::PerformLayout => leaf::perform_layout(
230 &tree.nodes[node_key].style,
231 tree.nodes[node_key].needs_measure.then(|| &tree.measure_funcs[node_key]),
232 known_dimensions,
233 parent_size,
234 available_space,
235 sizing_mode,
236 vertical_margins_are_collapsible,
237 ),
238 RunMode::ComputeSize => leaf::measure_size(
239 &tree.nodes[node_key].style,
240 tree.nodes[node_key].needs_measure.then(|| &tree.measure_funcs[node_key]),
241 known_dimensions,
242 parent_size,
243 available_space,
244 sizing_mode,
245 vertical_margins_are_collapsible,
246 )
247 .into(),
248 },
249 };
250
251 // Cache result
252 tree.nodes[node_key].cache.store(known_dimensions, available_space, cache_run_mode, computed_size_and_baselines);
253
254 #[cfg(feature = "debug")]
255 NODE_LOGGER.labelled_debug_log("RESULT", computed_size_and_baselines.size);
256 #[cfg(any(feature = "debug", feature = "profile"))]
257 NODE_LOGGER.pop_node();
258
259 computed_size_and_baselines
260}
261
262/// Creates a layout for this node and its children, recursively.
263/// Each hidden node has zero size and is placed at the origin
264fn perform_taffy_tree_hidden_layout(tree: &mut Taffy, node: NodeId) {
265 /// Recursive function to apply hidden layout to all descendents
266 fn perform_hidden_layout_inner(tree: &mut Taffy, node: NodeId, order: u32) {
267 let node_key = node.into();
268 *tree.layout_mut(node) = Layout::with_order(order);
269 tree.nodes[node_key].cache.clear();
270 for order in 0..tree.children[node_key].len() {
271 perform_hidden_layout_inner(tree, tree.child(node, order), order as _);
272 }
273 }
274
275 for order in 0..tree.children[node.into()].len() {
276 perform_hidden_layout_inner(tree, tree.child(node, order), order as _);
277 }
278}
279
280/// Rounds the calculated [`Layout`] to exact pixel values
281///
282/// In order to ensure that no gaps in the layout are introduced we:
283/// - Always round based on the cumulative x/y coordinates (relative to the viewport) rather than
284/// parent-relative coordinates
285/// - Compute width/height by first rounding the top/bottom/left/right and then computing the difference
286/// rather than rounding the width/height directly
287/// See <https://github.com/facebook/yoga/commit/aa5b296ac78f7a22e1aeaf4891243c6bb76488e2> for more context
288///
289/// In order to prevent innacuracies caused by rounding already-rounded values, we read from `unrounded_layout`
290/// and write to `final_layout`.
291fn round_layout(tree: &mut Taffy, node_id: NodeId, cumulative_x: f32, cumulative_y: f32) {
292 let node = &mut tree.nodes[node_id.into()];
293 let unrounded_layout = node.unrounded_layout;
294 let layout = &mut node.final_layout;
295
296 let cumulative_x = cumulative_x + unrounded_layout.location.x;
297 let cumulative_y = cumulative_y + unrounded_layout.location.y;
298
299 layout.location.x = round(unrounded_layout.location.x);
300 layout.location.y = round(unrounded_layout.location.y);
301 layout.size.width = round(cumulative_x + unrounded_layout.size.width) - round(cumulative_x);
302 layout.size.height = round(cumulative_y + unrounded_layout.size.height) - round(cumulative_y);
303
304 let child_count = tree.child_count(node_id).unwrap();
305 for index in 0..child_count {
306 let child = tree.child(node_id, index);
307 round_layout(tree, child, cumulative_x, cumulative_y);
308 }
309}