Skip to main content

blitz_dom/
resolve.rs

1//! Resolve style and layout
2
3use blitz_traits::node_id::NodeId;
4use std::cell::RefCell;
5
6use debug_timer::debug_timer;
7use kurbo::{Affine, Rect};
8use parley::LayoutContext;
9use selectors::Element as _;
10use style::dom::TDocument;
11
12#[cfg(feature = "parallel-construct")]
13use rayon::prelude::*;
14
15// FIXME: static thread_local FontCtx isn't necessarily correct in multi-document context.
16// Should use thread_local crate with ThreadLocal value store in the Document.
17thread_local! {
18    pub(crate) static LAYOUT_CTX: RefCell<Option<Box<LayoutContext<TextBrush>>>> = const { RefCell::new(None) };
19}
20
21use taffy::AvailableSpace;
22
23use crate::{
24    BaseDocument,
25    layout::{
26        LayoutPassState,
27        construct::{
28            ConstructionTask, ConstructionTaskData, ConstructionTaskResult,
29            ConstructionTaskResultData, LayoutChildren, build_inline_layout_into,
30            collect_layout_children,
31        },
32        damage::{ALL_DAMAGE, CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC},
33    },
34    node::TextBrush,
35};
36
37impl BaseDocument {
38    /// Restyle the tree and then relayout it
39    pub fn resolve(&mut self, current_time_for_animations: f64) {
40        if TDocument::as_node(&self.root_node())
41            .first_element_child()
42            .is_none()
43        {
44            #[cfg(feature = "tracing")]
45            tracing::warn!("No DOM - not resolving");
46            return;
47        }
48
49        // Process messages that have been sent to our message channel (e.g. loaded resource)
50        self.handle_messages();
51
52        // While render-blocking resources (e.g. stylesheets linked from the `<head>`) are
53        // still loading, don't resolve styles or layout (matching how browsers block
54        // rendering). Resolving styles before the document's stylesheets have loaded would
55        // give elements computed styles based on an incomplete cascade, and a later restyle
56        // (once the stylesheet loads) would treat those as genuine "before-change styles",
57        // spuriously starting CSS transitions from unstyled values. See issue #689.
58        //
59        // `handle_messages` above must still run so that loaded resources are ingested and
60        // this state can clear.
61        if self.has_pending_critical_resources() {
62            return;
63        }
64
65        self.resolve_scroll_animation();
66
67        // Drop scrollbar-activity entries whose fade-out has finished (also
68        // sheds entries for removed nodes).
69        {
70            use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
71            self.scrollbar_activity
72                .retain(|_, last| last.elapsed() < FADE_DELAY + FADE_DURATION);
73        }
74
75        let root_node_id = self.root_element().id;
76        debug_timer!(timer, feature = "log-phase-times");
77
78        // Apply any device changes (viewport resize, zoom, color-scheme, etc)
79        // accumulated since the last resolve as a single device rebuild.
80        self.flush_pending_device_changes();
81
82        // we need to resolve stylist first since it will need to drive our layout bits
83        self.resolve_stylist(current_time_for_animations);
84        timer.record_time("style");
85
86        // Non-incremental mode is "every node is damaged every frame": mark the
87        // whole tree and let the same damage pipeline reconstruct and relayout
88        // everything.
89        if !self.incremental_layout {
90            self.mark_all_damaged();
91            timer.record_time("mark_all");
92        }
93
94        // Propagate damage flags (from mutation and restyles) up and down the tree
95        self.propagate_damage_flags(root_node_id);
96        timer.record_time("damage");
97
98        // Fix up tree for layout (insert anonymous blocks as necessary, etc)
99        self.resolve_layout_children();
100        #[cfg(debug_assertions)]
101        self.assert_layout_parents_consistent();
102        timer.record_time("construct");
103
104        self.resolve_deferred_tasks();
105        // Flush background/mask images from style to dedicated storage on the
106        // nodes whose style changed (queued by the style traversal and by
107        // pseudo-element box construction), fetching any not-yet-loaded images.
108        self.flush_pending_style_images();
109        timer.record_time("pconstruct");
110
111        // Next we resolve layout with the data resolved by stlist
112        self.resolve_layout();
113        timer.record_time("layout");
114        self.nodes.bump_geometry_generation();
115
116        // Resolve transforms
117        self.resolve_transforms(root_node_id);
118        timer.record_time("transform");
119
120        // Build paint children / stacking contexts for damaged subtrees. Runs
121        // after layout and transforms so that stacking-context roots created by
122        // this frame's transforms are picked up, and before damage is cleared.
123        self.build_paint_tree(root_node_id);
124        timer.record_time("paint_tree");
125
126        // Clear all damage and dirty flags, walking only subtrees which are
127        // marked as (potentially) containing damage.
128        let doc_node_id = self.root_node().id;
129        self.clear_damage_and_dirty_flags(doc_node_id);
130        timer.record_time("c_damage");
131
132        // Re-resolve the hover node from the pointer position against the fresh
133        // layout. This must run *after* the damage/dirty flags are cleared
134        // above, so that the restyle hint and ancestor `dirty_descendants`
135        // flags set by any resulting hover change survive into the next resolve
136        // pass (the clearing loop would otherwise wipe them). Any resulting
137        // restyle is picked up on the next resolve pass; a redraw is requested
138        // if the hovered node actually changes.
139        self.refresh_hover();
140
141        let mut subdoc_is_animating = false;
142        for &node_id in &self.sub_document_nodes {
143            let node = &mut self.nodes[node_id];
144            let size = node.final_layout().size;
145            if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
146                // Set viewport
147                // viewport_mut handles change detection. So we just unconditionally set the values;
148                let mut sub_viewport = sub_doc.viewport_mut();
149                sub_viewport.hidpi_scale = self.viewport.hidpi_scale;
150                sub_viewport.zoom = self.viewport.zoom;
151                sub_viewport.color_scheme = self.viewport.color_scheme;
152
153                let viewport_scale = self.viewport.scale();
154                sub_viewport.window_size = (
155                    (size.width * viewport_scale) as u32,
156                    (size.height * viewport_scale) as u32,
157                );
158                drop(sub_viewport);
159
160                sub_doc.resolve(current_time_for_animations);
161
162                subdoc_is_animating |= sub_doc.is_animating();
163            }
164        }
165        self.subdoc_is_animating = subdoc_is_animating;
166        timer.record_time("subdocs");
167
168        timer.print_times(&format!("Resolve({}): ", self.id()));
169    }
170
171    fn resolve_transforms(&mut self, node_id: NodeId) -> Rect {
172        if !self.nodes.contains_key(node_id) {
173            return Rect::ZERO;
174        }
175
176        let scale = self.viewport.scale_f64();
177
178        if !self.nodes[node_id]
179            .damage()
180            .map(|d| d.contains(style::selector_parser::RestyleDamage::RECALCULATE_OVERFLOW))
181            .unwrap_or(false)
182        {
183            let node = &self.nodes[node_id];
184            let location = node.final_layout().location.map(|v| v as f64 * scale);
185
186            let mut transform = Affine::translate((location.x, location.y));
187            if let Some(t) = node.transform().as_deref() {
188                transform *= *t
189            }
190
191            let overflow = *node.scrollable_overflow();
192            return transform.transform_rect_bbox(overflow);
193        }
194
195        let transform = self.nodes[node_id].set_transform(scale as f32);
196
197        let w = self.nodes[node_id].final_layout().size.width as f64 * scale;
198        let h = self.nodes[node_id].final_layout().size.height as f64 * scale;
199        let mut overflow = Rect::new(0.0, 0.0, w, h);
200
201        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
202
203        if let Some(ref children) = layout_children {
204            for &child_id in children {
205                // Out-of-flow children are laid out relative to their containing
206                // block, not their DOM parent: they are visited (and their overflow
207                // accounted for) via the containing block's hoisted list below.
208                if self.nodes[child_id].is_out_of_flow() {
209                    continue;
210                }
211                let child_rect_in_self = self.resolve_transforms(child_id);
212                overflow = overflow.union(child_rect_in_self);
213            }
214        }
215        let hoisted_children =
216            std::mem::take(&mut *self.nodes[node_id].hoisted_children.borrow_mut());
217        for &child_id in &hoisted_children {
218            if !self.nodes.contains_key(child_id) {
219                continue;
220            }
221            let child_rect_in_self = self.resolve_transforms(child_id);
222            overflow = overflow.union(child_rect_in_self);
223        }
224        *self.nodes[node_id].hoisted_children.borrow_mut() = hoisted_children;
225        for pseudo in [self.nodes[node_id].before(), self.nodes[node_id].after()] {
226            let Some(pseudo) = pseudo else { continue };
227            if self.nodes[pseudo].is_out_of_flow() {
228                continue;
229            }
230            let child_rect_in_self = self.resolve_transforms(pseudo);
231            overflow = overflow.union(child_rect_in_self);
232        }
233
234        *self.nodes[node_id].scrollable_overflow_mut() = overflow;
235        *self.nodes[node_id].layout_children.get_mut() = layout_children;
236
237        let scaled_x = self.nodes[node_id].final_layout().location.x as f64 * scale;
238        let scaled_y = self.nodes[node_id].final_layout().location.y as f64 * scale;
239
240        let full = if let Some(t) = transform {
241            Affine::translate((scaled_x, scaled_y)) * t
242        } else {
243            Affine::translate((scaled_x, scaled_y))
244        };
245
246        full.transform_rect_bbox(overflow)
247    }
248
249    /// Ensure that the layout_children field is populated for all nodes
250    pub fn resolve_layout_children(&mut self) {
251        resolve_layout_children_recursive(self, self.root_node().id);
252
253        fn resolve_layout_children_recursive(doc: &mut BaseDocument, node_id: NodeId) {
254            // Anonymous blocks and pseudo-elements can be removed from the slab
255            // between render passes. Bail out rather than panicking on a stale key.
256            if doc.nodes.get(node_id).is_none() {
257                return;
258            }
259
260            let mut damage = doc.nodes[node_id].damage().unwrap_or(ALL_DAMAGE);
261            let _flags = doc.nodes[node_id].flags;
262
263            if damage.intersects(CONSTRUCT_FC | CONSTRUCT_BOX) {
264                //} || flags.contains(NodeFlags::IS_INLINE_ROOT) {
265
266                // Deallocate the anonymous blocks created for this node in the
267                // previous construction round. They live only in the slab, so
268                // reconstructing without freeing them would leak a slab entry per
269                // anonymous block per reconstruction.
270                let old_anonymous_blocks = std::mem::take(&mut doc.nodes[node_id].anonymous_blocks);
271                for anon_id in old_anonymous_blocks {
272                    doc.deallocate_anonymous_block(anon_id);
273                }
274
275                let mut collected = LayoutChildren::default();
276                collect_layout_children(doc, node_id, &mut collected);
277                let layout_children = collected.children;
278                doc.nodes[node_id].anonymous_blocks = collected.anonymous_blocks;
279
280                // Recurse into newly collected layout children
281                for child_id in layout_children.iter().copied() {
282                    resolve_layout_children_recursive(doc, child_id);
283                    doc.nodes[child_id].layout_parent.set(Some(node_id));
284                    if let Some(mut data) = doc.nodes[child_id]
285                        .try_stylo_element_data_mut()
286                        .and_then(|s| s.get_mut())
287                    {
288                        data.damage
289                            .remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
290                    }
291                }
292
293                *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children.clone());
294                // *doc.nodes[node_id].paint_children.borrow_mut() = Some(layout_children);
295
296                damage.remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
297                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
298            } else {
299                //if damage.contains(CONSTRUCT_DESCENDENT) {
300                let layout_children = doc.nodes[node_id].layout_children.borrow_mut().take();
301                if let Some(layout_children) = layout_children {
302                    for child_id in layout_children.iter().copied() {
303                        // Anonymous blocks and pseudo-elements can be removed from the
304                        // slab between render passes; skip stale IDs.
305                        if !doc.nodes.contains_key(child_id) {
306                            continue;
307                        }
308                        resolve_layout_children_recursive(doc, child_id);
309                        doc.nodes[child_id].layout_parent.set(Some(node_id));
310                    }
311
312                    *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children);
313                }
314
315                // damage.remove(CONSTRUCT_DESCENDENT);
316                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
317            }
318
319            doc.nodes[node_id].set_damage(damage);
320        }
321    }
322
323    /// Every layout child must point back at its container via
324    /// `layout_parent`, which `propagate_damage_flags` relies on to reach
325    /// anonymous boxes.
326    #[cfg(debug_assertions)]
327    fn assert_layout_parents_consistent(&self) {
328        use crate::node::NodeFlags;
329
330        for (parent_id, node) in self.nodes.iter() {
331            if !node.flags.contains(NodeFlags::IS_IN_DOCUMENT) {
332                continue;
333            }
334            let Some(children) = node.layout_children.borrow().clone() else {
335                continue;
336            };
337            for child_id in children {
338                let Some(child) = self.nodes.get(child_id) else {
339                    panic!("layout child {child_id:?} of {parent_id:?} is not in the slab");
340                };
341                debug_assert_eq!(
342                    child.layout_parent.get(),
343                    Some(parent_id),
344                    "layout_parent of {child_id:?} does not point at its layout container {parent_id:?}"
345                );
346            }
347        }
348    }
349
350    pub fn resolve_deferred_tasks(&mut self) {
351        let mut deferred_construction_nodes = std::mem::take(&mut self.deferred_construction_nodes);
352
353        // Deduplicate deferred tasks by node_id to avoid redundant work
354        deferred_construction_nodes.sort_unstable_by_key(|task| task.node_id);
355        deferred_construction_nodes.dedup_by_key(|task| task.node_id);
356
357        #[cfg(feature = "parallel-construct")]
358        let iter = deferred_construction_nodes.into_par_iter();
359        #[cfg(not(feature = "parallel-construct"))]
360        let iter = deferred_construction_nodes.into_iter();
361
362        let results: Vec<ConstructionTaskResult> = iter
363            .map(|task: ConstructionTask| match task.data {
364                ConstructionTaskData::InlineLayout(mut layout) => {
365                    #[cfg(feature = "parallel-construct")]
366                    let mut layout_ctx = LAYOUT_CTX
367                        .take()
368                        .unwrap_or_else(|| Box::new(LayoutContext::new()));
369                    #[cfg(feature = "parallel-construct")]
370                    let layout_ctx_mut = &mut layout_ctx;
371
372                    #[cfg(feature = "parallel-construct")]
373                    let mut font_ctx = self
374                        .thread_font_contexts
375                        .get_or(|| RefCell::new(Box::new(self.font_ctx.lock().unwrap().clone())))
376                        .borrow_mut();
377                    #[cfg(feature = "parallel-construct")]
378                    let font_ctx_mut = &mut *font_ctx;
379
380                    #[cfg(not(feature = "parallel-construct"))]
381                    let layout_ctx_mut = &mut self.layout_ctx;
382                    #[cfg(not(feature = "parallel-construct"))]
383                    let font_ctx_mut = &mut *self.font_ctx.lock().unwrap();
384
385                    layout.content_widths = None;
386                    build_inline_layout_into(
387                        &self.nodes,
388                        layout_ctx_mut,
389                        font_ctx_mut,
390                        &mut layout,
391                        self.viewport.scale(),
392                        task.node_id,
393                    );
394
395                    #[cfg(feature = "parallel-construct")]
396                    {
397                        LAYOUT_CTX.set(Some(layout_ctx));
398                    }
399
400                    // If layout doesn't contain any inline boxes, then it is safe to populate the content_widths
401                    // cache during this parallelized stage.
402                    // if layout.layout.inline_boxes().is_empty() {
403                    //     layout.content_widths();
404                    // }
405
406                    ConstructionTaskResult {
407                        node_id: task.node_id,
408                        data: ConstructionTaskResultData::InlineLayout(layout),
409                    }
410                }
411            })
412            .collect();
413
414        for result in results {
415            match result.data {
416                ConstructionTaskResultData::InlineLayout(layout) => {
417                    self.nodes[result.node_id].clear_layout_cache();
418                    self.nodes[result.node_id]
419                        .element_data_mut()
420                        .unwrap()
421                        .inline_layout_data = Some(layout);
422                }
423            }
424        }
425
426        self.deferred_construction_nodes.clear();
427    }
428
429    /// Walk the nodes now that they're properly styled and transfer their styles to the taffy style system
430    ///
431    /// TODO: update taffy to use an associated type instead of slab key
432    /// TODO: update taffy to support traited styles so we don't even need to rely on taffy for storage
433    pub fn resolve_layout(&mut self) {
434        let size = self.stylist.device().au_viewport_size();
435
436        let available_space = taffy::Size {
437            width: AvailableSpace::Definite(size.width.to_f32_px()),
438            height: AvailableSpace::Definite(size.height.to_f32_px()),
439        };
440
441        let root_element_id = crate::taffy_node_id(self.root_element().id);
442
443        // println!("\n\nRESOLVE LAYOUT\n===========\n");
444
445        let mut state = LayoutPassState::new(self);
446        #[cfg(feature = "writing-mode")]
447        let available_space = {
448            state.layout_wm = state.layout_wm_of(crate::dom_node_id(root_element_id));
449            if state.layout_wm.is_vertical() {
450                available_space.transpose()
451            } else {
452                available_space
453            }
454        };
455        taffy::compute_root_layout(&mut state, root_element_id, available_space);
456        #[cfg(feature = "writing-mode")]
457        state.physicalise_and_round_layout(root_element_id);
458        #[cfg(not(feature = "writing-mode"))]
459        taffy::round_layout(&mut state, root_element_id);
460
461        // println!("\n\n");
462        // taffy::print_tree(self, root_node_id)
463    }
464}