Skip to main content

blitz_dom/layout/
damage.rs

1use blitz_traits::node_id::NodeId;
2use std::ops::Range;
3
4use crate::Node;
5use crate::net::ResourceHandler;
6use crate::node::NodeFlags;
7use crate::{
8    BaseDocument, net::ImageHandler, node::ImageResourceData, node::Status, util::ImageLayerKind,
9};
10use style::properties::ComputedValues;
11use style::properties::generated::longhands::position::computed_value::T as Position;
12use style::selector_parser::RestyleDamage;
13use style::servo_arc::Arc as ServoArc;
14use style::url::ComputedUrl;
15use style::values::computed::Float;
16use style::values::computed::Overflow as StyloOverflow;
17use style::values::generics::image::Image as StyloImage;
18use style::values::specified::align::AlignFlags;
19use style::values::specified::box_::DisplayInside;
20use style::values::specified::box_::DisplayOutside;
21use taffy::Rect;
22use thin_vec::ThinVec;
23
24pub(crate) const CONSTRUCT_BOX: RestyleDamage =
25    RestyleDamage::from_bits_retain(0b_0000_0000_0001_0000);
26pub(crate) const CONSTRUCT_FC: RestyleDamage =
27    RestyleDamage::from_bits_retain(0b_0000_0000_0010_0000);
28pub(crate) const CONSTRUCT_DESCENDENT: RestyleDamage =
29    RestyleDamage::from_bits_retain(0b_0000_0000_0100_0000);
30
31pub(crate) const ONLY_RELAYOUT: RestyleDamage =
32    RestyleDamage::from_bits_retain(0b_0000_0000_0000_1000);
33
34pub(crate) const ALL_DAMAGE: RestyleDamage =
35    RestyleDamage::from_bits_retain(0b_0000_0000_0111_1111);
36
37/// `BLITZ_SUBTREE_SKIP=0` restores the unconditional walk.
38///
39/// The skip below is the sharpest correctness edge in this file: a subtree
40/// that is quietly not flushed lays out from a stale taffy style, and the
41/// symptom is a pane of wrong geometry rather than a crash. One binary that
42/// can be run both ways settles "is it the skip?" in two launches instead of
43/// a rebuild, which is what it was worth the day tab switching felt wrong —
44/// the answer then was no, and having the answer cheaply was the point.
45fn subtree_skip_enabled() -> bool {
46    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
47    *ENABLED.get_or_init(|| std::env::var("BLITZ_SUBTREE_SKIP").as_deref() != Ok("0"))
48}
49
50impl BaseDocument {
51    pub(crate) fn propagate_damage_flags(
52        &mut self,
53        node_id: NodeId,
54        damage_from_parent: RestyleDamage,
55    ) -> RestyleDamage {
56        let mut damage = if let Some(data) = self.nodes[node_id]
57            .stylo_element_data_opt_mut()
58            .and_then(|s| s.get_mut())
59        {
60            data.damage
61        } else {
62            return RestyleDamage::empty();
63        };
64        // Read before anything is folded in, which is the only moment "this
65        // node changed" is separable from "something under it changed". Paint
66        // damage needs the former: after the loop below, every ancestor up to
67        // the root carries its descendants' damage.
68        if !damage.is_empty() {
69            self.paint_damage.note_own_damage(node_id);
70        }
71        damage |= damage_from_parent;
72
73        // Flush updated pseudo-element styles to their anonymous nodes so that
74        // style changes which don't trigger box construction still take effect.
75        //
76        // TODO: see if this can be made more efficient (/run less often)
77        self.sync_pseudo_element_styles(node_id);
78
79        let damage_for_children = RestyleDamage::empty();
80        let children = std::mem::take(&mut self.nodes[node_id].children);
81        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
82        let use_layout_children = self.nodes[node_id].should_traverse_layout_children();
83        if use_layout_children {
84            let layout_children = layout_children.as_ref().unwrap();
85            for child in layout_children.iter() {
86                damage |= self.propagate_damage_flags(*child, damage_for_children);
87            }
88        } else {
89            for child in children.iter() {
90                damage |= self.propagate_damage_flags(*child, damage_for_children);
91            }
92            if let Some(before_id) = self.nodes[node_id].before() {
93                damage |= self.propagate_damage_flags(before_id, damage_for_children);
94            }
95            if let Some(after_id) = self.nodes[node_id].after() {
96                damage |= self.propagate_damage_flags(after_id, damage_for_children);
97            }
98        }
99
100        let node = &mut self.nodes[node_id];
101
102        // Put children back
103        node.children = children;
104        *node.layout_children.get_mut() = layout_children;
105
106        if damage.contains(CONSTRUCT_BOX) {
107            damage.insert(RestyleDamage::RELAYOUT);
108        }
109
110        // Compute damage to propagate to parent
111        let damage_for_parent = damage; // & RestyleDamage::RELAYOUT;
112
113        // If the node or any of it's children have been mutated or their layout styles
114        // have changed, then we should clear it's layout cache.
115        if damage.intersects(ONLY_RELAYOUT | CONSTRUCT_BOX) {
116            #[cfg(feature = "log-phase-times")]
117            crate::layout::layout_counters::note_cache_cleared();
118            node.cache_mut().clear();
119            if let Some(inline_layout) = node
120                .data
121                .downcast_element_mut()
122                .and_then(|el| el.inline_layout_data.as_mut())
123            {
124                inline_layout.content_widths = None;
125            }
126            damage.remove(ONLY_RELAYOUT);
127        }
128
129        // Store damage for current node
130        node.set_damage(damage);
131
132        // let _is_fc_root = node
133        //     .primary_styles()
134        //     .map(|s| is_fc_root(&s))
135        //     .unwrap_or(false);
136
137        // if damage.contains(CONSTRUCT_BOX) {
138        //     // damage_for_parent.insert(CONSTRUCT_FC | CONSTRUCT_DESCENDENT);
139        //     damage_for_parent.insert(CONSTRUCT_BOX);
140        // }
141
142        // if damage.contains(CONSTRUCT_FC) {
143        //     damage_for_parent.insert(CONSTRUCT_DESCENDENT);
144        //     // if !is_fc_root {
145        //     damage_for_parent.insert(CONSTRUCT_FC);
146        //     // }
147        // }
148
149        // Propagate damage to parent
150        damage_for_parent
151    }
152
153    /// Flush updated pseudo-element (`::before`/`::after`) styles from the owning
154    /// element's stylo data to the pseudo-element's anonymous node.
155    ///
156    /// Pseudo-element styles are normally flushed to the pseudo-element's node
157    /// during box construction (see `flush_pseudo_elements`), but in incremental
158    /// mode box construction only runs for nodes with construction damage.
159    /// Pseudo-element style changes which don't require reconstruction (e.g.
160    /// animations/transitions of repaint- or relayout-only properties) must still
161    /// be flushed to the pseudo-element's node - along with the damage they imply -
162    /// so that layout and paint see the new style.
163    fn sync_pseudo_element_styles(&mut self, node_id: NodeId) {
164        let node = &self.nodes[node_id];
165
166        let before_node_id = node.before();
167        let after_node_id = node.after();
168        if before_node_id.is_none() && after_node_id.is_none() {
169            return;
170        }
171
172        let (before_style, after_style) = {
173            let style_data = node.stylo_element_data_opt().and_then(|s| s.get());
174            let Some(style_data) = style_data.as_ref() else {
175                return;
176            };
177            // Note: yes these are kinda backwards (see `flush_pseudo_elements`)
178            let pseudos = style_data.styles.pseudos.as_array();
179            (pseudos[1].clone(), pseudos[0].clone())
180        };
181
182        // Creation and removal of pseudo-elements is handled during box construction
183        // (Stylo generates construction damage for those cases), so only the case
184        // where the pseudo-element both was and remains present is handled here.
185        for (pe_node_id, pe_style) in [(before_node_id, before_style), (after_node_id, after_style)]
186        {
187            let (Some(pe_node_id), Some(pe_style)) = (pe_node_id, pe_style) else {
188                continue;
189            };
190            let mut pe_data = self.nodes[pe_node_id]
191                .stylo_element_data_opt_mut()
192                .and_then(|s| s.get_mut());
193            let Some(pe_data) = pe_data.as_mut() else {
194                continue;
195            };
196            let Some(old_style) = pe_data.styles.primary.clone() else {
197                continue;
198            };
199            if std::ptr::eq(&*old_style, &*pe_style) {
200                continue;
201            }
202
203            let diff = RestyleDamage::compute_style_difference::<&Node>(&old_style, &pe_style);
204            pe_data.damage.insert(diff.damage);
205            pe_data.styles.primary = Some(pe_style);
206            pe_data.set_restyled();
207        }
208    }
209}
210
211// fn is_fc_root(style: &ComputedValues) -> bool {
212//     let display = style.clone_display();
213//     let display_inside = display.inside();
214
215//     match display_inside {
216//         DisplayInside::Flow => {
217//             // Depends on parent context
218//             false
219//         }
220
221//         DisplayInside::None => true,
222//         DisplayInside::FlowRoot => true,
223//         DisplayInside::Flex => true,
224//         DisplayInside::Grid => true,
225//         DisplayInside::Table => true,
226//         DisplayInside::TableCell => true,
227
228//         DisplayInside::Contents => false,
229//         DisplayInside::TableRowGroup => false,
230//         DisplayInside::TableColumn => false,
231//         DisplayInside::TableColumnGroup => false,
232//         DisplayInside::TableHeaderGroup => false,
233//         DisplayInside::TableFooterGroup => false,
234//         DisplayInside::TableRow => false,
235//     }
236// }
237
238pub(crate) fn compute_layout_damage(old: &ComputedValues, new: &ComputedValues) -> RestyleDamage {
239    let box_tree_needs_rebuild = || {
240        let old_box = old.get_box();
241        let new_box = new.get_box();
242
243        if old_box.display != new_box.display
244            || old_box.float != new_box.float
245            || old_box.position != new_box.position
246            || old.clone_visibility() != new.clone_visibility()
247        {
248            return true;
249        }
250
251        if old.get_font() != new.get_font() {
252            return true;
253        }
254
255        if new_box.display.outside() == DisplayOutside::Block
256            && new_box.display.inside() == DisplayInside::Flow
257        {
258            let alignment_establishes_new_block_formatting_context = |style: &ComputedValues| {
259                style.get_position().align_content.primary() != AlignFlags::NORMAL
260            };
261
262            let old_column = old.get_column();
263            let new_column = new.get_column();
264            if old_box.overflow_x.is_scrollable() != new_box.overflow_x.is_scrollable()
265                || old_column.is_multicol() != new_column.is_multicol()
266                || old_column.column_span != new_column.column_span
267                || alignment_establishes_new_block_formatting_context(old)
268                    != alignment_establishes_new_block_formatting_context(new)
269            {
270                return true;
271            }
272        }
273
274        if old_box.display.is_list_item() {
275            let old_list = old.get_list();
276            let new_list = new.get_list();
277            if old_list.list_style_position != new_list.list_style_position
278                || old_list.list_style_image != new_list.list_style_image
279                || (new_list.list_style_image == StyloImage::None
280                    && old_list.list_style_type != new_list.list_style_type)
281            {
282                return true;
283            }
284        }
285
286        if new.is_pseudo_style() && old.get_counters().content != new.get_counters().content {
287            return true;
288        }
289
290        false
291    };
292
293    let text_shaping_needs_recollect = || {
294        if old.clone_direction() != new.clone_direction()
295            || old.clone_unicode_bidi() != new.clone_unicode_bidi()
296        {
297            return true;
298        }
299
300        let old_text = old.get_inherited_text();
301        let new_text = new.get_inherited_text();
302        if !std::ptr::eq(old_text, new_text)
303            && (old_text.white_space_collapse != new_text.white_space_collapse
304                || old_text.text_transform != new_text.text_transform
305                || old_text.word_break != new_text.word_break
306                || old_text.overflow_wrap != new_text.overflow_wrap
307                || old_text.letter_spacing != new_text.letter_spacing
308                || old_text.word_spacing != new_text.word_spacing
309                || old_text.text_rendering != new_text.text_rendering)
310        {
311            return true;
312        }
313
314        false
315    };
316
317    #[allow(
318        clippy::if_same_then_else,
319        reason = "these branches will soon be different"
320    )]
321    if box_tree_needs_rebuild() {
322        ALL_DAMAGE
323    } else if text_shaping_needs_recollect() {
324        ALL_DAMAGE
325    } else {
326        // This element needs to be laid out again, but does not have any damage to
327        // its box. In the future, we will distinguish between types of damage to the
328        // fragment as well.
329        RestyleDamage::RELAYOUT
330    }
331}
332
333/// A child with a z_index that is hoisted up to it's containing Stacking Context for paint purposes
334#[derive(Debug, Clone)]
335pub struct HoistedPaintChild {
336    pub node_id: NodeId,
337    pub z_index: i32,
338    pub position: taffy::Point<f32>,
339    /// The ancestors this child was hoisted past whose overflow clips it.
340    ///
341    /// Hoisting moves a node out of the subtree whose clip layers would have
342    /// contained it, so without these it paints over anything its ancestors
343    /// were meant to cut it off at. Empty for the overwhelming majority of
344    /// hoisted children, which cross nothing that clips.
345    ///
346    /// Ids, not rectangles: this is collected before taffy runs, when every
347    /// box is still zero-sized. `resolve_hoisted_clips` turns them into
348    /// [`Self::clips`] once there is a layout to read.
349    pub clip_ancestors: Vec<NodeId>,
350    /// Where [`Self::clip_ancestors`] ended up, relative to the origin of the
351    /// stacking context this child paints in.
352    pub clips: Vec<taffy::Rect<f32>>,
353    /// Whether an ancestor's overflow clips this child from here upward.
354    ///
355    /// An ancestor does not clip a positioned box whose containing block is
356    /// outside that ancestor (CSS 2.1 11.1.1), so for an absolutely positioned
357    /// child this starts false and turns on at its containing block: an
358    /// `overflow: hidden` wrapper *between* the child and the box it is
359    /// positioned against has no say over it. In-flow children are clipped by
360    /// everything above them, and `position: fixed` by nothing, its containing
361    /// block being the viewport.
362    pub clips_apply: bool,
363    /// Absolutely positioned, so `clips_apply` turns on at its containing block.
364    pub starts_at_containing_block: bool,
365}
366
367impl HoistedPaintChild {
368    fn new(node_id: NodeId, z_index: i32, position: Position) -> Self {
369        Self {
370            node_id,
371            z_index,
372            position: taffy::Point::ZERO,
373            clip_ancestors: Vec::new(),
374            clips: Vec::new(),
375            clips_apply: !matches!(position, Position::Absolute | Position::Fixed),
376            starts_at_containing_block: position == Position::Absolute,
377        }
378    }
379}
380
381#[derive(Debug)]
382pub struct HoistedPaintChildren {
383    pub children: Vec<HoistedPaintChild>,
384    /// The number of hoisted point children with negative z_index
385    pub negative_z_count: u32,
386
387    pub content_area: taffy::Rect<f32>,
388}
389
390impl HoistedPaintChildren {
391    fn new() -> Self {
392        Self {
393            children: Vec::new(),
394            negative_z_count: 0,
395            content_area: taffy::Rect::ZERO,
396        }
397    }
398
399    pub fn reset(&mut self) {
400        self.children.clear();
401        self.negative_z_count = 0;
402    }
403
404    pub fn compute_content_size(&mut self, doc: &BaseDocument) {
405        fn child_pos(child: &HoistedPaintChild, doc: &BaseDocument) -> Rect<f32> {
406            let node = &doc.nodes[child.node_id];
407            let left = child.position.x + node.final_layout().location.x;
408            let top = child.position.y + node.final_layout().location.y;
409            let right = left + node.final_layout().size.width;
410            let bottom = top + node.final_layout().size.height;
411
412            taffy::Rect {
413                top,
414                left,
415                bottom,
416                right,
417            }
418        }
419
420        if self.children.is_empty() {
421            self.content_area = taffy::Rect::ZERO;
422        } else {
423            self.content_area = child_pos(&self.children[0], doc);
424            for child in self.children[1..].iter() {
425                let pos = child_pos(child, doc);
426                self.content_area.left = self.content_area.left.min(pos.left);
427                self.content_area.top = self.content_area.top.min(pos.top);
428                self.content_area.right = self.content_area.right.max(pos.right);
429                self.content_area.bottom = self.content_area.bottom.max(pos.bottom);
430            }
431        }
432    }
433
434    pub fn sort(&mut self) {
435        self.children.sort_by_key(|c| c.z_index);
436        self.negative_z_count = self.children.iter().take_while(|c| c.z_index < 0).count() as u32;
437    }
438
439    pub fn neg_z_range(&self) -> Range<usize> {
440        0..(self.negative_z_count as usize)
441    }
442
443    pub fn pos_z_range(&self) -> Range<usize> {
444        (self.negative_z_count as usize)..self.children.len()
445    }
446
447    pub fn neg_z_hoisted_children(
448        &self,
449    ) -> impl ExactSizeIterator<Item = &HoistedPaintChild> + DoubleEndedIterator {
450        self.children[self.neg_z_range()].iter()
451    }
452
453    pub fn pos_z_hoisted_children(
454        &self,
455    ) -> impl ExactSizeIterator<Item = &HoistedPaintChild> + DoubleEndedIterator {
456        self.children[self.pos_z_range()].iter()
457    }
458}
459
460impl BaseDocument {
461    pub(crate) fn invalidate_inline_contexts(&mut self) {
462        let scale = self.viewport.scale();
463
464        let font_ctx = &self.font_ctx;
465        let layout_ctx = &mut self.layout_ctx;
466
467        let mut anon_nodes = Vec::new();
468
469        for (_, node) in self.nodes.iter_mut() {
470            if !(node.flags.contains(NodeFlags::IS_IN_DOCUMENT)) {
471                continue;
472            }
473
474            let Some(element) = node.data.downcast_element_mut() else {
475                continue;
476            };
477
478            if element.inline_layout_data.is_some() {
479                if node.is_anonymous() {
480                    anon_nodes.push(node.id);
481                } else {
482                    node.insert_damage(ALL_DAMAGE);
483                }
484            } else if let Some(input) = element.text_input_data_mut() {
485                input.editor.set_scale(scale);
486                let mut font_ctx = font_ctx.lock().unwrap();
487                input.editor.refresh_layout(&mut font_ctx, layout_ctx);
488                // The placeholder is a second editor and needs the same scale.
489                // Left behind, it keeps whatever scale it was cloned at and its
490                // glyphs are painted at that size while everything around them
491                // is painted at the new one: on a retina display the
492                // placeholder comes out half size.
493                if let Some(placeholder) = input.placeholder_editor.as_mut() {
494                    placeholder.set_scale(scale);
495                    placeholder.refresh_layout(&mut font_ctx, layout_ctx);
496                }
497                node.insert_damage(ONLY_RELAYOUT);
498            }
499        }
500
501        for node_id in anon_nodes {
502            if let Some(parent_id) = *(self.nodes[node_id].layout_parent.get_mut()) {
503                self.nodes[parent_id].insert_damage(ALL_DAMAGE);
504            }
505        }
506    }
507
508    pub fn flush_styles_to_layout(&mut self, node_id: NodeId) {
509        // Rebuilt by the walk below, and stale otherwise: an incremental flush
510        // can rebuild a context whose hoisted children no longer cross
511        // anything that clips.
512        self.hoisted_clip_hosts.clear();
513        self.flush_styles_to_layout_impl(node_id, None);
514    }
515
516    /// Flush a CSS image layer list (`background-image` or `mask-image`) from style
517    /// to dedicated storage on the node, fetching any images which are not yet loaded.
518    fn flush_image_layers_from_style(&mut self, node_id: NodeId, kind: ImageLayerKind) {
519        let doc_id = self.id();
520        let node = self.nodes.get_mut(node_id).unwrap();
521        // Clone the primary style `Arc` into an owned value so the immutable
522        // borrow of `node` (held by the stylo element data guard) is released
523        // before we take a mutable borrow of `node.data` below.
524        let style = {
525            let stylo_element_data = node.stylo_element_data_opt().and_then(|s| s.get());
526            let primary_styles = stylo_element_data
527                .as_ref()
528                .and_then(|data| data.styles.get_primary());
529            let Some(style) = primary_styles else {
530                return;
531            };
532            style.clone()
533        };
534        let Some(elem) = node.data.downcast_element_mut() else {
535            return;
536        };
537
538        let (style_images, elem_images) = match kind {
539            ImageLayerKind::Background => (
540                &style.get_background().background_image.0,
541                &mut elem.background_images,
542            ),
543            ImageLayerKind::Mask => (&style.get_svg().mask_image.0, &mut elem.mask_images),
544        };
545
546        let len = style_images.len();
547        elem_images.resize_with(len, || None);
548
549        for idx in 0..len {
550            let style_image = &style_images[idx];
551            let new_image = match style_image {
552                StyloImage::Url(ComputedUrl::Valid(new_url)) => {
553                    let old_image = elem_images[idx].as_ref();
554                    let old_image_url = old_image.map(|data| &data.url);
555                    if old_image_url.is_some_and(|old_url| **new_url == **old_url) {
556                        break;
557                    }
558
559                    // Check cache first
560                    let url_str = new_url.as_str();
561                    if let Some(cached_image) = self.image_cache.get(url_str) {
562                        #[cfg(feature = "tracing")]
563                        tracing::info!("Loading image {url_str} from cache");
564                        Some(ImageResourceData {
565                            url: new_url.clone(),
566                            status: Status::Ok,
567                            image: cached_image.clone(),
568                        })
569                    } else if let Some(waiting_list) = self.pending_images.get_mut(url_str) {
570                        // Image is already being fetched, queue this node
571                        #[cfg(feature = "tracing")]
572                        tracing::info!("Image {url_str} already pending, queueing node {node_id}");
573                        waiting_list.push((node_id, kind.image_type(idx)));
574                        Some(ImageResourceData::new(new_url.clone()))
575                    } else {
576                        // Start fetch and track as pending
577                        #[cfg(feature = "tracing")]
578                        tracing::info!("Fetching image {url_str}");
579                        self.pending_images
580                            .insert(url_str.to_string(), vec![(node_id, kind.image_type(idx))]);
581
582                        self.net_provider.fetch(
583                            doc_id,
584                            crate::net::stamped_request(
585                                (**new_url).clone(),
586                                self.abort_signal.as_ref(),
587                            ),
588                            ResourceHandler::boxed(
589                                self.tx.clone(),
590                                doc_id,
591                                None, // Don't pass node_id, we'll handle via pending_images
592                                self.shell_provider.clone(),
593                                ImageHandler::new(kind.image_type(idx)),
594                            ),
595                        );
596
597                        Some(ImageResourceData::new(new_url.clone()))
598                    }
599                }
600                _ => None,
601            };
602
603            // Element will always exist due to resize_with above
604            elem_images[idx] = new_image;
605        }
606    }
607
608    /// Walk the whole tree, converting styles to layout
609    fn flush_styles_to_layout_impl(
610        &mut self,
611        node_id: NodeId,
612        parent_stacking_context: Option<&mut HoistedPaintChildren>,
613    ) {
614        let mut new_stacking_context: HoistedPaintChildren = HoistedPaintChildren::new();
615        let stacking_context = &mut new_stacking_context;
616
617        // Flush background/mask images from style to dedicated storage on the node
618        self.flush_image_layers_from_style(node_id, ImageLayerKind::Background);
619        self.flush_image_layers_from_style(node_id, ImageLayerKind::Mask);
620
621        let incremental = self.incremental_layout;
622
623        // Skip an untouched subtree outright, rather than walking it to find
624        // out it is untouched.
625        //
626        // `propagate_damage_flags` stores the union of a node's own damage and
627        // its whole subtree's, so an empty value means nothing under here
628        // changed. That was already enough to skip rebuilding the taffy style,
629        // and not enough to skip the recursion, because an ancestor rebuilds
630        // its stacking context from scratch and a subtree that feeds it would
631        // vanish from paint. `subtree_hoists` is that missing bit, set below
632        // while walking.
633        //
634        // It is the largest phase in an idle frame: at 7,008 nodes a frame
635        // that recomputes nothing still spent 3.5ms here, walking the tree to
636        // discover it had nothing to do.
637        // A node that has never had a taffy style built is not "untouched", it
638        // is unbuilt, and skipping it leaves layout running against defaults:
639        // no `min-width: 0`, no flex, so a revealed pane laid out at its
640        // max-content width of 150,948px. Nothing distinguished the two cases
641        // while stylo discarded the styles of a hidden subtree, because a
642        // subtree that had never been flushed had never been styled either.
643        let never_flushed = self
644            .nodes
645            .get(node_id)
646            .is_some_and(|node| node.style_source_opt().is_none());
647
648        // A paint-only restyle can replace the computed-values arc without
649        // adding layout damage. The cached taffy style may contain raw calc()
650        // pointers into that arc, so arc identity is also part of the subtree
651        // skip condition.
652        let style_changed = {
653            let node = &self.nodes[node_id];
654            let stylo_element_data = node.stylo_element_data_opt().and_then(|s| s.get());
655            let primary = stylo_element_data
656                .as_ref()
657                .and_then(|data| data.styles.get_primary());
658            match (primary, node.style_source_opt()) {
659                (Some(current), Some(cached)) => !ServoArc::ptr_eq(current, cached),
660                (None, None) => false,
661                _ => true,
662            }
663        };
664
665        if incremental
666            && subtree_skip_enabled()
667            && !never_flushed
668            && !style_changed
669            && self
670                .nodes
671                .get(node_id)
672                .and_then(|node| node.damage())
673                .is_some_and(|damage| damage.is_empty())
674            && !self.nodes[node_id].subtree_hoists()
675        {
676            return;
677        }
678
679        let display = {
680            let node = self.nodes.get_mut(node_id).unwrap();
681            let damage = node.damage().unwrap_or(ALL_DAMAGE);
682
683            // Only rebuild the taffy style when something asked for it.
684            //
685            // `propagate_damage_flags` stores the union of a node's own damage
686            // and its whole subtree's, so an empty value here means nothing
687            // under this node changed and last pass's taffy style is still
688            // correct. Recomputing it anyway is what made a steady-state frame
689            // — one where the page is laid out and only an animation is
690            // running — cost a full `to_taffy_style` for every node in the
691            // document, thirty times a second.
692            //
693            // Only in incremental mode. Without it `propagate_damage_flags`
694            // never runs, so the damage read above is whatever was last left
695            // on the node, and gating on it would skip real work.
696            //
697            // The recursion below is deliberately *not* gated. A node that
698            // contributes hoisted children to an ancestor's stacking context
699            // has to walk even when unchanged, because the ancestor rebuilds
700            // that list from scratch and would otherwise lose them.
701            // Damage alone is not a safe gate, because the taffy style borrows
702            // from the computed values rather than owning them: a `calc()`
703            // reaches taffy as a raw pointer into the stylo
704            // `CalcLengthPercentage` (see `stylo_taffy::convert`). A restyle
705            // that lands no relayout damage — a colour change, or one that
706            // computes to the same values — still replaces the primary
707            // `ComputedValues`, and if that drops the last reference the cached
708            // pointer is dangling. Layout then resolves freed memory, which is
709            // a segfault when the page is unmapped and a nonsense calc node
710            // when it is not: 0.6.x experimental died both ways, seconds after
711            // boot, whenever a slow command's response restyled the header.
712            //
713            // So rebuild whenever the arc is not the one the cached style was
714            // built from. Identity, not equality: a fresh arc means fresh
715            // allocations behind every pointer in the old style. The steady
716            // state this gate exists for is unaffected, because a frame that
717            // restyles nothing hands back the same arc.
718            let needs_style_flush = !incremental
719                || style_changed
720                || damage.intersects(RestyleDamage::RELAYOUT | CONSTRUCT_BOX);
721
722            if needs_style_flush {
723                // Compute the owned taffy style and display in an inner scope so the
724                // immutable borrow of `node` (held by the stylo element data guard)
725                // is released before we mutably access `node` below.
726                let (mut taffy_style, display_constructed_as, style_source) = {
727                    let stylo_element_data = node.stylo_element_data_opt().and_then(|s| s.get());
728                    let primary_styles = stylo_element_data
729                        .as_ref()
730                        .and_then(|data| data.styles.get_primary());
731
732                    let Some(style) = primary_styles else {
733                        return;
734                    };
735
736                    (
737                        stylo_taffy::to_taffy_style(style),
738                        style.clone_display(),
739                        style.clone(),
740                    )
741                };
742                taffy_style.item_is_replaced = node
743                    .data
744                    .downcast_element()
745                    .is_some_and(|el| crate::layout::replaced::is_replaced_element(&el.name.local));
746
747                // A rebuilt style and a retained layout cache have to agree.
748                // The cache is cleared by `propagate_damage_flags` only for
749                // relayout damage, so a style refreshed for any other reason
750                // leaves this node answering from a cache computed against the
751                // values it just replaced, while its parent lays out against
752                // the new ones. Comparing is cheap next to laying out, and
753                // equal styles are the common case: a recolour rebuilds an
754                // identical taffy style and keeps its cache.
755                let layout_inputs_changed = *node.style() != taffy_style;
756                *node.style_mut() = taffy_style;
757                *node.display_constructed_as_mut() = display_constructed_as;
758                if layout_inputs_changed {
759                    node.cache_mut().clear();
760                    if let Some(inline_layout) = node
761                        .data
762                        .downcast_element_mut()
763                        .and_then(|el| el.inline_layout_data.as_mut())
764                    {
765                        inline_layout.content_widths = None;
766                    }
767                }
768                // Stored last, and only on the path that rebuilt the style, so
769                // the arc held here is always the one the pointers in
770                // `node.style()` point into. It keeps those allocations alive
771                // for as long as the style that borrows them.
772                *node.style_source_mut() = Some(style_source);
773            } else if node
774                .stylo_element_data_opt()
775                .and_then(|s| s.get())
776                .as_ref()
777                .and_then(|data| data.styles.get_primary())
778                .is_none()
779            {
780                // Preserved from the ungated form: a node with no primary style
781                // is not laid out and its subtree is not walked.
782                return;
783            }
784
785            // In non-incremental mode we unconditionally clear the Taffy cache.
786            // In incremental mode this is handled as part of damage propagation.
787            if !incremental {
788                node.cache_mut().clear();
789                if let Some(inline_layout) = node
790                    .data
791                    .downcast_element_mut()
792                    .and_then(|el| el.inline_layout_data.as_mut())
793                {
794                    inline_layout.content_widths = None;
795                }
796            }
797
798            node.style().display
799        };
800
801        // A hidden subtree is not walked.
802        //
803        // Its taffy style is flushed above, which is what lets paint stop at
804        // this node — but only for children it reaches *through* this node. A
805        // positioned child with a z-index is hoisted into an ancestor's
806        // stacking context and painted from there, so it never passes this
807        // node's display check at all. Walking a hidden subtree therefore
808        // publishes its raised children into the visible tab's paint list: the
809        // application's panel-edge chevron is `absolute left-full z-20`, and
810        // one ghost chevron appeared per retained tab.
811        //
812        // This walk could not reach a hidden subtree before, because hiding a
813        // pane emptied its layout children and stylo discarded its styles.
814        if matches!(display, taffy::Display::None) {
815            return;
816        }
817
818        // Hoisted fixed nodes, held back until the borrow on paint_children is
819        // released so their real stacking context can be reached.
820        let mut deferred_fixed: Vec<(NodeId, i32, NodeId)> = Vec::new();
821
822        // If the node has children, then take those children and...
823        let children = self.nodes[node_id].layout_children.borrow_mut().take();
824        if let Some(mut children) = children {
825            let is_flex_or_grid = matches!(display, taffy::Display::Flex | taffy::Display::Grid);
826
827            // Recursively call flush_styles_to_layout on each child
828            for &child in children.iter() {
829                self.flush_styles_to_layout_impl(
830                    child,
831                    match self.nodes[child].is_stacking_context_root(is_flex_or_grid) {
832                        true => None,
833                        false => Some(stacking_context),
834                    },
835                );
836            }
837
838            // Sort layout_children
839            if is_flex_or_grid {
840                children.sort_by(|left, right| {
841                    let left_node = self.nodes.get(*left).unwrap();
842                    let right_node = self.nodes.get(*right).unwrap();
843                    left_node.order().cmp(&right_node.order())
844                });
845            }
846
847            // Reserve space for paint_children
848            let mut paint_children = self.nodes[node_id].paint_children.borrow_mut();
849            if paint_children.is_none() {
850                *paint_children = Some(ThinVec::new());
851            }
852            let paint_children = paint_children.as_mut().unwrap();
853            paint_children.clear();
854            paint_children.reserve(children.len());
855
856            // Push children to either paint_children or layout_children depending on
857            for &child_id in children.iter() {
858                let child = &self.nodes[child_id];
859
860                let Some(style) = child.primary_styles() else {
861                    paint_children.push(child_id);
862                    continue;
863                };
864
865                let position = style.clone_position();
866                let z_index = style.clone_z_index().integer_or(0);
867
868                // TODO: more complete hoisting detection
869                // z-index applies to static flex/grid items too
870                // (css-flexbox-1 §painting, css-grid-1 §z-order).
871                if z_index != 0 && (position != Position::Static || is_flex_or_grid) {
872                    // A hoisted fixed node paints in the stacking context its
873                    // box tree gives it, not the one the hoist moved it to.
874                    // `hoist_fixed_position_nodes` reparents it onto the root
875                    // element so its insets resolve against the viewport, which
876                    // is what CSS asks for; the stacking context is a separate
877                    // question and follows the original ancestors.
878                    if let Some(&origin) = self.hoisted_fixed_parents.get(&child_id) {
879                        deferred_fixed.push((child_id, z_index, origin));
880                    } else {
881                        stacking_context
882                            .children
883                            .push(HoistedPaintChild::new(child_id, z_index, position))
884                    }
885                } else {
886                    paint_children.push(child_id);
887                }
888            }
889
890            // Sort paint_children
891            paint_children.sort_by(|left, right| {
892                let left_node = self.nodes.get(*left).unwrap();
893                let right_node = self.nodes.get(*right).unwrap();
894                node_to_paint_order(left_node, is_flex_or_grid)
895                    .cmp(&node_to_paint_order(right_node, is_flex_or_grid))
896            });
897
898            // Put children back
899            *self.nodes[node_id].layout_children.borrow_mut() = Some(children);
900        }
901
902        // Outside the block above, so the borrow on paint_children has ended:
903        // reaching another node's stacking context needs `self` mutably.
904        let hoisted_fixed_here = !deferred_fixed.is_empty();
905        for (child_id, z_index, origin) in deferred_fixed {
906            self.place_hoisted_fixed(child_id, z_index, origin, node_id, stacking_context);
907        }
908
909        // Anything this subtree contributes upward makes it unskippable next
910        // frame. A hoisted fixed node counts even when this node establishes a
911        // stacking context, because `place_hoisted_fixed` reaches a context
912        // that is not this one.
913        let feeds_an_ancestor = hoisted_fixed_here
914            || (parent_stacking_context.is_some() && !stacking_context.children.is_empty());
915        *self.nodes[node_id].subtree_hoists_mut() = feeds_an_ancestor;
916
917        if let Some(parent_stacking_context) = parent_stacking_context {
918            let position = self.nodes[node_id].final_layout().location;
919            let scroll_offset = *self.nodes[node_id].scroll_offset();
920
921            // Everything below is leaving this node's subtree, so this node's
922            // own clip layer will not contain any of it. Note the clip here and
923            // let it ride up with the children it applies to.
924            let (clips_here, is_containing_block) = self.nodes[node_id]
925                .primary_styles()
926                .map(|styles| {
927                    let box_styles = styles.get_box();
928                    (
929                        !matches!(box_styles.overflow_x, StyloOverflow::Visible)
930                            || !matches!(box_styles.overflow_y, StyloOverflow::Visible),
931                        // A positioned box is a containing block for absolutely
932                        // positioned descendants. A transform or a filter makes
933                        // one too, but both make a stacking context as well,
934                        // which stops the hoist before it reaches here.
935                        box_styles.position != Position::Static,
936                    )
937                })
938                .unwrap_or((false, false));
939
940            for hoisted in stacking_context.children.iter_mut() {
941                // Before the push, because a containing block clips its own
942                // absolutely positioned children.
943                if hoisted.starts_at_containing_block && is_containing_block {
944                    hoisted.clips_apply = true;
945                }
946                if clips_here && hoisted.clips_apply {
947                    hoisted.clip_ancestors.push(node_id);
948                }
949
950                hoisted.position.x += position.x - scroll_offset.x as f32;
951                hoisted.position.y += position.y - scroll_offset.y as f32;
952            }
953            parent_stacking_context
954                .children
955                .extend(stacking_context.children.iter().cloned());
956        } else {
957            stacking_context.sort();
958            stacking_context.compute_content_size(self);
959            if stacking_context
960                .children
961                .iter()
962                .any(|child| !child.clip_ancestors.is_empty())
963            {
964                self.hoisted_clip_hosts.push(node_id);
965            }
966            self.nodes[node_id].stacking_context = Some(Box::new(new_stacking_context));
967        }
968    }
969}
970
971impl BaseDocument {
972    /// Put a hoisted `position: fixed` node into the stacking context its box
973    /// tree gives it, rather than the root's.
974    ///
975    /// `origin` is the layout parent the node was taken from. Walking up from
976    /// there finds the nearest ancestor that establishes a stacking context,
977    /// which is where CSS says the node paints. When that ancestor is the node
978    /// we are already building a context for, the caller's context is it and
979    /// nothing special is needed.
980    ///
981    /// Descendants are flushed before their parent's hoisting pass, so an
982    /// ancestor's context is already built and sorted by the time this runs.
983    /// Pushing into it means sorting it again.
984    ///
985    /// The offset that compensates for the move is filled in later, by
986    /// `correct_hoisted_fixed_positions`, because layout does not exist yet
987    /// when this runs.
988    fn place_hoisted_fixed(
989        &mut self,
990        child_id: NodeId,
991        z_index: i32,
992        origin: NodeId,
993        current: NodeId,
994        current_context: &mut HoistedPaintChildren,
995    ) {
996        let host = self.nearest_stacking_context_ancestor(origin);
997
998        if host == Some(current) || host.is_none() {
999            current_context.children.push(HoistedPaintChild::new(
1000                child_id,
1001                z_index,
1002                Position::Fixed,
1003            ));
1004            return;
1005        }
1006        let host = host.unwrap();
1007
1008        // The offset cannot be computed here: this runs before taffy has laid
1009        // anything out, so every absolute position is still zero. It is filled
1010        // in by `correct_hoisted_fixed_positions` once layout exists.
1011        let position = taffy::Point::ZERO;
1012
1013        let Some(context) = self.nodes[host].stacking_context.as_mut() else {
1014            // No context to join. Falling back to the caller's keeps the node
1015            // painted rather than dropping it.
1016            current_context.children.push(HoistedPaintChild::new(
1017                child_id,
1018                z_index,
1019                Position::Fixed,
1020            ));
1021            return;
1022        };
1023        let mut hoisted = HoistedPaintChild::new(child_id, z_index, Position::Fixed);
1024        hoisted.position = position;
1025        context.children.push(hoisted);
1026        let mut context = self.nodes[host].stacking_context.take().unwrap();
1027        context.sort();
1028        context.compute_content_size(self);
1029        self.nodes[host].stacking_context = Some(context);
1030    }
1031
1032    /// The nearest ancestor of `node_id`, inclusive, that establishes a
1033    /// stacking context.
1034    pub(crate) fn nearest_stacking_context_ancestor(&self, node_id: NodeId) -> Option<NodeId> {
1035        let mut current = Some(node_id);
1036        while let Some(id) = current {
1037            let node = self.nodes.get(id)?;
1038            let is_flex_or_grid_item = node
1039                .layout_parent
1040                .get()
1041                .and_then(|parent| self.nodes.get(parent))
1042                .is_some_and(|parent| {
1043                    matches!(
1044                        parent.style().display,
1045                        taffy::Display::Flex | taffy::Display::Grid
1046                    )
1047                });
1048            if node.is_stacking_context_root(is_flex_or_grid_item) {
1049                return Some(id);
1050            }
1051            current = node.layout_parent.get();
1052        }
1053        None
1054    }
1055}
1056
1057#[inline(always)]
1058fn position_to_order(pos: Position) -> i32 {
1059    match pos {
1060        Position::Static => 0,
1061        // All positioned descendants with z-index: auto share one paint
1062        // level (CSS 2.1 Appendix E step 8); the stable sort keeps them in
1063        // tree order among themselves, above in-flow content and floats.
1064        Position::Relative | Position::Sticky | Position::Absolute | Position::Fixed => 2,
1065    }
1066}
1067#[inline(always)]
1068fn float_to_order(pos: Float) -> i32 {
1069    match pos {
1070        Float::None => 0,
1071        _ => 1,
1072    }
1073}
1074
1075/// Paint sort key: (paint level, order-modified position). Positioned
1076/// (z-index: auto) descendants paint above in-flow content (CSS 2.1
1077/// Appendix E step 8); within a level the stable sort preserves
1078/// (order-modified) document order.
1079#[inline(always)]
1080fn node_to_paint_order(node: &Node, is_flex_or_grid: bool) -> (i32, i32) {
1081    let Some(style) = node.primary_styles() else {
1082        return (0, 0);
1083    };
1084    let position = style.clone_position();
1085    if is_flex_or_grid {
1086        match position {
1087            Position::Static => (0, style.clone_order()),
1088            Position::Relative | Position::Sticky => (2, style.clone_order()),
1089            // Out-of-flow children are not flex/grid items: `order` does
1090            // not apply; tree order does.
1091            Position::Absolute | Position::Fixed => (2, 0),
1092        }
1093    } else {
1094        (
1095            position_to_order(position) + float_to_order(style.clone_float()),
1096            0,
1097        )
1098    }
1099}