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