Skip to main content

apace_dom/
resolve.rs

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