Skip to main content

blitz_dom/layout/
damage.rs

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