blitz_dom/layout/
damage.rs

1use crate::NON_INCREMENTAL;
2use crate::node::NodeFlags;
3use crate::{BaseDocument, net::ImageHandler, node::BackgroundImageData, util::ImageType};
4use blitz_traits::net::Request;
5use style::properties::ComputedValues;
6use style::properties::generated::longhands::position::computed_value::T as Position;
7use style::selector_parser::RestyleDamage;
8use style::servo::url::ComputedUrl;
9use style::values::generics::image::Image as StyloImage;
10use style::values::specified::align::AlignFlags;
11use style::values::specified::box_::DisplayInside;
12use style::values::specified::box_::DisplayOutside;
13
14pub(crate) const CONSTRUCT_BOX: RestyleDamage =
15    RestyleDamage::from_bits_retain(0b_0000_0000_0001_0000);
16pub(crate) const CONSTRUCT_FC: RestyleDamage =
17    RestyleDamage::from_bits_retain(0b_0000_0000_0010_0000);
18pub(crate) const CONSTRUCT_DESCENDENT: RestyleDamage =
19    RestyleDamage::from_bits_retain(0b_0000_0000_0100_0000);
20
21pub(crate) const ONLY_RELAYOUT: RestyleDamage =
22    RestyleDamage::from_bits_retain(0b_0000_0000_0000_1000);
23
24pub(crate) const ALL_DAMAGE: RestyleDamage =
25    RestyleDamage::from_bits_retain(0b_0000_0000_0111_1111);
26
27impl BaseDocument {
28    #[cfg(feature = "incremental")]
29    pub(crate) fn propagate_damage_flags(
30        &mut self,
31        node_id: usize,
32        damage_from_parent: RestyleDamage,
33    ) -> RestyleDamage {
34        let Some(mut damage) = self.nodes[node_id].damage() else {
35            return RestyleDamage::empty();
36        };
37        damage |= damage_from_parent;
38
39        let damage_for_children = RestyleDamage::empty();
40        let children = std::mem::take(&mut self.nodes[node_id].children);
41        let layout_children = std::mem::take(self.nodes[node_id].layout_children.get_mut());
42        let use_layout_children = self.nodes[node_id].should_traverse_layout_children();
43        if use_layout_children {
44            let layout_children = layout_children.as_ref().unwrap();
45            for child in layout_children.iter() {
46                damage |= self.propagate_damage_flags(*child, damage_for_children);
47            }
48        } else {
49            for child in children.iter() {
50                damage |= self.propagate_damage_flags(*child, damage_for_children);
51            }
52            if let Some(before_id) = self.nodes[node_id].before {
53                damage |= self.propagate_damage_flags(before_id, damage_for_children);
54            }
55            if let Some(after_id) = self.nodes[node_id].after {
56                damage |= self.propagate_damage_flags(after_id, damage_for_children);
57            }
58        }
59
60        let node = &mut self.nodes[node_id];
61
62        // Put children back
63        node.children = children;
64        *node.layout_children.get_mut() = layout_children;
65
66        if damage.contains(CONSTRUCT_BOX) {
67            damage.insert(RestyleDamage::RELAYOUT);
68        }
69
70        // Compute damage to propagate to parent
71        let damage_for_parent = damage; // & RestyleDamage::RELAYOUT;
72
73        // If the node or any of it's children have been mutated or their layout styles
74        // have changed, then we should clear it's layout cache.
75        if damage.intersects(ONLY_RELAYOUT | CONSTRUCT_BOX) {
76            node.cache.clear();
77            if let Some(inline_layout) = node
78                .data
79                .downcast_element_mut()
80                .and_then(|el| el.inline_layout_data.as_mut())
81            {
82                inline_layout.content_widths = None;
83            }
84            damage.remove(ONLY_RELAYOUT);
85        }
86
87        // Store damage for current node
88        node.set_damage(damage);
89
90        // let _is_fc_root = node
91        //     .primary_styles()
92        //     .map(|s| is_fc_root(&s))
93        //     .unwrap_or(false);
94
95        // if damage.contains(CONSTRUCT_BOX) {
96        //     // damage_for_parent.insert(CONSTRUCT_FC | CONSTRUCT_DESCENDENT);
97        //     damage_for_parent.insert(CONSTRUCT_BOX);
98        // }
99
100        // if damage.contains(CONSTRUCT_FC) {
101        //     damage_for_parent.insert(CONSTRUCT_DESCENDENT);
102        //     // if !is_fc_root {
103        //     damage_for_parent.insert(CONSTRUCT_FC);
104        //     // }
105        // }
106
107        // Propagate damage to parent
108        damage_for_parent
109    }
110}
111
112// #[cfg(feature = "incremental")]
113// fn is_fc_root(style: &ComputedValues) -> bool {
114//     let display = style.clone_display();
115//     let display_inside = display.inside();
116
117//     match display_inside {
118//         DisplayInside::Flow => {
119//             // Depends on parent context
120//             false
121//         }
122
123//         DisplayInside::None => true,
124//         DisplayInside::FlowRoot => true,
125//         DisplayInside::Flex => true,
126//         DisplayInside::Grid => true,
127//         DisplayInside::Table => true,
128//         DisplayInside::TableCell => true,
129
130//         DisplayInside::Contents => false,
131//         DisplayInside::TableRowGroup => false,
132//         DisplayInside::TableColumn => false,
133//         DisplayInside::TableColumnGroup => false,
134//         DisplayInside::TableHeaderGroup => false,
135//         DisplayInside::TableFooterGroup => false,
136//         DisplayInside::TableRow => false,
137//     }
138// }
139
140pub(crate) fn compute_layout_damage(old: &ComputedValues, new: &ComputedValues) -> RestyleDamage {
141    let box_tree_needs_rebuild = || {
142        let old_box = old.get_box();
143        let new_box = new.get_box();
144
145        if old_box.display != new_box.display
146            || old_box.float != new_box.float
147            || old_box.position != new_box.position
148        {
149            return true;
150        }
151
152        if old.get_font() != new.get_font() {
153            return true;
154        }
155
156        if new_box.display.outside() == DisplayOutside::Block
157            && new_box.display.inside() == DisplayInside::Flow
158        {
159            let alignment_establishes_new_block_formatting_context = |style: &ComputedValues| {
160                style.get_position().align_content.0.primary() != AlignFlags::NORMAL
161            };
162
163            let old_column = old.get_column();
164            let new_column = new.get_column();
165            if old_box.overflow_x.is_scrollable() != new_box.overflow_x.is_scrollable()
166                || old_column.is_multicol() != new_column.is_multicol()
167                || old_column.column_span != new_column.column_span
168                || alignment_establishes_new_block_formatting_context(old)
169                    != alignment_establishes_new_block_formatting_context(new)
170            {
171                return true;
172            }
173        }
174
175        if old_box.display.is_list_item() {
176            let old_list = old.get_list();
177            let new_list = new.get_list();
178            if old_list.list_style_position != new_list.list_style_position
179                || old_list.list_style_image != new_list.list_style_image
180                || (new_list.list_style_image == StyloImage::None
181                    && old_list.list_style_type != new_list.list_style_type)
182            {
183                return true;
184            }
185        }
186
187        if new.is_pseudo_style() && old.get_counters().content != new.get_counters().content {
188            return true;
189        }
190
191        false
192    };
193
194    let text_shaping_needs_recollect = || {
195        if old.clone_direction() != new.clone_direction()
196            || old.clone_unicode_bidi() != new.clone_unicode_bidi()
197        {
198            return true;
199        }
200
201        let old_text = old.get_inherited_text();
202        let new_text = new.get_inherited_text();
203        if !std::ptr::eq(old_text, new_text)
204            && (old_text.white_space_collapse != new_text.white_space_collapse
205                || old_text.text_transform != new_text.text_transform
206                || old_text.word_break != new_text.word_break
207                || old_text.overflow_wrap != new_text.overflow_wrap
208                || old_text.letter_spacing != new_text.letter_spacing
209                || old_text.word_spacing != new_text.word_spacing
210                || old_text.text_rendering != new_text.text_rendering)
211        {
212            return true;
213        }
214
215        false
216    };
217
218    #[allow(
219        clippy::if_same_then_else,
220        reason = "these branches will soon be different"
221    )]
222    if box_tree_needs_rebuild() {
223        ALL_DAMAGE
224    } else if text_shaping_needs_recollect() {
225        ALL_DAMAGE
226    } else {
227        // This element needs to be laid out again, but does not have any damage to
228        // its box. In the future, we will distinguish between types of damage to the
229        // fragment as well.
230        RestyleDamage::RELAYOUT
231    }
232}
233
234impl BaseDocument {
235    pub(crate) fn invalidate_inline_contexts(&mut self) {
236        let scale = self.viewport.scale();
237
238        let font_ctx = &self.font_ctx;
239        let layout_ctx = &mut self.layout_ctx;
240
241        for (_, node) in self.nodes.iter_mut() {
242            if !(node.flags.contains(NodeFlags::IS_IN_DOCUMENT)) {
243                continue;
244            }
245            let Some(element) = node.data.downcast_element_mut() else {
246                continue;
247            };
248
249            if element.inline_layout_data.is_some() {
250                node.insert_damage(ALL_DAMAGE);
251            } else if let Some(input) = element.text_input_data_mut() {
252                input.editor.set_scale(scale);
253                let mut font_ctx = font_ctx.lock().unwrap();
254                input.editor.refresh_layout(&mut font_ctx, layout_ctx);
255                node.insert_damage(ONLY_RELAYOUT);
256            }
257        }
258    }
259
260    /// Walk the whole tree, converting styles to layout
261    pub fn flush_styles_to_layout(&mut self, node_id: usize) {
262        let doc_id = self.id();
263
264        let display = {
265            let node = self.nodes.get_mut(node_id).unwrap();
266            let _damage = node.damage().unwrap_or(ALL_DAMAGE);
267            let stylo_element_data = node.stylo_element_data.borrow();
268            let primary_styles = stylo_element_data
269                .as_ref()
270                .and_then(|data| data.styles.get_primary());
271
272            let Some(style) = primary_styles else {
273                return;
274            };
275
276            // if damage.intersects(RestyleDamage::RELAYOUT | CONSTRUCT_BOX) {
277            node.style = stylo_taffy::to_taffy_style(style);
278            node.display_constructed_as = style.clone_display();
279            // }
280
281            // Flush background image from style to dedicated storage on the node
282            // TODO: handle multiple background images
283            if let Some(elem) = node.data.downcast_element_mut() {
284                let style_bgs = &style.get_background().background_image.0;
285                let elem_bgs = &mut elem.background_images;
286
287                let len = style_bgs.len();
288                elem_bgs.resize_with(len, || None);
289
290                for idx in 0..len {
291                    let background_image = &style_bgs[idx];
292                    let new_bg_image = match background_image {
293                        StyloImage::Url(ComputedUrl::Valid(new_url)) => {
294                            let old_bg_image = elem_bgs[idx].as_ref();
295                            let old_bg_image_url = old_bg_image.map(|data| &data.url);
296                            if old_bg_image_url.is_some_and(|old_url| **new_url == **old_url) {
297                                break;
298                            }
299
300                            self.net_provider.fetch(
301                                doc_id,
302                                Request::get((**new_url).clone()),
303                                Box::new(ImageHandler::new(node_id, ImageType::Background(idx))),
304                            );
305
306                            let bg_image_data = BackgroundImageData::new(new_url.clone());
307                            Some(bg_image_data)
308                        }
309                        _ => None,
310                    };
311
312                    // Element will always exist due to resize_with above
313                    elem_bgs[idx] = new_bg_image;
314                }
315            }
316
317            // In non-incremental mode we unconditionally clear the Taffy cache.
318            // In incremental mode this is handled as part of damage propagation.
319            if NON_INCREMENTAL {
320                node.cache.clear();
321                if let Some(inline_layout) = node
322                    .data
323                    .downcast_element_mut()
324                    .and_then(|el| el.inline_layout_data.as_mut())
325                {
326                    inline_layout.content_widths = None;
327                }
328            }
329
330            node.style.display
331        };
332
333        // If the node has children, then take those children and...
334        let children = self.nodes[node_id].layout_children.borrow_mut().take();
335        if let Some(mut children) = children {
336            // Recursively call flush_styles_to_layout on each child
337            for child in children.iter() {
338                self.flush_styles_to_layout(*child);
339            }
340
341            // If the node is a Flexbox or Grid node then sort by css order property
342            if matches!(display, taffy::Display::Flex | taffy::Display::Grid) {
343                children.sort_by(|left, right| {
344                    let left_node = self.nodes.get(*left).unwrap();
345                    let right_node = self.nodes.get(*right).unwrap();
346                    left_node.order().cmp(&right_node.order())
347                });
348            }
349
350            // Put children back
351            *self.nodes[node_id].layout_children.borrow_mut() = Some(children);
352
353            // Sort paint_children in place
354            self.nodes[node_id]
355                .paint_children
356                .borrow_mut()
357                .as_mut()
358                .unwrap()
359                .sort_by(|left, right| {
360                    let left_node = self.nodes.get(*left).unwrap();
361                    let right_node = self.nodes.get(*right).unwrap();
362                    left_node
363                        .z_index()
364                        .cmp(&right_node.z_index())
365                        .then_with(|| {
366                            fn position_to_order(pos: Position) -> u8 {
367                                match pos {
368                                    Position::Static | Position::Relative | Position::Sticky => 0,
369                                    Position::Absolute | Position::Fixed => 1,
370                                }
371                            }
372                            let left_position = left_node
373                                .primary_styles()
374                                .map(|s| position_to_order(s.clone_position()))
375                                .unwrap_or(0);
376                            let right_position = right_node
377                                .primary_styles()
378                                .map(|s| position_to_order(s.clone_position()))
379                                .unwrap_or(0);
380
381                            left_position.cmp(&right_position)
382                        })
383                })
384        }
385    }
386}