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