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}