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