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::BackgroundImageData, node::Status, util::ImageType,
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    /// Walk the whole tree, converting styles to layout
374    fn flush_styles_to_layout_impl(
375        &mut self,
376        node_id: usize,
377        parent_stacking_context: Option<&mut HoistedPaintChildren>,
378    ) {
379        let doc_id = self.id();
380
381        let mut new_stacking_context: HoistedPaintChildren = HoistedPaintChildren::new();
382        let stacking_context = &mut new_stacking_context;
383
384        let display = {
385            let node = self.nodes.get_mut(node_id).unwrap();
386            let _damage = node.damage().unwrap_or(ALL_DAMAGE);
387            let stylo_element_data = node.stylo_element_data.get();
388            let primary_styles = stylo_element_data
389                .as_ref()
390                .and_then(|data| data.styles.get_primary());
391
392            let Some(style) = primary_styles else {
393                return;
394            };
395
396            // if damage.intersects(RestyleDamage::RELAYOUT | CONSTRUCT_BOX) {
397            node.style = stylo_taffy::to_taffy_style(style);
398            node.display_constructed_as = style.clone_display();
399            // }
400
401            // Flush background image from style to dedicated storage on the node
402            // TODO: handle multiple background images
403            if let Some(elem) = node.data.downcast_element_mut() {
404                let style_bgs = &style.get_background().background_image.0;
405                let elem_bgs = &mut elem.background_images;
406
407                let len = style_bgs.len();
408                elem_bgs.resize_with(len, || None);
409
410                for idx in 0..len {
411                    let background_image = &style_bgs[idx];
412                    let new_bg_image = match background_image {
413                        StyloImage::Url(ComputedUrl::Valid(new_url)) => {
414                            let old_bg_image = elem_bgs[idx].as_ref();
415                            let old_bg_image_url = old_bg_image.map(|data| &data.url);
416                            if old_bg_image_url.is_some_and(|old_url| **new_url == **old_url) {
417                                break;
418                            }
419
420                            // Check cache first
421                            let url_str = new_url.as_str();
422                            if let Some(cached_image) = self.image_cache.get(url_str) {
423                                #[cfg(feature = "tracing")]
424                                tracing::info!("Loading image {url_str} from cache");
425                                Some(BackgroundImageData {
426                                    url: new_url.clone(),
427                                    status: Status::Ok,
428                                    image: cached_image.clone(),
429                                })
430                            } else if let Some(waiting_list) = self.pending_images.get_mut(url_str)
431                            {
432                                // Image is already being fetched, queue this node
433                                #[cfg(feature = "tracing")]
434                                tracing::info!(
435                                    "Image {url_str} already pending, queueing node {node_id}"
436                                );
437                                waiting_list.push((node_id, ImageType::Background(idx)));
438                                Some(BackgroundImageData::new(new_url.clone()))
439                            } else {
440                                // Start fetch and track as pending
441                                #[cfg(feature = "tracing")]
442                                tracing::info!("Fetching image {url_str}");
443                                self.pending_images.insert(
444                                    url_str.to_string(),
445                                    vec![(node_id, ImageType::Background(idx))],
446                                );
447
448                                self.net_provider.fetch(
449                                    doc_id,
450                                    crate::net::stamped_request(
451                                        (**new_url).clone(),
452                                        self.abort_signal.as_ref(),
453                                    ),
454                                    ResourceHandler::boxed(
455                                        self.tx.clone(),
456                                        doc_id,
457                                        None, // Don't pass node_id, we'll handle via pending_images
458                                        self.shell_provider.clone(),
459                                        ImageHandler::new(ImageType::Background(idx)),
460                                    ),
461                                );
462
463                                Some(BackgroundImageData::new(new_url.clone()))
464                            }
465                        }
466                        _ => None,
467                    };
468
469                    // Element will always exist due to resize_with above
470                    elem_bgs[idx] = new_bg_image;
471                }
472            }
473
474            // In non-incremental mode we unconditionally clear the Taffy cache.
475            // In incremental mode this is handled as part of damage propagation.
476            if NON_INCREMENTAL {
477                node.cache.clear();
478                if let Some(inline_layout) = node
479                    .data
480                    .downcast_element_mut()
481                    .and_then(|el| el.inline_layout_data.as_mut())
482                {
483                    inline_layout.content_widths = None;
484                }
485            }
486
487            node.style.display
488        };
489
490        // If the node has children, then take those children and...
491        let children = self.nodes[node_id].layout_children.borrow_mut().take();
492        if let Some(mut children) = children {
493            let is_flex_or_grid = matches!(display, taffy::Display::Flex | taffy::Display::Grid);
494
495            // Recursively call flush_styles_to_layout on each child
496            for &child in children.iter() {
497                self.flush_styles_to_layout_impl(
498                    child,
499                    match self.nodes[child].is_stacking_context_root(is_flex_or_grid) {
500                        true => None,
501                        false => Some(stacking_context),
502                    },
503                );
504            }
505
506            // Sort layout_children
507            if is_flex_or_grid {
508                children.sort_by(|left, right| {
509                    let left_node = self.nodes.get(*left).unwrap();
510                    let right_node = self.nodes.get(*right).unwrap();
511                    left_node.order().cmp(&right_node.order())
512                });
513            }
514
515            // Reserve space for paint_children
516            let mut paint_children = self.nodes[node_id].paint_children.borrow_mut();
517            if paint_children.is_none() {
518                *paint_children = Some(Vec::new());
519            }
520            let paint_children = paint_children.as_mut().unwrap();
521            paint_children.clear();
522            paint_children.reserve(children.len());
523
524            // Push children to either paint_children or layout_children depending on
525            for &child_id in children.iter() {
526                let child = &self.nodes[child_id];
527
528                let Some(style) = child.primary_styles() else {
529                    paint_children.push(child_id);
530                    continue;
531                };
532
533                let position = style.clone_position();
534                let z_index = style.clone_z_index().integer_or(0);
535
536                // TODO: more complete hoisting detection
537                if position != Position::Static && z_index != 0 {
538                    stacking_context.children.push(HoistedPaintChild {
539                        node_id: child_id,
540                        z_index,
541                        position: taffy::Point::ZERO,
542                    })
543                } else {
544                    paint_children.push(child_id);
545                }
546            }
547
548            // Sort paint_children
549            paint_children.sort_by(|left, right| {
550                let left_node = self.nodes.get(*left).unwrap();
551                let right_node = self.nodes.get(*right).unwrap();
552                node_to_paint_order(left_node, is_flex_or_grid)
553                    .cmp(&node_to_paint_order(right_node, is_flex_or_grid))
554            });
555
556            // Put children back
557            *self.nodes[node_id].layout_children.borrow_mut() = Some(children);
558        }
559
560        if let Some(parent_stacking_context) = parent_stacking_context {
561            let position = self.nodes[node_id].final_layout.location;
562            let scroll_offset = self.nodes[node_id].scroll_offset;
563            for hoisted in stacking_context.children.iter_mut() {
564                hoisted.position.x += position.x - scroll_offset.x as f32;
565                hoisted.position.y += position.y - scroll_offset.y as f32;
566            }
567            parent_stacking_context
568                .children
569                .extend(stacking_context.children.iter().cloned());
570        } else {
571            stacking_context.sort();
572            stacking_context.compute_content_size(self);
573            self.nodes[node_id].stacking_context = Some(Box::new(new_stacking_context));
574        }
575    }
576}
577
578#[inline(always)]
579fn position_to_order(pos: Position) -> i32 {
580    match pos {
581        Position::Static | Position::Relative | Position::Sticky => 0,
582        Position::Absolute | Position::Fixed => 2,
583    }
584}
585#[inline(always)]
586fn float_to_order(pos: Float) -> i32 {
587    match pos {
588        Float::None => 0,
589        _ => 1,
590    }
591}
592
593#[inline(always)]
594fn node_to_paint_order(node: &Node, is_flex_or_grid: bool) -> i32 {
595    let Some(style) = node.primary_styles() else {
596        return 0;
597    };
598    if is_flex_or_grid {
599        match style.clone_position() {
600            Position::Static | Position::Relative | Position::Sticky => style.clone_order(),
601            Position::Absolute | Position::Fixed => 0,
602        }
603    } else {
604        position_to_order(style.clone_position()) + float_to_order(style.clone_float())
605    }
606}