Skip to main content

bliss_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            println!("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        self.resolve_scroll_animation();
54
55        // D-2c-followup: root_element widened to Option<&Node>. The early
56        // `is_none()` guard above ensures this branch is only taken when a root
57        // element exists, so `unwrap_or(0)` is dead — but it's still the
58        // compiler requirement. The guard above is now LOAD-BEARING for the
59        // unwrap_or(0) safety: deleting it would silently propagate `0` to
60        // `propagate_damage_flags` / `flush_styles_to_layout`, which would
61        // compute layout against the document root node (id 0, an empty
62        // layout block) producing garbage layout with no panic. The
63        // debug_assert below catches a future refactor that breaks this
64        // invariant on day 1.
65        debug_assert!(
66            self.root_element().is_some(),
67            "resolve: 'no DOM' guard above lost its effect — root_element became None when an element child was expected"
68        );
69        let root_node_id = self.root_element().map(|r| r.id).unwrap_or(0);
70        debug_timer!(timer, feature = "log_phase_times");
71
72        // we need to resolve stylist first since it will need to drive our layout bits
73        self.resolve_stylist(current_time_for_animations);
74        timer.record_time("style");
75
76        // Propagate damage flags (from mutation and restyles) up and down the tree
77        #[cfg(feature = "incremental")]
78        self.propagate_damage_flags(root_node_id, RestyleDamage::empty());
79        #[cfg(feature = "incremental")]
80        timer.record_time("damage");
81
82        // Fix up tree for layout (insert anonymous blocks as necessary, etc)
83        self.resolve_layout_children();
84        timer.record_time("construct");
85
86        self.resolve_deferred_tasks();
87        timer.record_time("pconstruct");
88
89        // Merge stylo into taffy
90        self.flush_styles_to_layout(root_node_id);
91        timer.record_time("flush");
92
93        // Next we resolve layout with the data resolved by stlist
94        self.resolve_layout();
95        timer.record_time("layout");
96
97        // Clear all damage and dirty flags
98        #[cfg(feature = "incremental")]
99        {
100            for (_, node) in self.nodes.iter_mut() {
101                node.clear_damage_mut();
102                node.unset_dirty_descendants();
103            }
104            timer.record_time("c_damage");
105        }
106
107        let mut subdoc_is_animating = false;
108        for &node_id in &self.sub_document_nodes {
109            let node = &mut self.nodes[node_id];
110            let size = node.final_layout.size;
111            if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
112                // Set viewport
113                // viewport_mut handles change detection. So we just unconditionally set the values;
114                let mut sub_viewport = sub_doc.viewport_mut();
115                sub_viewport.hidpi_scale = self.viewport.hidpi_scale;
116                sub_viewport.zoom = self.viewport.zoom;
117                sub_viewport.color_scheme = self.viewport.color_scheme;
118
119                let viewport_scale = self.viewport.scale();
120                sub_viewport.window_size = (
121                    (size.width * viewport_scale) as u32,
122                    (size.height * viewport_scale) as u32,
123                );
124                drop(sub_viewport);
125
126                sub_doc.resolve(current_time_for_animations);
127
128                subdoc_is_animating |= sub_doc.is_animating();
129            }
130        }
131        self.subdoc_is_animating = subdoc_is_animating;
132        timer.record_time("subdocs");
133
134        timer.print_times(&format!("Resolve({}): ", self.id()));
135    }
136
137    pub fn resolve_scroll_animation(&mut self) {
138        match &mut self.scroll_animation {
139            ScrollAnimationState::Fling(fling_state) => {
140                let time_ms = SystemTime::now()
141                    .duration_since(UNIX_EPOCH)
142                    .unwrap_or_default()
143                    .as_millis() as u64 as f64;
144
145                let time_diff_ms = time_ms - fling_state.last_seen_time;
146
147                // 0.95 @ 60fps normalized to actual frame times
148                let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
149
150                fling_state.x_velocity *= deceleration;
151                fling_state.y_velocity *= deceleration;
152                fling_state.last_seen_time = time_ms;
153                let fling_state = fling_state.clone();
154
155                let dx = fling_state.x_velocity * time_diff_ms;
156                let dy = fling_state.y_velocity * time_diff_ms;
157
158                self.scroll_by(Some(fling_state.target), dx, dy, &mut |_| {});
159                if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
160                    self.scroll_animation = ScrollAnimationState::None;
161                }
162            }
163            ScrollAnimationState::None => {
164                // Do nothing
165            }
166        }
167    }
168
169    /// Ensure that the layout_children field is populated for all nodes
170    pub fn resolve_layout_children(&mut self) {
171        resolve_layout_children_recursive(self, self.root_node().id);
172
173        fn resolve_layout_children_recursive(doc: &mut BaseDocument, node_id: usize) {
174            let mut damage = doc.nodes[node_id].damage().unwrap_or(ALL_DAMAGE);
175            let _flags = doc.nodes[node_id].flags;
176
177            if NON_INCREMENTAL || damage.intersects(CONSTRUCT_FC | CONSTRUCT_BOX) {
178                //} || flags.contains(NodeFlags::IS_INLINE_ROOT) {
179                let mut layout_children = Vec::new();
180                let mut anonymous_block: Option<usize> = None;
181                collect_layout_children(doc, node_id, &mut layout_children, &mut anonymous_block);
182
183                // Recurse into newly collected layout children
184                for child_id in layout_children.iter().copied() {
185                    resolve_layout_children_recursive(doc, child_id);
186                    doc.nodes[child_id].layout_parent.set(Some(node_id));
187                    if let Some(data) = doc.nodes[child_id].stylo_element_data.get_mut() {
188                        data.damage
189                            .remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
190                    }
191                }
192
193                *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children.clone());
194                // *doc.nodes[node_id].paint_children.borrow_mut() = Some(layout_children);
195
196                damage.remove(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
197                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
198            } else {
199                //if damage.contains(CONSTRUCT_DESCENDENT) {
200                let layout_children = doc.nodes[node_id].layout_children.borrow_mut().take();
201                if let Some(layout_children) = layout_children {
202                    // Recurse into previously computed layout children
203                    for child_id in layout_children.iter().copied() {
204                        resolve_layout_children_recursive(doc, child_id);
205                        doc.nodes[child_id].layout_parent.set(Some(node_id));
206                    }
207
208                    *doc.nodes[node_id].layout_children.borrow_mut() = Some(layout_children);
209                }
210
211                // damage.remove(CONSTRUCT_DESCENDENT);
212                // damage.insert(RestyleDamage::RELAYOUT | RestyleDamage::REPAINT);
213            }
214
215            doc.nodes[node_id].set_damage(damage);
216        }
217    }
218
219    pub fn resolve_deferred_tasks(&mut self) {
220        let mut deferred_construction_nodes = std::mem::take(&mut self.deferred_construction_nodes);
221
222        // Deduplicate deferred tasks by node_id to avoid redundant work
223        deferred_construction_nodes.sort_unstable_by_key(|task| task.node_id);
224        deferred_construction_nodes.dedup_by_key(|task| task.node_id);
225
226        #[cfg(feature = "parallel-construct")]
227        let iter = deferred_construction_nodes.into_par_iter();
228        #[cfg(not(feature = "parallel-construct"))]
229        let iter = deferred_construction_nodes.into_iter();
230
231        let results: Vec<ConstructionTaskResult> = iter
232            .map(|task: ConstructionTask| match task.data {
233                ConstructionTaskData::InlineLayout(mut layout) => {
234                    #[cfg(feature = "parallel-construct")]
235                    let mut layout_ctx = LAYOUT_CTX
236                        .take()
237                        .unwrap_or_else(|| Box::new(LayoutContext::new()));
238                    #[cfg(feature = "parallel-construct")]
239                    let layout_ctx_mut = &mut layout_ctx;
240
241                    #[cfg(feature = "parallel-construct")]
242                    let mut font_ctx = self
243                        .thread_font_contexts
244                        .get_or(|| {
245                            RefCell::new(Box::new(
246                                self.font_ctx
247                                    .lock()
248                                    .unwrap_or_else(|e| e.into_inner())
249                                    .clone(),
250                            ))
251                        })
252                        .borrow_mut();
253                    #[cfg(feature = "parallel-construct")]
254                    let font_ctx_mut = &mut *font_ctx;
255
256                    #[cfg(not(feature = "parallel-construct"))]
257                    let layout_ctx_mut = &mut self.layout_ctx;
258                    #[cfg(not(feature = "parallel-construct"))]
259                    let font_ctx_mut =
260                        &mut *self.font_ctx.lock().unwrap_or_else(|e| e.into_inner());
261
262                    layout.content_widths = None;
263                    build_inline_layout_into(
264                        &self.nodes,
265                        layout_ctx_mut,
266                        font_ctx_mut,
267                        &mut layout,
268                        self.viewport.scale(),
269                        task.node_id,
270                    );
271
272                    #[cfg(feature = "parallel-construct")]
273                    {
274                        LAYOUT_CTX.set(Some(layout_ctx));
275                    }
276
277                    // Pre-populate the content_widths cache. When there are no inline boxes,
278                    // this is trivially safe and saves a Parley call during layout. When there
279                    // ARE inline boxes, their sizes are not yet set (box sizing happens during
280                    // compute_inline_layout_inner), so the cached widths may be stale — they
281                    // will be recomputed on first access during layout. Only cache when no
282                    // inline boxes are present (pure text runs) where box sizes are irrelevant.
283                    if layout.layout.inline_boxes().is_empty() {
284                        layout.content_widths();
285                    }
286
287                    ConstructionTaskResult {
288                        node_id: task.node_id,
289                        data: ConstructionTaskResultData::InlineLayout(layout),
290                    }
291                }
292            })
293            .collect();
294
295        for result in results {
296            match result.data {
297                ConstructionTaskResultData::InlineLayout(layout) => {
298                    self.nodes[result.node_id].cache.clear();
299                    if let Some(elem) = self.nodes[result.node_id].element_data_mut() {
300                        elem.inline_layout_data = Some(layout);
301                    }
302                }
303            }
304        }
305
306        self.deferred_construction_nodes.clear();
307    }
308
309    /// Walk the nodes now that they're properly styled and transfer their styles to the taffy style system
310    ///
311    /// TODO: update taffy to use an associated type instead of slab key
312    /// TODO: update taffy to support traited styles so we don't even need to rely on taffy for storage
313    pub fn resolve_layout(&mut self) {
314        let size = self.stylist.device().au_viewport_size();
315
316        let available_space = taffy::Size {
317            width: AvailableSpace::Definite(size.width.to_f32_px()),
318            height: AvailableSpace::Definite(size.height.to_f32_px()),
319        };
320
321        // D-2c-followup: propagate the `Option<&Node>` widening; the early
322        // guard above guarantees this `unwrap_or(0)` is unreachable, but the
323        // compiler won't let us call `.id` on `Option<&Node>` anymore.
324        debug_assert!(
325            self.root_element().is_some(),
326            "resolve_layout: 'no DOM' guard above lost its effect — root_element became None when an element child was expected"
327        );
328        let root_element_id =
329            taffy::NodeId::from(self.root_element().map(|r| r.id).unwrap_or(0));
330
331        // println!("\n\nRESOLVE LAYOUT\n===========\n");
332
333        taffy::compute_root_layout(self, root_element_id, available_space);
334        taffy::round_layout(self, root_element_id);
335
336        // println!("\n\n");
337        // taffy::print_tree(self, root_node_id)
338    }
339}