Skip to main content

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