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
23#[cfg(feature = "incremental")]
24use style::selector_parser::RestyleDamage;
25use taffy::AvailableSpace;
26
27use crate::{
28    BaseDocument, NON_INCREMENTAL,
29    events::ScrollAnimationState,
30    layout::{
31        construct::{
32            ConstructionTask, ConstructionTaskData, ConstructionTaskResult,
33            ConstructionTaskResultData, build_inline_layout_into, 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        let root_node_id = self.root_element().id;
58        debug_timer!(timer, feature = "log_phase_times");
59
60        // we need to resolve stylist first since it will need to drive our layout bits
61        self.resolve_stylist(current_time_for_animations);
62        timer.record_time("style");
63
64        // Propagate damage flags (from mutation and restyles) up and down the tree
65        #[cfg(feature = "incremental")]
66        self.propagate_damage_flags(root_node_id, RestyleDamage::empty());
67        #[cfg(feature = "incremental")]
68        timer.record_time("damage");
69
70        // Fix up tree for layout (insert anonymous blocks as necessary, etc)
71        self.resolve_layout_children();
72        timer.record_time("construct");
73
74        self.resolve_deferred_tasks();
75        timer.record_time("pconstruct");
76
77        // Merge stylo into taffy
78        self.flush_styles_to_layout(root_node_id);
79        timer.record_time("flush");
80
81        // Next we resolve layout with the data resolved by stlist
82        self.resolve_layout();
83        timer.record_time("layout");
84
85        self.resolve_transforms(root_node_id);
86        timer.record_time("transform");
87
88        // Clear all damage and dirty flags
89        #[cfg(feature = "incremental")]
90        {
91            for (_, node) in self.nodes.iter_mut() {
92                node.clear_damage_mut();
93                node.unset_dirty_descendants();
94            }
95            timer.record_time("c_damage");
96        }
97
98        let mut subdoc_is_animating = false;
99        for &node_id in &self.sub_document_nodes {
100            let node = &mut self.nodes[node_id];
101            let size = node.final_layout.size;
102            if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
103                // Set viewport
104                // viewport_mut handles change detection. So we just unconditionally set the values;
105                let mut sub_viewport = sub_doc.viewport_mut();
106                sub_viewport.hidpi_scale = self.viewport.hidpi_scale;
107                sub_viewport.zoom = self.viewport.zoom;
108                sub_viewport.color_scheme = self.viewport.color_scheme;
109
110                let viewport_scale = self.viewport.scale();
111                sub_viewport.window_size = (
112                    (size.width * viewport_scale) as u32,
113                    (size.height * viewport_scale) as u32,
114                );
115                drop(sub_viewport);
116
117                sub_doc.resolve(current_time_for_animations);
118
119                subdoc_is_animating |= sub_doc.is_animating();
120            }
121        }
122        self.subdoc_is_animating = subdoc_is_animating;
123        timer.record_time("subdocs");
124
125        timer.print_times(&format!("Resolve({}): ", self.id()));
126    }
127
128    fn resolve_transforms(&mut self, node_id: usize) -> Rect {
129        if !self.nodes.contains(node_id) {
130            return Rect::ZERO;
131        }
132
133        if !self.nodes[node_id]
134            .damage()
135            .map(|d| d.contains(style::selector_parser::RestyleDamage::RECALCULATE_OVERFLOW))
136            .unwrap_or(false)
137        {
138            return self.nodes[node_id].scrollable_overflow;
139        }
140
141        let scale = self.viewport.scale_f64();
142
143        let transform = self.nodes[node_id].set_transform(scale as f32);
144
145        let w = self.nodes[node_id].final_layout.size.width as f64 * scale;
146        let h = self.nodes[node_id].final_layout.size.height as f64 * scale;
147        let mut overflow = Rect::new(0.0, 0.0, w, h);
148
149        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
150
151        if let Some(ref children) = layout_children {
152            for &child_id in children {
153                let child_rect_in_self = self.resolve_transforms(child_id);
154                overflow = overflow.union(child_rect_in_self);
155            }
156        }
157        if let Some(before) = self.nodes[node_id].before {
158            let child_rect_in_self = self.resolve_transforms(before);
159            overflow = overflow.union(child_rect_in_self);
160        }
161        if let Some(after) = self.nodes[node_id].after {
162            let child_rect_in_self = self.resolve_transforms(after);
163            overflow = overflow.union(child_rect_in_self);
164        }
165
166        self.nodes[node_id].scrollable_overflow = overflow;
167        *self.nodes[node_id].layout_children.get_mut() = layout_children;
168
169        let scaled_x = self.nodes[node_id].final_layout.location.x as f64 * scale;
170        let scaled_y = self.nodes[node_id].final_layout.location.y as f64 * scale;
171
172        let full = if let Some(t) = transform {
173            Affine::translate((scaled_x, scaled_y)) * t
174        } else {
175            Affine::translate((scaled_x, scaled_y))
176        };
177
178        full.transform_rect_bbox(overflow)
179    }
180
181    pub fn resolve_scroll_animation(&mut self) {
182        match &mut self.scroll_animation {
183            ScrollAnimationState::Fling(fling_state) => {
184                let time_ms = SystemTime::now()
185                    .duration_since(UNIX_EPOCH)
186                    .unwrap()
187                    .as_millis() as u64 as f64;
188
189                let time_diff_ms = time_ms - fling_state.last_seen_time;
190
191                // 0.95 @ 60fps normalized to actual frame times
192                let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
193
194                fling_state.x_velocity *= deceleration;
195                fling_state.y_velocity *= deceleration;
196                fling_state.last_seen_time = time_ms;
197                let fling_state = fling_state.clone();
198
199                let dx = fling_state.x_velocity * time_diff_ms;
200                let dy = fling_state.y_velocity * time_diff_ms;
201
202                self.scroll_by(Some(fling_state.target), dx, dy, &mut |_| {});
203                if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
204                    self.scroll_animation = ScrollAnimationState::None;
205                }
206            }
207            ScrollAnimationState::None => {
208                // Do nothing
209            }
210        }
211    }
212
213    /// Ensure that the layout_children field is populated for all nodes
214    pub fn resolve_layout_children(&mut self) {
215        resolve_layout_children_recursive(self, self.root_node().id);
216
217        fn resolve_layout_children_recursive(doc: &mut BaseDocument, node_id: usize) {
218            // Anonymous blocks and pseudo-elements can be removed from the slab
219            // between render passes. Bail out rather than panicking on a stale key.
220            if doc.nodes.get(node_id).is_none() {
221                return;
222            }
223
224            let mut damage = doc.nodes[node_id].damage().unwrap_or(ALL_DAMAGE);
225            let _flags = doc.nodes[node_id].flags;
226
227            if NON_INCREMENTAL || damage.intersects(CONSTRUCT_FC | CONSTRUCT_BOX) {
228                //} || flags.contains(NodeFlags::IS_INLINE_ROOT) {
229                let mut layout_children = Vec::new();
230                let mut anonymous_block: Option<usize> = None;
231                collect_layout_children(doc, node_id, &mut layout_children, &mut anonymous_block);
232
233                // Recurse into newly collected layout children
234                for child_id in layout_children.iter().copied() {
235                    resolve_layout_children_recursive(doc, child_id);
236                    doc.nodes[child_id].layout_parent.set(Some(node_id));
237                    if let Some(mut data) = doc.nodes[child_id].stylo_element_data.get_mut() {
238                        data.damage
239                            .remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
240                    }
241                }
242
243                *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children.clone());
244                // *doc.nodes[node_id].paint_children.borrow_mut() = Some(layout_children);
245
246                damage.remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
247                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
248            } else {
249                //if damage.contains(CONSTRUCT_DESCENDENT) {
250                let layout_children = doc.nodes[node_id].layout_children.borrow_mut().take();
251                if let Some(layout_children) = layout_children {
252                    for child_id in layout_children.iter().copied() {
253                        // Anonymous blocks and pseudo-elements can be removed from the
254                        // slab between render passes; skip stale IDs.
255                        if !doc.nodes.contains(child_id) {
256                            continue;
257                        }
258                        resolve_layout_children_recursive(doc, child_id);
259                        doc.nodes[child_id].layout_parent.set(Some(node_id));
260                    }
261
262                    *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children);
263                }
264
265                // damage.remove(CONSTRUCT_DESCENDENT);
266                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
267            }
268
269            doc.nodes[node_id].set_damage(damage);
270        }
271    }
272
273    pub fn resolve_deferred_tasks(&mut self) {
274        let mut deferred_construction_nodes = std::mem::take(&mut self.deferred_construction_nodes);
275
276        // Deduplicate deferred tasks by node_id to avoid redundant work
277        deferred_construction_nodes.sort_unstable_by_key(|task| task.node_id);
278        deferred_construction_nodes.dedup_by_key(|task| task.node_id);
279
280        #[cfg(feature = "parallel-construct")]
281        let iter = deferred_construction_nodes.into_par_iter();
282        #[cfg(not(feature = "parallel-construct"))]
283        let iter = deferred_construction_nodes.into_iter();
284
285        let results: Vec<ConstructionTaskResult> = iter
286            .map(|task: ConstructionTask| match task.data {
287                ConstructionTaskData::InlineLayout(mut layout) => {
288                    #[cfg(feature = "parallel-construct")]
289                    let mut layout_ctx = LAYOUT_CTX
290                        .take()
291                        .unwrap_or_else(|| Box::new(LayoutContext::new()));
292                    #[cfg(feature = "parallel-construct")]
293                    let layout_ctx_mut = &mut layout_ctx;
294
295                    #[cfg(feature = "parallel-construct")]
296                    let mut font_ctx = self
297                        .thread_font_contexts
298                        .get_or(|| RefCell::new(Box::new(self.font_ctx.lock().unwrap().clone())))
299                        .borrow_mut();
300                    #[cfg(feature = "parallel-construct")]
301                    let font_ctx_mut = &mut *font_ctx;
302
303                    #[cfg(not(feature = "parallel-construct"))]
304                    let layout_ctx_mut = &mut self.layout_ctx;
305                    #[cfg(not(feature = "parallel-construct"))]
306                    let font_ctx_mut = &mut *self.font_ctx.lock().unwrap();
307
308                    layout.content_widths = None;
309                    build_inline_layout_into(
310                        &self.nodes,
311                        layout_ctx_mut,
312                        font_ctx_mut,
313                        &mut layout,
314                        self.viewport.scale(),
315                        task.node_id,
316                    );
317
318                    #[cfg(feature = "parallel-construct")]
319                    {
320                        LAYOUT_CTX.set(Some(layout_ctx));
321                    }
322
323                    // If layout doesn't contain any inline boxes, then it is safe to populate the content_widths
324                    // cache during this parallelized stage.
325                    // if layout.layout.inline_boxes().is_empty() {
326                    //     layout.content_widths();
327                    // }
328
329                    ConstructionTaskResult {
330                        node_id: task.node_id,
331                        data: ConstructionTaskResultData::InlineLayout(layout),
332                    }
333                }
334            })
335            .collect();
336
337        for result in results {
338            match result.data {
339                ConstructionTaskResultData::InlineLayout(layout) => {
340                    self.nodes[result.node_id].cache.clear();
341                    self.nodes[result.node_id]
342                        .element_data_mut()
343                        .unwrap()
344                        .inline_layout_data = Some(layout);
345                }
346            }
347        }
348
349        self.deferred_construction_nodes.clear();
350    }
351
352    /// Walk the nodes now that they're properly styled and transfer their styles to the taffy style system
353    ///
354    /// TODO: update taffy to use an associated type instead of slab key
355    /// TODO: update taffy to support traited styles so we don't even need to rely on taffy for storage
356    pub fn resolve_layout(&mut self) {
357        let size = self.stylist.device().au_viewport_size();
358
359        let available_space = taffy::Size {
360            width: AvailableSpace::Definite(size.width.to_f32_px()),
361            height: AvailableSpace::Definite(size.height.to_f32_px()),
362        };
363
364        let root_element_id = taffy::NodeId::from(self.root_element().id);
365
366        // println!("\n\nRESOLVE LAYOUT\n===========\n");
367
368        taffy::compute_root_layout(self, root_element_id, available_space);
369        taffy::round_layout(self, root_element_id);
370
371        // println!("\n\n");
372        // taffy::print_tree(self, root_node_id)
373    }
374}