Skip to main content

blitz_dom/layout/
damage.rs

1use std::ops::Range;
2
3use crate::net::ResourceHandler;
4use crate::node::NodeFlags;
5use crate::{
6    BaseDocument, net::ImageHandler, node::ImageResourceData, node::Status, util::ImageLayerKind,
7};
8use crate::{NON_INCREMENTAL, Node};
9use style::properties::ComputedValues;
10use style::properties::generated::longhands::position::computed_value::T as Position;
11use style::selector_parser::RestyleDamage;
12use style::servo::url::ComputedUrl;
13use style::values::computed::Float;
14use style::values::generics::image::Image as StyloImage;
15use style::values::specified::align::AlignFlags;
16use style::values::specified::box_::DisplayInside;
17use style::values::specified::box_::DisplayOutside;
18use taffy::Rect;
19
20pub(crate) const CONSTRUCT_BOX: RestyleDamage =
21    RestyleDamage::from_bits_retain(0b_0000_0000_0001_0000);
22pub(crate) const CONSTRUCT_FC: RestyleDamage =
23    RestyleDamage::from_bits_retain(0b_0000_0000_0010_0000);
24pub(crate) const CONSTRUCT_DESCENDENT: RestyleDamage =
25    RestyleDamage::from_bits_retain(0b_0000_0000_0100_0000);
26
27pub(crate) const ONLY_RELAYOUT: RestyleDamage =
28    RestyleDamage::from_bits_retain(0b_0000_0000_0000_1000);
29
30pub(crate) const ALL_DAMAGE: RestyleDamage =
31    RestyleDamage::from_bits_retain(0b_0000_0000_0111_1111);
32
33impl BaseDocument {
34    #[cfg(feature = "incremental")]
35    pub(crate) fn propagate_damage_flags(
36        &mut self,
37        node_id: usize,
38        damage_from_parent: RestyleDamage,
39    ) -> RestyleDamage {
40        let mut damage = if let Some(data) = self.nodes[node_id].stylo_element_data.get_mut() {
41            data.damage
42        } else {
43            return RestyleDamage::empty();
44        };
45        damage |= damage_from_parent;
46
47        let damage_for_children = RestyleDamage::empty();
48        let children = std::mem::take(&mut self.nodes[node_id].children);
49        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
50        let use_layout_children = self.nodes[node_id].should_traverse_layout_children();
51        if use_layout_children {
52            let layout_children = layout_children.as_ref().unwrap();
53            for child in layout_children.iter() {
54                damage |= self.propagate_damage_flags(*child, damage_for_children);
55            }
56        } else {
57            for child in children.iter() {
58                damage |= self.propagate_damage_flags(*child, damage_for_children);
59            }
60            if let Some(before_id) = self.nodes[node_id].before {
61                damage |= self.propagate_damage_flags(before_id, damage_for_children);
62            }
63            if let Some(after_id) = self.nodes[node_id].after {
64                damage |= self.propagate_damage_flags(after_id, damage_for_children);
65            }
66        }
67
68        let node = &mut self.nodes[node_id];
69
70        // Put children back
71        node.children = children;
72        *node.layout_children.get_mut() = layout_children;
73
74        if damage.contains(CONSTRUCT_BOX) {
75            damage.insert(RestyleDamage::RELAYOUT);
76        }
77
78        // Compute damage to propagate to parent
79        let damage_for_parent = damage; // & RestyleDamage::RELAYOUT;
80
81        // If the node or any of it's children have been mutated or their layout styles
82        // have changed, then we should clear it's layout cache.
83        if damage.intersects(ONLY_RELAYOUT | CONSTRUCT_BOX) {
84            node.cache.clear();
85            if let Some(inline_layout) = node
86                .data
87                .downcast_element_mut()
88                .and_then(|el| el.inline_layout_data.as_mut())
89            {
90                inline_layout.content_widths = None;
91            }
92            damage.remove(ONLY_RELAYOUT);
93        }
94
95        // Store damage for current node
96        node.set_damage(damage);
97
98        // let _is_fc_root = node
99        //     .primary_styles()
100        //     .map(|s| is_fc_root(&s))
101        //     .unwrap_or(false);
102
103        // if damage.contains(CONSTRUCT_BOX) {
104        //     // damage_for_parent.insert(CONSTRUCT_FC | CONSTRUCT_DESCENDENT);
105        //     damage_for_parent.insert(CONSTRUCT_BOX);
106        // }
107
108        // if damage.contains(CONSTRUCT_FC) {
109        //     damage_for_parent.insert(CONSTRUCT_DESCENDENT);
110        //     // if !is_fc_root {
111        //     damage_for_parent.insert(CONSTRUCT_FC);
112        //     // }
113        // }
114
115        // Propagate damage to parent
116        damage_for_parent
117    }
118}
119
120// #[cfg(feature = "incremental")]
121// fn is_fc_root(style: &ComputedValues) -> bool {
122//     let display = style.clone_display();
123//     let display_inside = display.inside();
124
125//     match display_inside {
126//         DisplayInside::Flow => {
127//             // Depends on parent context
128//             false
129//         }
130
131//         DisplayInside::None => true,
132//         DisplayInside::FlowRoot => true,
133//         DisplayInside::Flex => true,
134//         DisplayInside::Grid => true,
135//         DisplayInside::Table => true,
136//         DisplayInside::TableCell => true,
137
138//         DisplayInside::Contents => false,
139//         DisplayInside::TableRowGroup => false,
140//         DisplayInside::TableColumn => false,
141//         DisplayInside::TableColumnGroup => false,
142//         DisplayInside::TableHeaderGroup => false,
143//         DisplayInside::TableFooterGroup => false,
144//         DisplayInside::TableRow => false,
145//     }
146// }
147
148pub(crate) fn compute_layout_damage(old: &ComputedValues, new: &ComputedValues) -> RestyleDamage {
149    let box_tree_needs_rebuild = || {
150        let old_box = old.get_box();
151        let new_box = new.get_box();
152
153        if old_box.display != new_box.display
154            || old_box.float != new_box.float
155            || old_box.position != new_box.position
156            || old.clone_visibility() != new.clone_visibility()
157        {
158            return true;
159        }
160
161        if old.get_font() != new.get_font() {
162            return true;
163        }
164
165        if new_box.display.outside() == DisplayOutside::Block
166            && new_box.display.inside() == DisplayInside::Flow
167        {
168            let alignment_establishes_new_block_formatting_context = |style: &ComputedValues| {
169                style.get_position().align_content.primary() != AlignFlags::NORMAL
170            };
171
172            let old_column = old.get_column();
173            let new_column = new.get_column();
174            if old_box.overflow_x.is_scrollable() != new_box.overflow_x.is_scrollable()
175                || old_column.is_multicol() != new_column.is_multicol()
176                || old_column.column_span != new_column.column_span
177                || alignment_establishes_new_block_formatting_context(old)
178                    != alignment_establishes_new_block_formatting_context(new)
179            {
180                return true;
181            }
182        }
183
184        if old_box.display.is_list_item() {
185            let old_list = old.get_list();
186            let new_list = new.get_list();
187            if old_list.list_style_position != new_list.list_style_position
188                || old_list.list_style_image != new_list.list_style_image
189                || (new_list.list_style_image == StyloImage::None
190                    && old_list.list_style_type != new_list.list_style_type)
191            {
192                return true;
193            }
194        }
195
196        if new.is_pseudo_style() && old.get_counters().content != new.get_counters().content {
197            return true;
198        }
199
200        false
201    };
202
203    let text_shaping_needs_recollect = || {
204        if old.clone_direction() != new.clone_direction()
205            || old.clone_unicode_bidi() != new.clone_unicode_bidi()
206        {
207            return true;
208        }
209
210        let old_text = old.get_inherited_text();
211        let new_text = new.get_inherited_text();
212        if !std::ptr::eq(old_text, new_text)
213            && (old_text.white_space_collapse != new_text.white_space_collapse
214                || old_text.text_transform != new_text.text_transform
215                || old_text.word_break != new_text.word_break
216                || old_text.overflow_wrap != new_text.overflow_wrap
217                || old_text.letter_spacing != new_text.letter_spacing
218                || old_text.word_spacing != new_text.word_spacing
219                || old_text.text_rendering != new_text.text_rendering)
220        {
221            return true;
222        }
223
224        false
225    };
226
227    #[allow(
228        clippy::if_same_then_else,
229        reason = "these branches will soon be different"
230    )]
231    if box_tree_needs_rebuild() {
232        ALL_DAMAGE
233    } else if text_shaping_needs_recollect() {
234        ALL_DAMAGE
235    } else {
236        // This element needs to be laid out again, but does not have any damage to
237        // its box. In the future, we will distinguish between types of damage to the
238        // fragment as well.
239        RestyleDamage::RELAYOUT
240    }
241}
242
243/// A child with a z_index that is hoisted up to it's containing Stacking Context for paint purposes
244#[derive(Debug, Clone)]
245pub struct HoistedPaintChild {
246    pub node_id: usize,
247    pub z_index: i32,
248    pub position: taffy::Point<f32>,
249}
250
251#[derive(Debug)]
252pub struct HoistedPaintChildren {
253    pub children: Vec<HoistedPaintChild>,
254    /// The number of hoisted point children with negative z_index
255    pub negative_z_count: u32,
256
257    pub content_area: taffy::Rect<f32>,
258}
259
260impl HoistedPaintChildren {
261    fn new() -> Self {
262        Self {
263            children: Vec::new(),
264            negative_z_count: 0,
265            content_area: taffy::Rect::ZERO,
266        }
267    }
268
269    pub fn reset(&mut self) {
270        self.children.clear();
271        self.negative_z_count = 0;
272    }
273
274    pub fn compute_content_size(&mut self, doc: &BaseDocument) {
275        fn child_pos(child: &HoistedPaintChild, doc: &BaseDocument) -> Rect<f32> {
276            let node = &doc.nodes[child.node_id];
277            let left = child.position.x + node.final_layout.location.x;
278            let top = child.position.y + node.final_layout.location.y;
279            let right = left + node.final_layout.size.width;
280            let bottom = top + node.final_layout.size.height;
281
282            taffy::Rect {
283                top,
284                left,
285                bottom,
286                right,
287            }
288        }
289
290        if self.children.is_empty() {
291            self.content_area = taffy::Rect::ZERO;
292        } else {
293            self.content_area = child_pos(&self.children[0], doc);
294            for child in self.children[1..].iter() {
295                let pos = child_pos(child, doc);
296                self.content_area.left = self.content_area.left.min(pos.left);
297                self.content_area.top = self.content_area.top.min(pos.top);
298                self.content_area.right = self.content_area.right.max(pos.right);
299                self.content_area.bottom = self.content_area.bottom.max(pos.bottom);
300            }
301        }
302    }
303
304    pub fn sort(&mut self) {
305        self.children.sort_by_key(|c| c.z_index);
306        self.negative_z_count = self.children.iter().take_while(|c| c.z_index < 0).count() as u32;
307    }
308
309    pub fn neg_z_range(&self) -> Range<usize> {
310        0..(self.negative_z_count as usize)
311    }
312
313    pub fn pos_z_range(&self) -> Range<usize> {
314        (self.negative_z_count as usize)..self.children.len()
315    }
316
317    pub fn neg_z_hoisted_children(
318        &self,
319    ) -> impl ExactSizeIterator<Item = &HoistedPaintChild> + DoubleEndedIterator {
320        self.children[self.neg_z_range()].iter()
321    }
322
323    pub fn pos_z_hoisted_children(
324        &self,
325    ) -> impl ExactSizeIterator<Item = &HoistedPaintChild> + DoubleEndedIterator {
326        self.children[self.pos_z_range()].iter()
327    }
328}
329
330impl BaseDocument {
331    pub(crate) fn invalidate_inline_contexts(&mut self) {
332        let scale = self.viewport.scale();
333
334        let font_ctx = &self.font_ctx;
335        let layout_ctx = &mut self.layout_ctx;
336
337        let mut anon_nodes = Vec::new();
338
339        for (_, node) in self.nodes.iter_mut() {
340            if !(node.flags.contains(NodeFlags::IS_IN_DOCUMENT)) {
341                continue;
342            }
343
344            let Some(element) = node.data.downcast_element_mut() else {
345                continue;
346            };
347
348            if element.inline_layout_data.is_some() {
349                if node.is_anonymous() {
350                    anon_nodes.push(node.id);
351                } else {
352                    node.insert_damage(ALL_DAMAGE);
353                }
354            } else if let Some(input) = element.text_input_data_mut() {
355                input.editor.set_scale(scale);
356                let mut font_ctx = font_ctx.lock().unwrap();
357                input.editor.refresh_layout(&mut font_ctx, layout_ctx);
358                node.insert_damage(ONLY_RELAYOUT);
359            }
360        }
361
362        for node_id in anon_nodes {
363            if let Some(parent_id) = *(self.nodes[node_id].layout_parent.get_mut()) {
364                self.nodes[parent_id].insert_damage(ALL_DAMAGE);
365            }
366        }
367    }
368
369    pub fn flush_styles_to_layout(&mut self, node_id: usize) {
370        self.flush_styles_to_layout_impl(node_id, None);
371    }
372
373    /// Flush a CSS image layer list (`background-image` or `mask-image`) from style
374    /// to dedicated storage on the node, fetching any images which are not yet loaded.
375    fn flush_image_layers_from_style(&mut self, node_id: usize, kind: ImageLayerKind) {
376        let doc_id = self.id();
377        let node = self.nodes.get_mut(node_id).unwrap();
378        let stylo_element_data = node.stylo_element_data.get();
379        let primary_styles = stylo_element_data
380            .as_ref()
381            .and_then(|data| data.styles.get_primary());
382        let Some(style) = primary_styles else {
383            return;
384        };
385        let Some(elem) = node.data.downcast_element_mut() else {
386            return;
387        };
388
389        let (style_images, elem_images) = match kind {
390            ImageLayerKind::Background => (
391                &style.get_background().background_image.0,
392                &mut elem.background_images,
393            ),
394            ImageLayerKind::Mask => (&style.get_svg().mask_image.0, &mut elem.mask_images),
395        };
396
397        let len = style_images.len();
398        elem_images.resize_with(len, || None);
399
400        for idx in 0..len {
401            let style_image = &style_images[idx];
402            let new_image = match style_image {
403                StyloImage::Url(ComputedUrl::Valid(new_url)) => {
404                    let old_image = elem_images[idx].as_ref();
405                    let old_image_url = old_image.map(|data| &data.url);
406                    if old_image_url.is_some_and(|old_url| **new_url == **old_url) {
407                        break;
408                    }
409
410                    // Check cache first
411                    let url_str = new_url.as_str();
412                    if let Some(cached_image) = self.image_cache.get(url_str) {
413                        #[cfg(feature = "tracing")]
414                        tracing::info!("Loading image {url_str} from cache");
415                        Some(ImageResourceData {
416                            url: new_url.clone(),
417                            status: Status::Ok,
418                            image: cached_image.clone(),
419                        })
420                    } else if let Some(waiting_list) = self.pending_images.get_mut(url_str) {
421                        // Image is already being fetched, queue this node
422                        #[cfg(feature = "tracing")]
423                        tracing::info!("Image {url_str} already pending, queueing node {node_id}");
424                        waiting_list.push((node_id, kind.image_type(idx)));
425                        Some(ImageResourceData::new(new_url.clone()))
426                    } else {
427                        // Start fetch and track as pending
428                        #[cfg(feature = "tracing")]
429                        tracing::info!("Fetching image {url_str}");
430                        self.pending_images
431                            .insert(url_str.to_string(), vec![(node_id, kind.image_type(idx))]);
432
433                        self.net_provider.fetch(
434                            doc_id,
435                            crate::net::stamped_request(
436                                (**new_url).clone(),
437                                self.abort_signal.as_ref(),
438                            ),
439                            ResourceHandler::boxed(
440                                self.tx.clone(),
441                                doc_id,
442                                None, // Don't pass node_id, we'll handle via pending_images
443                                self.shell_provider.clone(),
444                                ImageHandler::new(kind.image_type(idx)),
445                            ),
446                        );
447
448                        Some(ImageResourceData::new(new_url.clone()))
449                    }
450                }
451                _ => None,
452            };
453
454            // Element will always exist due to resize_with above
455            elem_images[idx] = new_image;
456        }
457    }
458
459    /// Walk the whole tree, converting styles to layout
460    fn flush_styles_to_layout_impl(
461        &mut self,
462        node_id: usize,
463        parent_stacking_context: Option<&mut HoistedPaintChildren>,
464    ) {
465        let mut new_stacking_context: HoistedPaintChildren = HoistedPaintChildren::new();
466        let stacking_context = &mut new_stacking_context;
467
468        // Flush background/mask images from style to dedicated storage on the node
469        self.flush_image_layers_from_style(node_id, ImageLayerKind::Background);
470        self.flush_image_layers_from_style(node_id, ImageLayerKind::Mask);
471
472        let display = {
473            let node = self.nodes.get_mut(node_id).unwrap();
474            let _damage = node.damage().unwrap_or(ALL_DAMAGE);
475            let stylo_element_data = node.stylo_element_data.get();
476            let primary_styles = stylo_element_data
477                .as_ref()
478                .and_then(|data| data.styles.get_primary());
479
480            let Some(style) = primary_styles else {
481                return;
482            };
483
484            // if damage.intersects(RestyleDamage::RELAYOUT | CONSTRUCT_BOX) {
485            node.style = stylo_taffy::to_taffy_style(style);
486            node.display_constructed_as = style.clone_display();
487            // }
488
489            // In non-incremental mode we unconditionally clear the Taffy cache.
490            // In incremental mode this is handled as part of damage propagation.
491            if NON_INCREMENTAL {
492                node.cache.clear();
493                if let Some(inline_layout) = node
494                    .data
495                    .downcast_element_mut()
496                    .and_then(|el| el.inline_layout_data.as_mut())
497                {
498                    inline_layout.content_widths = None;
499                }
500            }
501
502            node.style.display
503        };
504
505        // If the node has children, then take those children and...
506        let children = self.nodes[node_id].layout_children.borrow_mut().take();
507        if let Some(mut children) = children {
508            let is_flex_or_grid = matches!(display, taffy::Display::Flex | taffy::Display::Grid);
509
510            // Recursively call flush_styles_to_layout on each child
511            for &child in children.iter() {
512                self.flush_styles_to_layout_impl(
513                    child,
514                    match self.nodes[child].is_stacking_context_root(is_flex_or_grid) {
515                        true => None,
516                        false => Some(stacking_context),
517                    },
518                );
519            }
520
521            // Sort layout_children
522            if is_flex_or_grid {
523                children.sort_by(|left, right| {
524                    let left_node = self.nodes.get(*left).unwrap();
525                    let right_node = self.nodes.get(*right).unwrap();
526                    left_node.order().cmp(&right_node.order())
527                });
528            }
529
530            // Reserve space for paint_children
531            let mut paint_children = self.nodes[node_id].paint_children.borrow_mut();
532            if paint_children.is_none() {
533                *paint_children = Some(Vec::new());
534            }
535            let paint_children = paint_children.as_mut().unwrap();
536            paint_children.clear();
537            paint_children.reserve(children.len());
538
539            // Push children to either paint_children or layout_children depending on
540            for &child_id in children.iter() {
541                let child = &self.nodes[child_id];
542
543                let Some(style) = child.primary_styles() else {
544                    paint_children.push(child_id);
545                    continue;
546                };
547
548                let position = style.clone_position();
549                let z_index = style.clone_z_index().integer_or(0);
550
551                // TODO: more complete hoisting detection
552                // z-index applies to static flex/grid items too
553                // (css-flexbox-1 §painting, css-grid-1 §z-order).
554                if z_index != 0 && (position != Position::Static || is_flex_or_grid) {
555                    stacking_context.children.push(HoistedPaintChild {
556                        node_id: child_id,
557                        z_index,
558                        position: taffy::Point::ZERO,
559                    })
560                } else {
561                    paint_children.push(child_id);
562                }
563            }
564
565            // Sort paint_children
566            paint_children.sort_by(|left, right| {
567                let left_node = self.nodes.get(*left).unwrap();
568                let right_node = self.nodes.get(*right).unwrap();
569                node_to_paint_order(left_node, is_flex_or_grid)
570                    .cmp(&node_to_paint_order(right_node, is_flex_or_grid))
571            });
572
573            // Put children back
574            *self.nodes[node_id].layout_children.borrow_mut() = Some(children);
575        }
576
577        if let Some(parent_stacking_context) = parent_stacking_context {
578            let position = self.nodes[node_id].final_layout.location;
579            let scroll_offset = self.nodes[node_id].scroll_offset;
580            for hoisted in stacking_context.children.iter_mut() {
581                hoisted.position.x += position.x - scroll_offset.x as f32;
582                hoisted.position.y += position.y - scroll_offset.y as f32;
583            }
584            parent_stacking_context
585                .children
586                .extend(stacking_context.children.iter().cloned());
587        } else {
588            stacking_context.sort();
589            stacking_context.compute_content_size(self);
590            self.nodes[node_id].stacking_context = Some(Box::new(new_stacking_context));
591        }
592    }
593}
594
595#[inline(always)]
596fn position_to_order(pos: Position) -> i32 {
597    match pos {
598        Position::Static => 0,
599        // All positioned descendants with z-index: auto share one paint
600        // level (CSS 2.1 Appendix E step 8); the stable sort keeps them in
601        // tree order among themselves, above in-flow content and floats.
602        Position::Relative | Position::Sticky | Position::Absolute | Position::Fixed => 2,
603    }
604}
605#[inline(always)]
606fn float_to_order(pos: Float) -> i32 {
607    match pos {
608        Float::None => 0,
609        _ => 1,
610    }
611}
612
613/// Paint sort key: (paint level, order-modified position). Positioned
614/// (z-index: auto) descendants paint above in-flow content (CSS 2.1
615/// Appendix E step 8); within a level the stable sort preserves
616/// (order-modified) document order.
617#[inline(always)]
618fn node_to_paint_order(node: &Node, is_flex_or_grid: bool) -> (i32, i32) {
619    let Some(style) = node.primary_styles() else {
620        return (0, 0);
621    };
622    let position = style.clone_position();
623    if is_flex_or_grid {
624        match position {
625            Position::Static => (0, style.clone_order()),
626            Position::Relative | Position::Sticky => (2, style.clone_order()),
627            // Out-of-flow children are not flex/grid items: `order` does
628            // not apply; tree order does.
629            Position::Absolute | Position::Fixed => (2, 0),
630        }
631    } else {
632        (
633            position_to_order(position) + float_to_order(style.clone_float()),
634            0,
635        )
636    }
637}