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 style::selector_parser::RestyleDamage;
22use taffy::AvailableSpace;
23
24use crate::{
25    BaseDocument,
26    layout::{
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        // we need to resolve stylist first since it will need to drive our layout bits
79        self.resolve_stylist(current_time_for_animations);
80        timer.record_time("style");
81
82        // Propagate damage flags (from mutation and restyles) up and down the tree
83        if self.incremental_layout {
84            self.propagate_damage_flags(root_node_id, RestyleDamage::empty());
85            timer.record_time("damage");
86        }
87
88        // Fix up tree for layout (insert anonymous blocks as necessary, etc)
89        self.resolve_layout_children();
90        timer.record_time("construct");
91
92        self.resolve_deferred_tasks();
93        // Flush background/mask images from style to dedicated storage on the
94        // nodes whose style changed (queued by the style traversal and by
95        // pseudo-element box construction), fetching any not-yet-loaded images.
96        self.flush_pending_style_images();
97        timer.record_time("pconstruct");
98
99        // Merge stylo into taffy
100        self.flush_styles_to_layout(root_node_id);
101        timer.record_time("flush");
102
103        // Next we resolve layout with the data resolved by stlist
104        self.resolve_layout();
105        timer.record_time("layout");
106
107        // Resolve transforms
108        self.resolve_transforms(root_node_id);
109        timer.record_time("transform");
110
111        // Clear all damage and dirty flags, walking only subtrees which are
112        // marked as (potentially) containing damage.
113        if self.incremental_layout {
114            let doc_node_id = self.root_node().id;
115            self.clear_damage_and_dirty_flags(doc_node_id);
116            timer.record_time("c_damage");
117        }
118
119        // Re-resolve the hover node from the pointer position against the fresh
120        // layout. This must run *after* the damage/dirty flags are cleared
121        // above, so that the restyle hint and ancestor `dirty_descendants`
122        // flags set by any resulting hover change survive into the next resolve
123        // pass (the clearing loop would otherwise wipe them). Any resulting
124        // restyle is picked up on the next resolve pass; a redraw is requested
125        // if the hovered node actually changes.
126        self.refresh_hover();
127
128        let mut subdoc_is_animating = false;
129        for &node_id in &self.sub_document_nodes {
130            let node = &mut self.nodes[node_id];
131            let size = node.final_layout().size;
132            if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
133                // Set viewport
134                // viewport_mut handles change detection. So we just unconditionally set the values;
135                let mut sub_viewport = sub_doc.viewport_mut();
136                sub_viewport.hidpi_scale = self.viewport.hidpi_scale;
137                sub_viewport.zoom = self.viewport.zoom;
138                sub_viewport.color_scheme = self.viewport.color_scheme;
139
140                let viewport_scale = self.viewport.scale();
141                sub_viewport.window_size = (
142                    (size.width * viewport_scale) as u32,
143                    (size.height * viewport_scale) as u32,
144                );
145                drop(sub_viewport);
146
147                sub_doc.resolve(current_time_for_animations);
148
149                subdoc_is_animating |= sub_doc.is_animating();
150            }
151        }
152        self.subdoc_is_animating = subdoc_is_animating;
153        timer.record_time("subdocs");
154
155        timer.print_times(&format!("Resolve({}): ", self.id()));
156    }
157
158    fn resolve_transforms(&mut self, node_id: NodeId) -> Rect {
159        if !self.nodes.contains_key(node_id) {
160            return Rect::ZERO;
161        }
162
163        if !self.nodes[node_id]
164            .damage()
165            .map(|d| d.contains(style::selector_parser::RestyleDamage::RECALCULATE_OVERFLOW))
166            .unwrap_or(false)
167        {
168            return *self.nodes[node_id].scrollable_overflow();
169        }
170
171        let scale = self.viewport.scale_f64();
172
173        let transform = self.nodes[node_id].set_transform(scale as f32);
174
175        let w = self.nodes[node_id].final_layout().size.width as f64 * scale;
176        let h = self.nodes[node_id].final_layout().size.height as f64 * scale;
177        let mut overflow = Rect::new(0.0, 0.0, w, h);
178
179        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
180
181        if let Some(ref children) = layout_children {
182            for &child_id in children {
183                let child_rect_in_self = self.resolve_transforms(child_id);
184                overflow = overflow.union(child_rect_in_self);
185            }
186        }
187        if let Some(before) = self.nodes[node_id].before() {
188            let child_rect_in_self = self.resolve_transforms(before);
189            overflow = overflow.union(child_rect_in_self);
190        }
191        if let Some(after) = self.nodes[node_id].after() {
192            let child_rect_in_self = self.resolve_transforms(after);
193            overflow = overflow.union(child_rect_in_self);
194        }
195
196        *self.nodes[node_id].scrollable_overflow_mut() = overflow;
197        *self.nodes[node_id].layout_children.get_mut() = layout_children;
198
199        let scaled_x = self.nodes[node_id].final_layout().location.x as f64 * scale;
200        let scaled_y = self.nodes[node_id].final_layout().location.y as f64 * scale;
201
202        let full = if let Some(t) = transform {
203            Affine::translate((scaled_x, scaled_y)) * t
204        } else {
205            Affine::translate((scaled_x, scaled_y))
206        };
207
208        full.transform_rect_bbox(overflow)
209    }
210
211    /// Ensure that the layout_children field is populated for all nodes
212    pub fn resolve_layout_children(&mut self) {
213        resolve_layout_children_recursive(self, self.root_node().id);
214
215        fn resolve_layout_children_recursive(doc: &mut BaseDocument, node_id: NodeId) {
216            // Anonymous blocks and pseudo-elements can be removed from the slab
217            // between render passes. Bail out rather than panicking on a stale key.
218            if doc.nodes.get(node_id).is_none() {
219                return;
220            }
221
222            let mut damage = doc.nodes[node_id].damage().unwrap_or(ALL_DAMAGE);
223            let _flags = doc.nodes[node_id].flags;
224
225            if !doc.incremental_layout || damage.intersects(CONSTRUCT_FC | CONSTRUCT_BOX) {
226                //} || flags.contains(NodeFlags::IS_INLINE_ROOT) {
227
228                // Deallocate the anonymous blocks created for this node in the
229                // previous construction round. They live only in the slab, so
230                // reconstructing without freeing them would leak a slab entry per
231                // anonymous block per reconstruction.
232                let old_anonymous_blocks = std::mem::take(&mut doc.nodes[node_id].anonymous_blocks);
233                for anon_id in old_anonymous_blocks {
234                    doc.deallocate_anonymous_block(anon_id);
235                }
236
237                let mut collected = LayoutChildren::default();
238                collect_layout_children(doc, node_id, &mut collected);
239                let layout_children = collected.children;
240                doc.nodes[node_id].anonymous_blocks = collected.anonymous_blocks;
241
242                // Recurse into newly collected layout children
243                for child_id in layout_children.iter().copied() {
244                    resolve_layout_children_recursive(doc, child_id);
245                    doc.nodes[child_id].layout_parent.set(Some(node_id));
246                    if let Some(mut data) = doc.nodes[child_id]
247                        .stylo_element_data_opt_mut()
248                        .and_then(|s| s.get_mut())
249                    {
250                        data.damage
251                            .remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
252                    }
253                }
254
255                *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children.clone());
256                // *doc.nodes[node_id].paint_children.borrow_mut() = Some(layout_children);
257
258                damage.remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
259                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
260            } else {
261                //if damage.contains(CONSTRUCT_DESCENDENT) {
262                let layout_children = doc.nodes[node_id].layout_children.borrow_mut().take();
263                if let Some(layout_children) = layout_children {
264                    for child_id in layout_children.iter().copied() {
265                        // Anonymous blocks and pseudo-elements can be removed from the
266                        // slab between render passes; skip stale IDs.
267                        if !doc.nodes.contains_key(child_id) {
268                            continue;
269                        }
270                        resolve_layout_children_recursive(doc, child_id);
271                        doc.nodes[child_id].layout_parent.set(Some(node_id));
272                    }
273
274                    *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children);
275                }
276
277                // damage.remove(CONSTRUCT_DESCENDENT);
278                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
279            }
280
281            doc.nodes[node_id].set_damage(damage);
282        }
283    }
284
285    pub fn resolve_deferred_tasks(&mut self) {
286        let mut deferred_construction_nodes = std::mem::take(&mut self.deferred_construction_nodes);
287
288        // Deduplicate deferred tasks by node_id to avoid redundant work
289        deferred_construction_nodes.sort_unstable_by_key(|task| task.node_id);
290        deferred_construction_nodes.dedup_by_key(|task| task.node_id);
291
292        #[cfg(feature = "parallel-construct")]
293        let iter = deferred_construction_nodes.into_par_iter();
294        #[cfg(not(feature = "parallel-construct"))]
295        let iter = deferred_construction_nodes.into_iter();
296
297        let results: Vec<ConstructionTaskResult> = iter
298            .map(|task: ConstructionTask| match task.data {
299                ConstructionTaskData::InlineLayout(mut layout) => {
300                    #[cfg(feature = "parallel-construct")]
301                    let mut layout_ctx = LAYOUT_CTX
302                        .take()
303                        .unwrap_or_else(|| Box::new(LayoutContext::new()));
304                    #[cfg(feature = "parallel-construct")]
305                    let layout_ctx_mut = &mut layout_ctx;
306
307                    #[cfg(feature = "parallel-construct")]
308                    let mut font_ctx = self
309                        .thread_font_contexts
310                        .get_or(|| RefCell::new(Box::new(self.font_ctx.lock().unwrap().clone())))
311                        .borrow_mut();
312                    #[cfg(feature = "parallel-construct")]
313                    let font_ctx_mut = &mut *font_ctx;
314
315                    #[cfg(not(feature = "parallel-construct"))]
316                    let layout_ctx_mut = &mut self.layout_ctx;
317                    #[cfg(not(feature = "parallel-construct"))]
318                    let font_ctx_mut = &mut *self.font_ctx.lock().unwrap();
319
320                    layout.content_widths = None;
321                    build_inline_layout_into(
322                        &self.nodes,
323                        layout_ctx_mut,
324                        font_ctx_mut,
325                        &mut layout,
326                        self.viewport.scale(),
327                        task.node_id,
328                    );
329
330                    #[cfg(feature = "parallel-construct")]
331                    {
332                        LAYOUT_CTX.set(Some(layout_ctx));
333                    }
334
335                    // If layout doesn't contain any inline boxes, then it is safe to populate the content_widths
336                    // cache during this parallelized stage.
337                    // if layout.layout.inline_boxes().is_empty() {
338                    //     layout.content_widths();
339                    // }
340
341                    ConstructionTaskResult {
342                        node_id: task.node_id,
343                        data: ConstructionTaskResultData::InlineLayout(layout),
344                    }
345                }
346            })
347            .collect();
348
349        for result in results {
350            match result.data {
351                ConstructionTaskResultData::InlineLayout(layout) => {
352                    self.nodes[result.node_id].cache_mut().clear();
353                    self.nodes[result.node_id]
354                        .element_data_mut()
355                        .unwrap()
356                        .inline_layout_data = Some(layout);
357                }
358            }
359        }
360
361        self.deferred_construction_nodes.clear();
362    }
363
364    /// Walk the nodes now that they're properly styled and transfer their styles to the taffy style system
365    ///
366    /// TODO: update taffy to use an associated type instead of slab key
367    /// TODO: update taffy to support traited styles so we don't even need to rely on taffy for storage
368    pub fn resolve_layout(&mut self) {
369        let size = self.stylist.device().au_viewport_size();
370
371        let available_space = taffy::Size {
372            width: AvailableSpace::Definite(size.width.to_f32_px()),
373            height: AvailableSpace::Definite(size.height.to_f32_px()),
374        };
375
376        let root_element_id = crate::taffy_node_id(self.root_element().id);
377
378        // println!("\n\nRESOLVE LAYOUT\n===========\n");
379
380        taffy::compute_root_layout(self, root_element_id, available_space);
381        taffy::round_layout(self, root_element_id);
382
383        // println!("\n\n");
384        // taffy::print_tree(self, root_node_id)
385    }
386}