Skip to main content

ps_blitz_dom/
resolve.rs

1//! Resolve style and layout
2
3use std::{
4    cell::RefCell,
5    time::{SystemTime, UNIX_EPOCH},
6};
7
8use debug_timer::debug_timer;
9use kurbo::{Affine, Rect};
10use parley::LayoutContext;
11use selectors::Element as _;
12use style::dom::TDocument;
13
14#[cfg(feature = "parallel-construct")]
15use rayon::prelude::*;
16
17// FIXME: static thread_local FontCtx isn't necessarily correct in multi-document context.
18// Should use thread_local crate with ThreadLocal value store in the Document.
19thread_local! {
20    pub(crate) static LAYOUT_CTX: RefCell<Option<Box<LayoutContext<TextBrush>>>> = const { RefCell::new(None) };
21}
22
23use style::properties::generated::longhands::position::computed_value::T as Position;
24use style::selector_parser::RestyleDamage;
25use taffy::AvailableSpace;
26
27use crate::{
28    BaseDocument,
29    events::ScrollAnimationState,
30    layout::{
31        construct::{
32            ConstructionTask, ConstructionTaskData, ConstructionTaskResult,
33            ConstructionTaskResultData, LayoutChildren, build_inline_layout_into,
34            collect_layout_children,
35        },
36        damage::{ALL_DAMAGE, CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC},
37    },
38    node::TextBrush,
39};
40
41impl BaseDocument {
42    /// Restyle the tree and then relayout it
43    pub fn resolve(&mut self, current_time_for_animations: f64) {
44        if TDocument::as_node(&&self.nodes[0])
45            .first_element_child()
46            .is_none()
47        {
48            #[cfg(feature = "tracing")]
49            tracing::warn!("No DOM - not resolving");
50            return;
51        }
52
53        // Process messages that have been sent to our message channel (e.g. loaded resource)
54        self.handle_messages();
55
56        self.resolve_scroll_animation();
57
58        // Retain completed activity entries so an initially visible scrollbar
59        // stays faded after its first interaction. Only removed nodes need to
60        // shed their entry.
61        let nodes = &self.nodes;
62        self.scrollbar_activity
63            .retain(|node_id, _| nodes.contains(*node_id));
64
65        let root_node_id = self.root_element().id;
66        debug_timer!(timer, feature = "log-phase-times");
67
68        // we need to resolve stylist first since it will need to drive our layout bits
69        self.resolve_stylist(current_time_for_animations);
70        timer.record_time("style");
71
72        // Propagate damage flags (from mutation and restyles) up and down the tree
73        if self.incremental_layout {
74            self.propagate_damage_flags(root_node_id, RestyleDamage::empty());
75            timer.record_time("damage");
76        }
77
78        // Fix up tree for layout (insert anonymous blocks as necessary, etc)
79        self.resolve_layout_children();
80        timer.record_time("construct");
81
82        self.resolve_deferred_tasks();
83        timer.record_time("pconstruct");
84
85        // Merge stylo into taffy
86        self.flush_styles_to_layout(root_node_id);
87        timer.record_time("flush");
88
89        // Next we resolve layout with the data resolved by stlist
90        self.resolve_layout();
91        timer.record_time("layout");
92
93        self.resolve_transforms(root_node_id);
94        timer.record_time("transform");
95
96        // Clear all damage and dirty flags
97        if self.incremental_layout {
98            for (_, node) in self.nodes.iter_mut() {
99                node.clear_damage_mut();
100                node.unset_dirty_descendants();
101            }
102            timer.record_time("c_damage");
103        }
104
105        let mut subdoc_is_animating = false;
106        for &node_id in &self.sub_document_nodes {
107            let node = &mut self.nodes[node_id];
108            let size = node.final_layout.size;
109            if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
110                // Set viewport
111                // viewport_mut handles change detection. So we just unconditionally set the values;
112                let mut sub_viewport = sub_doc.viewport_mut();
113                sub_viewport.hidpi_scale = self.viewport.hidpi_scale;
114                sub_viewport.zoom = self.viewport.zoom;
115                sub_viewport.color_scheme = self.viewport.color_scheme;
116
117                let viewport_scale = self.viewport.scale();
118                sub_viewport.window_size = (
119                    (size.width * viewport_scale) as u32,
120                    (size.height * viewport_scale) as u32,
121                );
122                drop(sub_viewport);
123
124                sub_doc.resolve(current_time_for_animations);
125
126                subdoc_is_animating |= sub_doc.is_animating();
127            }
128        }
129        self.subdoc_is_animating = subdoc_is_animating;
130        timer.record_time("subdocs");
131
132        // Printed with the phases so a single line says both how long layout
133        // took and how much of the tree it touched. Without the counts the
134        // timings cannot distinguish a few slow nodes from a cache miss across
135        // the document, and those need opposite fixes.
136        #[cfg(feature = "log-phase-times")]
137        {
138            // Named before the counters are drained, and only when the pass was
139            // expensive enough to be worth looking at.
140            let offenders = crate::layout::layout_counters::worst_offenders(6);
141            if offenders.first().is_some_and(|(_, count)| *count > 8) {
142                let described: Vec<String> = offenders
143                    .iter()
144                    .map(|(id, count)| {
145                        let tag = self
146                            .nodes
147                            .get(*id)
148                            .and_then(|node| node.element_data())
149                            .map(|element| element.name.local.to_string())
150                            .unwrap_or_else(|| "?".to_string());
151                        let display = self
152                            .nodes
153                            .get(*id)
154                            .map(|node| format!("{:?}", node.style.display))
155                            .unwrap_or_default();
156                        format!("{id}:{tag}({display})x{count}")
157                    })
158                    .collect();
159                println!("  layout hotspots: {}", described.join(" "));
160            }
161            let counts = crate::layout::layout_counters::take();
162            let total_nodes = self.nodes.len();
163            let hit_rate = if counts.lookups > 0 {
164                (counts.hits as f64 / counts.lookups as f64) * 100.0
165            } else {
166                0.0
167            };
168            timer.print_times(&format!(
169                "Resolve({}) [computed {} over {} distinct of {total_nodes} nodes, \
170                 cache {}/{} hits {hit_rate:.0}%, {} cleared]: ",
171                self.id(),
172                counts.computed,
173                counts.distinct,
174                counts.hits,
175                counts.lookups,
176                counts.caches_cleared,
177            ));
178        }
179        #[cfg(not(feature = "log-phase-times"))]
180        timer.print_times(&format!("Resolve({}): ", self.id()));
181    }
182
183    fn resolve_transforms(&mut self, node_id: usize) -> Rect {
184        if !self.nodes.contains(node_id) {
185            return Rect::ZERO;
186        }
187
188        if !self.nodes[node_id]
189            .damage()
190            .map(|d| d.contains(style::selector_parser::RestyleDamage::RECALCULATE_OVERFLOW))
191            .unwrap_or(false)
192        {
193            return self.nodes[node_id].scrollable_overflow;
194        }
195
196        let scale = self.viewport.scale_f64();
197
198        let transform = self.nodes[node_id].set_transform(scale as f32);
199
200        let w = self.nodes[node_id].final_layout.size.width as f64 * scale;
201        let h = self.nodes[node_id].final_layout.size.height as f64 * scale;
202        let mut overflow = Rect::new(0.0, 0.0, w, h);
203
204        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
205
206        if let Some(ref children) = layout_children {
207            for &child_id in children {
208                let child_rect_in_self = self.resolve_transforms(child_id);
209                overflow = overflow.union(child_rect_in_self);
210            }
211        }
212        if let Some(before) = self.nodes[node_id].before {
213            let child_rect_in_self = self.resolve_transforms(before);
214            overflow = overflow.union(child_rect_in_self);
215        }
216        if let Some(after) = self.nodes[node_id].after {
217            let child_rect_in_self = self.resolve_transforms(after);
218            overflow = overflow.union(child_rect_in_self);
219        }
220
221        self.nodes[node_id].scrollable_overflow = overflow;
222        *self.nodes[node_id].layout_children.get_mut() = layout_children;
223
224        let scaled_x = self.nodes[node_id].final_layout.location.x as f64 * scale;
225        let scaled_y = self.nodes[node_id].final_layout.location.y as f64 * scale;
226
227        let full = if let Some(t) = transform {
228            Affine::translate((scaled_x, scaled_y)) * t
229        } else {
230            Affine::translate((scaled_x, scaled_y))
231        };
232
233        full.transform_rect_bbox(overflow)
234    }
235
236    pub fn resolve_scroll_animation(&mut self) {
237        match &mut self.scroll_animation {
238            ScrollAnimationState::Fling(fling_state) => {
239                let time_ms = SystemTime::now()
240                    .duration_since(UNIX_EPOCH)
241                    .unwrap()
242                    .as_millis() as u64 as f64;
243
244                let time_diff_ms = time_ms - fling_state.last_seen_time;
245
246                // 0.95 @ 60fps normalized to actual frame times
247                let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
248
249                fling_state.x_velocity *= deceleration;
250                fling_state.y_velocity *= deceleration;
251                fling_state.last_seen_time = time_ms;
252                let fling_state = fling_state.clone();
253
254                let dx = fling_state.x_velocity * time_diff_ms;
255                let dy = fling_state.y_velocity * time_diff_ms;
256
257                self.scroll_by(Some(fling_state.target), dx, dy, &mut |_| {});
258                if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
259                    self.scroll_animation = ScrollAnimationState::None;
260                }
261            }
262            ScrollAnimationState::None => {
263                // Do nothing
264            }
265        }
266    }
267
268    /// Ensure that the layout_children field is populated for all nodes
269    pub fn resolve_layout_children(&mut self) {
270        resolve_layout_children_recursive(self, self.root_node().id);
271
272        fn resolve_layout_children_recursive(doc: &mut BaseDocument, node_id: usize) {
273            // Anonymous blocks and pseudo-elements can be removed from the slab
274            // between render passes. Bail out rather than panicking on a stale key.
275            if doc.nodes.get(node_id).is_none() {
276                return;
277            }
278
279            let mut damage = doc.nodes[node_id].damage().unwrap_or(ALL_DAMAGE);
280            let _flags = doc.nodes[node_id].flags;
281
282            if !doc.incremental_layout || damage.intersects(CONSTRUCT_FC | CONSTRUCT_BOX) {
283                //} || flags.contains(NodeFlags::IS_INLINE_ROOT) {
284                let mut collected = LayoutChildren::default();
285                collect_layout_children(doc, node_id, &mut collected);
286                let layout_children = collected.children;
287
288                // Recurse into newly collected layout children
289                for child_id in layout_children.iter().copied() {
290                    resolve_layout_children_recursive(doc, child_id);
291                    doc.nodes[child_id].layout_parent.set(Some(node_id));
292                    if let Some(mut data) = doc.nodes[child_id].stylo_element_data.get_mut() {
293                        data.damage
294                            .remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
295                    }
296                }
297
298                *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children.clone());
299                // *doc.nodes[node_id].paint_children.borrow_mut() = Some(layout_children);
300
301                damage.remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
302                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
303            } else {
304                //if damage.contains(CONSTRUCT_DESCENDENT) {
305                let layout_children = doc.nodes[node_id].layout_children.borrow_mut().take();
306                if let Some(layout_children) = layout_children {
307                    for child_id in layout_children.iter().copied() {
308                        // Anonymous blocks and pseudo-elements can be removed from the
309                        // slab between render passes; skip stale IDs.
310                        if !doc.nodes.contains(child_id) {
311                            continue;
312                        }
313                        resolve_layout_children_recursive(doc, child_id);
314                        doc.nodes[child_id].layout_parent.set(Some(node_id));
315                    }
316
317                    *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children);
318                }
319
320                // damage.remove(CONSTRUCT_DESCENDENT);
321                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
322            }
323
324            doc.nodes[node_id].set_damage(damage);
325        }
326    }
327
328    pub fn resolve_deferred_tasks(&mut self) {
329        let mut deferred_construction_nodes = std::mem::take(&mut self.deferred_construction_nodes);
330
331        // Deduplicate deferred tasks by node_id to avoid redundant work
332        deferred_construction_nodes.sort_unstable_by_key(|task| task.node_id);
333        deferred_construction_nodes.dedup_by_key(|task| task.node_id);
334
335        #[cfg(feature = "parallel-construct")]
336        let iter = deferred_construction_nodes.into_par_iter();
337        #[cfg(not(feature = "parallel-construct"))]
338        let iter = deferred_construction_nodes.into_iter();
339
340        let results: Vec<ConstructionTaskResult> = iter
341            .map(|task: ConstructionTask| match task.data {
342                ConstructionTaskData::InlineLayout(mut layout) => {
343                    #[cfg(feature = "parallel-construct")]
344                    let mut layout_ctx = LAYOUT_CTX
345                        .take()
346                        .unwrap_or_else(|| Box::new(LayoutContext::new()));
347                    #[cfg(feature = "parallel-construct")]
348                    let layout_ctx_mut = &mut layout_ctx;
349
350                    #[cfg(feature = "parallel-construct")]
351                    let mut font_ctx = self
352                        .thread_font_contexts
353                        .get_or(|| RefCell::new(Box::new(self.font_ctx.lock().unwrap().clone())))
354                        .borrow_mut();
355                    #[cfg(feature = "parallel-construct")]
356                    let font_ctx_mut = &mut *font_ctx;
357
358                    #[cfg(not(feature = "parallel-construct"))]
359                    let layout_ctx_mut = &mut self.layout_ctx;
360                    #[cfg(not(feature = "parallel-construct"))]
361                    let font_ctx_mut = &mut *self.font_ctx.lock().unwrap();
362
363                    layout.content_widths = None;
364                    build_inline_layout_into(
365                        &self.nodes,
366                        layout_ctx_mut,
367                        font_ctx_mut,
368                        &mut layout,
369                        self.viewport.scale(),
370                        task.node_id,
371                    );
372
373                    #[cfg(feature = "parallel-construct")]
374                    {
375                        LAYOUT_CTX.set(Some(layout_ctx));
376                    }
377
378                    // If layout doesn't contain any inline boxes, then it is safe to populate the content_widths
379                    // cache during this parallelized stage.
380                    // if layout.layout.inline_boxes().is_empty() {
381                    //     layout.content_widths();
382                    // }
383
384                    ConstructionTaskResult {
385                        node_id: task.node_id,
386                        data: ConstructionTaskResultData::InlineLayout(layout),
387                    }
388                }
389            })
390            .collect();
391
392        for result in results {
393            match result.data {
394                ConstructionTaskResultData::InlineLayout(layout) => {
395                    self.nodes[result.node_id].cache.clear();
396                    self.nodes[result.node_id]
397                        .element_data_mut()
398                        .unwrap()
399                        .inline_layout_data = Some(layout);
400                }
401            }
402        }
403
404        self.deferred_construction_nodes.clear();
405    }
406
407    /// Walk the nodes now that they're properly styled and transfer their styles to the taffy style system
408    ///
409    /// TODO: update taffy to use an associated type instead of slab key
410    /// TODO: update taffy to support traited styles so we don't even need to rely on taffy for storage
411    pub fn resolve_layout(&mut self) {
412        let size = self.stylist.device().au_viewport_size();
413
414        let available_space = taffy::Size {
415            width: AvailableSpace::Definite(size.width.to_f32_px()),
416            height: AvailableSpace::Definite(size.height.to_f32_px()),
417        };
418
419        let root_element_id = taffy::NodeId::from(self.root_element().id);
420
421        // println!("\n\nRESOLVE LAYOUT\n===========\n");
422
423        taffy::compute_root_layout(self, root_element_id, available_space);
424        taffy::round_layout(self, root_element_id);
425
426        // Taffy currently maps CSS `position: fixed` to absolute positioning,
427        // which leaves the box relative to its DOM layout parent. A portal
428        // mounted after a full-height application root therefore starts one
429        // viewport below the window even with `top: 0`. Cancel the layout
430        // parent's document-space offset so fixed boxes use the viewport as
431        // their containing block, as CSS requires.
432        let fixed_nodes = self
433            .nodes
434            .iter()
435            .filter_map(|(node_id, node)| {
436                let is_fixed = node
437                    .primary_styles()
438                    .is_some_and(|style| style.clone_position() == Position::Fixed);
439                is_fixed.then_some((node_id, node.layout_parent.get()))
440            })
441            .collect::<Vec<_>>();
442
443        for (node_id, parent_id) in fixed_nodes {
444            let Some(parent_id) = parent_id else {
445                continue;
446            };
447            let parent_position = self.nodes[parent_id].absolute_position(0.0, 0.0);
448            self.nodes[node_id].final_layout.location.x -= parent_position.x;
449            self.nodes[node_id].final_layout.location.y -= parent_position.y;
450        }
451
452        // println!("\n\n");
453        // taffy::print_tree(self, root_node_id)
454    }
455}