Skip to main content

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::selector_parser::RestyleDamage;
24use taffy::AvailableSpace;
25
26use crate::{
27    BaseDocument,
28    events::ScrollAnimationState,
29    layout::{
30        construct::{
31            ConstructionTask, ConstructionTaskData, ConstructionTaskResult,
32            ConstructionTaskResultData, LayoutChildren, build_inline_layout_into,
33            collect_layout_children,
34        },
35        damage::{ALL_DAMAGE, CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC},
36    },
37    node::TextBrush,
38};
39
40impl BaseDocument {
41    /// Restyle the tree and then relayout it
42    pub fn resolve(&mut self, current_time_for_animations: f64) {
43        if TDocument::as_node(&&self.nodes[0])
44            .first_element_child()
45            .is_none()
46        {
47            #[cfg(feature = "tracing")]
48            tracing::warn!("No DOM - not resolving");
49            return;
50        }
51
52        // Process messages that have been sent to our message channel (e.g. loaded resource)
53        self.handle_messages();
54
55        self.resolve_scroll_animation();
56
57        // Drop scrollbar-activity entries whose fade-out has finished (also
58        // sheds entries for removed nodes).
59        {
60            use crate::node::scrollbar::{FADE_DELAY, FADE_DURATION};
61            self.scrollbar_activity
62                .retain(|_, last| last.elapsed() < FADE_DELAY + FADE_DURATION);
63        }
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        timer.print_times(&format!("Resolve({}): ", self.id()));
133    }
134
135    fn resolve_transforms(&mut self, node_id: usize) -> Rect {
136        if !self.nodes.contains(node_id) {
137            return Rect::ZERO;
138        }
139
140        if !self.nodes[node_id]
141            .damage()
142            .map(|d| d.contains(style::selector_parser::RestyleDamage::RECALCULATE_OVERFLOW))
143            .unwrap_or(false)
144        {
145            return self.nodes[node_id].scrollable_overflow;
146        }
147
148        let scale = self.viewport.scale_f64();
149
150        let transform = self.nodes[node_id].set_transform(scale as f32);
151
152        let w = self.nodes[node_id].final_layout.size.width as f64 * scale;
153        let h = self.nodes[node_id].final_layout.size.height as f64 * scale;
154        let mut overflow = Rect::new(0.0, 0.0, w, h);
155
156        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
157
158        if let Some(ref children) = layout_children {
159            for &child_id in children {
160                let child_rect_in_self = self.resolve_transforms(child_id);
161                overflow = overflow.union(child_rect_in_self);
162            }
163        }
164        if let Some(before) = self.nodes[node_id].before {
165            let child_rect_in_self = self.resolve_transforms(before);
166            overflow = overflow.union(child_rect_in_self);
167        }
168        if let Some(after) = self.nodes[node_id].after {
169            let child_rect_in_self = self.resolve_transforms(after);
170            overflow = overflow.union(child_rect_in_self);
171        }
172
173        self.nodes[node_id].scrollable_overflow = overflow;
174        *self.nodes[node_id].layout_children.get_mut() = layout_children;
175
176        let scaled_x = self.nodes[node_id].final_layout.location.x as f64 * scale;
177        let scaled_y = self.nodes[node_id].final_layout.location.y as f64 * scale;
178
179        let full = if let Some(t) = transform {
180            Affine::translate((scaled_x, scaled_y)) * t
181        } else {
182            Affine::translate((scaled_x, scaled_y))
183        };
184
185        full.transform_rect_bbox(overflow)
186    }
187
188    pub fn resolve_scroll_animation(&mut self) {
189        match &mut self.scroll_animation {
190            ScrollAnimationState::Fling(fling_state) => {
191                let time_ms = SystemTime::now()
192                    .duration_since(UNIX_EPOCH)
193                    .unwrap()
194                    .as_millis() as u64 as f64;
195
196                let time_diff_ms = time_ms - fling_state.last_seen_time;
197
198                // 0.95 @ 60fps normalized to actual frame times
199                let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
200
201                fling_state.x_velocity *= deceleration;
202                fling_state.y_velocity *= deceleration;
203                fling_state.last_seen_time = time_ms;
204                let fling_state = fling_state.clone();
205
206                let dx = fling_state.x_velocity * time_diff_ms;
207                let dy = fling_state.y_velocity * time_diff_ms;
208
209                self.scroll_by(Some(fling_state.target), dx, dy, &mut |_| {});
210                if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
211                    self.scroll_animation = ScrollAnimationState::None;
212                }
213            }
214            ScrollAnimationState::None => {
215                // Do nothing
216            }
217        }
218    }
219
220    /// Ensure that the layout_children field is populated for all nodes
221    pub fn resolve_layout_children(&mut self) {
222        resolve_layout_children_recursive(self, self.root_node().id);
223
224        fn resolve_layout_children_recursive(doc: &mut BaseDocument, node_id: usize) {
225            // Anonymous blocks and pseudo-elements can be removed from the slab
226            // between render passes. Bail out rather than panicking on a stale key.
227            if doc.nodes.get(node_id).is_none() {
228                return;
229            }
230
231            let mut damage = doc.nodes[node_id].damage().unwrap_or(ALL_DAMAGE);
232            let _flags = doc.nodes[node_id].flags;
233
234            if !doc.incremental_layout || damage.intersects(CONSTRUCT_FC | CONSTRUCT_BOX) {
235                //} || flags.contains(NodeFlags::IS_INLINE_ROOT) {
236                let mut collected = LayoutChildren::default();
237                collect_layout_children(doc, node_id, &mut collected);
238                let layout_children = collected.children;
239
240                // Recurse into newly collected layout children
241                for child_id in layout_children.iter().copied() {
242                    resolve_layout_children_recursive(doc, child_id);
243                    doc.nodes[child_id].layout_parent.set(Some(node_id));
244                    if let Some(mut data) = doc.nodes[child_id].stylo_element_data.get_mut() {
245                        data.damage
246                            .remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
247                    }
248                }
249
250                *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children.clone());
251                // *doc.nodes[node_id].paint_children.borrow_mut() = Some(layout_children);
252
253                damage.remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
254                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
255            } else {
256                //if damage.contains(CONSTRUCT_DESCENDENT) {
257                let layout_children = doc.nodes[node_id].layout_children.borrow_mut().take();
258                if let Some(layout_children) = layout_children {
259                    for child_id in layout_children.iter().copied() {
260                        // Anonymous blocks and pseudo-elements can be removed from the
261                        // slab between render passes; skip stale IDs.
262                        if !doc.nodes.contains(child_id) {
263                            continue;
264                        }
265                        resolve_layout_children_recursive(doc, child_id);
266                        doc.nodes[child_id].layout_parent.set(Some(node_id));
267                    }
268
269                    *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children);
270                }
271
272                // damage.remove(CONSTRUCT_DESCENDENT);
273                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
274            }
275
276            doc.nodes[node_id].set_damage(damage);
277        }
278    }
279
280    pub fn resolve_deferred_tasks(&mut self) {
281        let mut deferred_construction_nodes = std::mem::take(&mut self.deferred_construction_nodes);
282
283        // Deduplicate deferred tasks by node_id to avoid redundant work
284        deferred_construction_nodes.sort_unstable_by_key(|task| task.node_id);
285        deferred_construction_nodes.dedup_by_key(|task| task.node_id);
286
287        #[cfg(feature = "parallel-construct")]
288        let iter = deferred_construction_nodes.into_par_iter();
289        #[cfg(not(feature = "parallel-construct"))]
290        let iter = deferred_construction_nodes.into_iter();
291
292        let results: Vec<ConstructionTaskResult> = iter
293            .map(|task: ConstructionTask| match task.data {
294                ConstructionTaskData::InlineLayout(mut layout) => {
295                    #[cfg(feature = "parallel-construct")]
296                    let mut layout_ctx = LAYOUT_CTX
297                        .take()
298                        .unwrap_or_else(|| Box::new(LayoutContext::new()));
299                    #[cfg(feature = "parallel-construct")]
300                    let layout_ctx_mut = &mut layout_ctx;
301
302                    #[cfg(feature = "parallel-construct")]
303                    let mut font_ctx = self
304                        .thread_font_contexts
305                        .get_or(|| RefCell::new(Box::new(self.font_ctx.lock().unwrap().clone())))
306                        .borrow_mut();
307                    #[cfg(feature = "parallel-construct")]
308                    let font_ctx_mut = &mut *font_ctx;
309
310                    #[cfg(not(feature = "parallel-construct"))]
311                    let layout_ctx_mut = &mut self.layout_ctx;
312                    #[cfg(not(feature = "parallel-construct"))]
313                    let font_ctx_mut = &mut *self.font_ctx.lock().unwrap();
314
315                    layout.content_widths = None;
316                    build_inline_layout_into(
317                        &self.nodes,
318                        layout_ctx_mut,
319                        font_ctx_mut,
320                        &mut layout,
321                        self.viewport.scale(),
322                        task.node_id,
323                    );
324
325                    #[cfg(feature = "parallel-construct")]
326                    {
327                        LAYOUT_CTX.set(Some(layout_ctx));
328                    }
329
330                    // If layout doesn't contain any inline boxes, then it is safe to populate the content_widths
331                    // cache during this parallelized stage.
332                    // if layout.layout.inline_boxes().is_empty() {
333                    //     layout.content_widths();
334                    // }
335
336                    ConstructionTaskResult {
337                        node_id: task.node_id,
338                        data: ConstructionTaskResultData::InlineLayout(layout),
339                    }
340                }
341            })
342            .collect();
343
344        for result in results {
345            match result.data {
346                ConstructionTaskResultData::InlineLayout(layout) => {
347                    self.nodes[result.node_id].cache.clear();
348                    self.nodes[result.node_id]
349                        .element_data_mut()
350                        .unwrap()
351                        .inline_layout_data = Some(layout);
352                }
353            }
354        }
355
356        self.deferred_construction_nodes.clear();
357    }
358
359    /// Walk the nodes now that they're properly styled and transfer their styles to the taffy style system
360    ///
361    /// TODO: update taffy to use an associated type instead of slab key
362    /// TODO: update taffy to support traited styles so we don't even need to rely on taffy for storage
363    pub fn resolve_layout(&mut self) {
364        let size = self.stylist.device().au_viewport_size();
365
366        let available_space = taffy::Size {
367            width: AvailableSpace::Definite(size.width.to_f32_px()),
368            height: AvailableSpace::Definite(size.height.to_f32_px()),
369        };
370
371        let root_element_id = taffy::NodeId::from(self.root_element().id);
372
373        // println!("\n\nRESOLVE LAYOUT\n===========\n");
374
375        taffy::compute_root_layout(self, root_element_id, available_space);
376        taffy::round_layout(self, root_element_id);
377
378        // println!("\n\n");
379        // taffy::print_tree(self, root_node_id)
380    }
381}