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 parley::LayoutContext;
10use selectors::Element as _;
11use style::dom::TDocument;
12
13#[cfg(feature = "parallel-construct")]
14use rayon::prelude::*;
15
16// FIXME: static thread_local FontCtx isn't necessarily correct in multi-document context.
17// Should use thread_local crate with ThreadLocal value store in the Document.
18thread_local! {
19    pub(crate) static LAYOUT_CTX: RefCell<Option<Box<LayoutContext<TextBrush>>>> = const { RefCell::new(None) };
20}
21
22#[cfg(feature = "incremental")]
23use style::selector_parser::RestyleDamage;
24use taffy::AvailableSpace;
25
26use crate::{
27    BaseDocument, NON_INCREMENTAL,
28    events::ScrollAnimationState,
29    layout::{
30        construct::{
31            ConstructionTask, ConstructionTaskData, ConstructionTaskResult,
32            ConstructionTaskResultData, build_inline_layout_into, collect_layout_children,
33        },
34        damage::{ALL_DAMAGE, CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC},
35    },
36    node::TextBrush,
37};
38
39impl BaseDocument {
40    /// Restyle the tree and then relayout it
41    pub fn resolve(&mut self, current_time_for_animations: f64) {
42        if TDocument::as_node(&&self.nodes[0])
43            .first_element_child()
44            .is_none()
45        {
46            #[cfg(feature = "tracing")]
47            tracing::warn!("No DOM - not resolving");
48            return;
49        }
50
51        // Process messages that have been sent to our message channel (e.g. loaded resource)
52        self.handle_messages();
53
54        self.resolve_scroll_animation();
55
56        let root_node_id = self.root_element().id;
57        debug_timer!(timer, feature = "log_phase_times");
58
59        // we need to resolve stylist first since it will need to drive our layout bits
60        self.resolve_stylist(current_time_for_animations);
61        timer.record_time("style");
62
63        // Propagate damage flags (from mutation and restyles) up and down the tree
64        #[cfg(feature = "incremental")]
65        self.propagate_damage_flags(root_node_id, RestyleDamage::empty());
66        #[cfg(feature = "incremental")]
67        timer.record_time("damage");
68
69        // Fix up tree for layout (insert anonymous blocks as necessary, etc)
70        self.resolve_layout_children();
71        timer.record_time("construct");
72
73        self.resolve_deferred_tasks();
74        timer.record_time("pconstruct");
75
76        // Merge stylo into taffy
77        self.flush_styles_to_layout(root_node_id);
78        timer.record_time("flush");
79
80        // Next we resolve layout with the data resolved by stlist
81        self.resolve_layout();
82        timer.record_time("layout");
83
84        // Clear all damage and dirty flags
85        #[cfg(feature = "incremental")]
86        {
87            for (_, node) in self.nodes.iter_mut() {
88                node.clear_damage_mut();
89                node.unset_dirty_descendants();
90            }
91            timer.record_time("c_damage");
92        }
93
94        let mut subdoc_is_animating = false;
95        for &node_id in &self.sub_document_nodes {
96            let node = &mut self.nodes[node_id];
97            let size = node.final_layout.size;
98            if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
99                // Set viewport
100                // viewport_mut handles change detection. So we just unconditionally set the values;
101                let mut sub_viewport = sub_doc.viewport_mut();
102                sub_viewport.hidpi_scale = self.viewport.hidpi_scale;
103                sub_viewport.zoom = self.viewport.zoom;
104                sub_viewport.color_scheme = self.viewport.color_scheme;
105
106                let viewport_scale = self.viewport.scale();
107                sub_viewport.window_size = (
108                    (size.width * viewport_scale) as u32,
109                    (size.height * viewport_scale) as u32,
110                );
111                drop(sub_viewport);
112
113                sub_doc.resolve(current_time_for_animations);
114
115                subdoc_is_animating |= sub_doc.is_animating();
116            }
117        }
118        self.subdoc_is_animating = subdoc_is_animating;
119        timer.record_time("subdocs");
120
121        timer.print_times(&format!("Resolve({}): ", self.id()));
122    }
123
124    pub fn resolve_scroll_animation(&mut self) {
125        match &mut self.scroll_animation {
126            ScrollAnimationState::Fling(fling_state) => {
127                let time_ms = SystemTime::now()
128                    .duration_since(UNIX_EPOCH)
129                    .unwrap()
130                    .as_millis() as u64 as f64;
131
132                let time_diff_ms = time_ms - fling_state.last_seen_time;
133
134                // 0.95 @ 60fps normalized to actual frame times
135                let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
136
137                fling_state.x_velocity *= deceleration;
138                fling_state.y_velocity *= deceleration;
139                fling_state.last_seen_time = time_ms;
140                let fling_state = fling_state.clone();
141
142                let dx = fling_state.x_velocity * time_diff_ms;
143                let dy = fling_state.y_velocity * time_diff_ms;
144
145                self.scroll_by(Some(fling_state.target), dx, dy, &mut |_| {});
146                if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
147                    self.scroll_animation = ScrollAnimationState::None;
148                }
149            }
150            ScrollAnimationState::None => {
151                // Do nothing
152            }
153        }
154    }
155
156    /// Ensure that the layout_children field is populated for all nodes
157    pub fn resolve_layout_children(&mut self) {
158        resolve_layout_children_recursive(self, self.root_node().id);
159
160        fn resolve_layout_children_recursive(doc: &mut BaseDocument, node_id: usize) {
161            // Anonymous blocks and pseudo-elements can be removed from the slab
162            // between render passes. Bail out rather than panicking on a stale key.
163            if doc.nodes.get(node_id).is_none() {
164                return;
165            }
166
167            let mut damage = doc.nodes[node_id].damage().unwrap_or(ALL_DAMAGE);
168            let _flags = doc.nodes[node_id].flags;
169
170            if NON_INCREMENTAL || damage.intersects(CONSTRUCT_FC | CONSTRUCT_BOX) {
171                //} || flags.contains(NodeFlags::IS_INLINE_ROOT) {
172                let mut layout_children = Vec::new();
173                let mut anonymous_block: Option<usize> = None;
174                collect_layout_children(doc, node_id, &mut layout_children, &mut anonymous_block);
175
176                // Recurse into newly collected layout children
177                for child_id in layout_children.iter().copied() {
178                    resolve_layout_children_recursive(doc, child_id);
179                    doc.nodes[child_id].layout_parent.set(Some(node_id));
180                    if let Some(mut data) = doc.nodes[child_id].stylo_element_data.get_mut() {
181                        data.damage
182                            .remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
183                    }
184                }
185
186                *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children.clone());
187                // *doc.nodes[node_id].paint_children.borrow_mut() = Some(layout_children);
188
189                damage.remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
190                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
191            } else {
192                //if damage.contains(CONSTRUCT_DESCENDENT) {
193                let layout_children = doc.nodes[node_id].layout_children.borrow_mut().take();
194                if let Some(layout_children) = layout_children {
195                    for child_id in layout_children.iter().copied() {
196                        // Anonymous blocks and pseudo-elements can be removed from the
197                        // slab between render passes; skip stale IDs.
198                        if !doc.nodes.contains(child_id) {
199                            continue;
200                        }
201                        resolve_layout_children_recursive(doc, child_id);
202                        doc.nodes[child_id].layout_parent.set(Some(node_id));
203                    }
204
205                    *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children);
206                }
207
208                // damage.remove(CONSTRUCT_DESCENDENT);
209                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
210            }
211
212            doc.nodes[node_id].set_damage(damage);
213        }
214    }
215
216    pub fn resolve_deferred_tasks(&mut self) {
217        let mut deferred_construction_nodes = std::mem::take(&mut self.deferred_construction_nodes);
218
219        // Deduplicate deferred tasks by node_id to avoid redundant work
220        deferred_construction_nodes.sort_unstable_by_key(|task| task.node_id);
221        deferred_construction_nodes.dedup_by_key(|task| task.node_id);
222
223        #[cfg(feature = "parallel-construct")]
224        let iter = deferred_construction_nodes.into_par_iter();
225        #[cfg(not(feature = "parallel-construct"))]
226        let iter = deferred_construction_nodes.into_iter();
227
228        let results: Vec<ConstructionTaskResult> = iter
229            .map(|task: ConstructionTask| match task.data {
230                ConstructionTaskData::InlineLayout(mut layout) => {
231                    #[cfg(feature = "parallel-construct")]
232                    let mut layout_ctx = LAYOUT_CTX
233                        .take()
234                        .unwrap_or_else(|| Box::new(LayoutContext::new()));
235                    #[cfg(feature = "parallel-construct")]
236                    let layout_ctx_mut = &mut layout_ctx;
237
238                    #[cfg(feature = "parallel-construct")]
239                    let mut font_ctx = self
240                        .thread_font_contexts
241                        .get_or(|| RefCell::new(Box::new(self.font_ctx.lock().unwrap().clone())))
242                        .borrow_mut();
243                    #[cfg(feature = "parallel-construct")]
244                    let font_ctx_mut = &mut *font_ctx;
245
246                    #[cfg(not(feature = "parallel-construct"))]
247                    let layout_ctx_mut = &mut self.layout_ctx;
248                    #[cfg(not(feature = "parallel-construct"))]
249                    let font_ctx_mut = &mut *self.font_ctx.lock().unwrap();
250
251                    layout.content_widths = None;
252                    build_inline_layout_into(
253                        &self.nodes,
254                        layout_ctx_mut,
255                        font_ctx_mut,
256                        &mut layout,
257                        self.viewport.scale(),
258                        task.node_id,
259                    );
260
261                    #[cfg(feature = "parallel-construct")]
262                    {
263                        LAYOUT_CTX.set(Some(layout_ctx));
264                    }
265
266                    // If layout doesn't contain any inline boxes, then it is safe to populate the content_widths
267                    // cache during this parallelized stage.
268                    // if layout.layout.inline_boxes().is_empty() {
269                    //     layout.content_widths();
270                    // }
271
272                    ConstructionTaskResult {
273                        node_id: task.node_id,
274                        data: ConstructionTaskResultData::InlineLayout(layout),
275                    }
276                }
277            })
278            .collect();
279
280        for result in results {
281            match result.data {
282                ConstructionTaskResultData::InlineLayout(layout) => {
283                    self.nodes[result.node_id].cache.clear();
284                    self.nodes[result.node_id]
285                        .element_data_mut()
286                        .unwrap()
287                        .inline_layout_data = Some(layout);
288                }
289            }
290        }
291
292        self.deferred_construction_nodes.clear();
293    }
294
295    /// Walk the nodes now that they're properly styled and transfer their styles to the taffy style system
296    ///
297    /// TODO: update taffy to use an associated type instead of slab key
298    /// TODO: update taffy to support traited styles so we don't even need to rely on taffy for storage
299    pub fn resolve_layout(&mut self) {
300        let size = self.stylist.device().au_viewport_size();
301
302        let available_space = taffy::Size {
303            width: AvailableSpace::Definite(size.width.to_f32_px()),
304            height: AvailableSpace::Definite(size.height.to_f32_px()),
305        };
306
307        let root_element_id = taffy::NodeId::from(self.root_element().id);
308
309        // println!("\n\nRESOLVE LAYOUT\n===========\n");
310
311        taffy::compute_root_layout(self, root_element_id, available_space);
312        taffy::round_layout(self, root_element_id);
313
314        // println!("\n\n");
315        // taffy::print_tree(self, root_node_id)
316    }
317}