Skip to main content

blitz_dom/layout/
damage.rs

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