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