Skip to main content

apace_dom/layout/
paint_tree.rs

1//! Construction of the paint tree (`Node::paint_children` and
2//! `Node::stacking_context`) from the laid-out box tree.
3//!
4//! The pass runs once per frame after layout and builds *topology* only: which
5//! node ids each node paints itself and which z-indexed descendants are hoisted
6//! to the nearest stacking-context root. Geometry (a hoisted box's offset from
7//! its stacking-context root) is derived at use time from `final_layout()` and
8//! memoised per geometry generation, so replaying a cached list for a clean
9//! subtree can never paint or hit-test a box where layout no longer puts it.
10
11use apace_traits::node_id::NodeId;
12use std::cell::Cell;
13use std::ops::Range;
14use stylo_takia::CopyStyleGetters;
15use thin_vec::ThinVec;
16
17use crate::layout::damage::ALL_DAMAGE;
18use crate::tree::NodeTree;
19use crate::{BaseDocument, Node};
20use style::properties::generated::longhands::position::computed_value::T as Position;
21use style::selector_parser::RestyleDamage;
22use style::values::computed::Float;
23use style::values::specified::box_::DisplayInside;
24use takia::{Point, Rect};
25
26/// Offset of the hoisted child `child_id` from the border box of its
27/// stacking-context root `sc_root_id`, excluding the child's own
28/// `final_layout().location`: the sum of `location - scroll_offset` of every
29/// box strictly between the two on the child's positioning chain.
30///
31/// Walks `Node::containing_block` (the out-of-flow containing block for boxes
32/// hoisted by Takia, otherwise `layout_parent`, which is set for every layout
33/// child by `resolve_layout_children`, anonymous blocks included).
34pub fn hoisted_child_position(tree: &NodeTree, sc_root_id: NodeId, child_id: NodeId) -> Point<f32> {
35    let child = &tree[child_id];
36    let mut position = Point::ZERO;
37    let mut current = child.containing_block();
38
39    // A fixed-position box whose containing block is the root element is
40    // positioned against the viewport and does not scroll with it: cancel the
41    // scroll the walk below (or the stacking-context root itself) applies.
42    // A fixed box contained by a transformed ancestor scrolls with it.
43    if child.takia_position() == takia::Position::Fixed
44        && let Some(cb) = current.and_then(|id| tree.get(id))
45        && cb.containing_block().is_none()
46        && !cb.flags.is_top_layer()
47    {
48        let scroll_offset = *cb.scroll_offset();
49        let viewport_scroll = tree.viewport_scroll();
50        position.x += scroll_offset.x as f32 + viewport_scroll.x as f32;
51        position.y += scroll_offset.y as f32 + viewport_scroll.y as f32;
52    }
53
54    while let Some(id) = current {
55        if id == sc_root_id {
56            break;
57        }
58        let Some(node) = tree.get(id) else {
59            break;
60        };
61        let location = node.final_layout().location;
62        let scroll_offset = *node.scroll_offset();
63        position.x += location.x - scroll_offset.x as f32;
64        position.y += location.y - scroll_offset.y as f32;
65        current = node.containing_block();
66    }
67    position
68}
69
70/// A child with a z_index that is hoisted up to it's containing Stacking Context for paint purposes
71#[derive(Debug, Clone)]
72pub struct HoistedPaintChild {
73    pub node_id: NodeId,
74    pub z_index: i32,
75    /// `(geometry generation, offset from the stacking-context root)`, see
76    /// [`Self::position`]. Generation `0` is never current.
77    position: Cell<(u64, Point<f32>)>,
78}
79
80impl HoistedPaintChild {
81    fn new(node_id: NodeId, z_index: i32) -> Self {
82        Self {
83            node_id,
84            z_index,
85            position: Cell::new((0, Point::ZERO)),
86        }
87    }
88
89    /// The child's offset from the border box of its stacking-context root
90    /// (`sc_root_id`), excluding the child's own `final_layout().location`.
91    /// Computed at most once per geometry generation.
92    pub fn position(&self, tree: &NodeTree, sc_root_id: NodeId) -> Point<f32> {
93        let generation = tree.geometry_generation();
94        let (stamp, cached) = self.position.get();
95        if stamp == generation {
96            return cached;
97        }
98        let position = hoisted_child_position(tree, sc_root_id, self.node_id);
99        self.position.set((generation, position));
100        position
101    }
102
103    /// Whether [`Self::position`] is memoised for the current geometry generation.
104    pub fn position_is_cached(&self, tree: &NodeTree) -> bool {
105        self.position.get().0 == tree.geometry_generation()
106    }
107}
108
109#[derive(Debug)]
110pub struct StackingContext {
111    pub children: ThinVec<HoistedPaintChild>,
112    /// The number of hoisted point children with negative z_index
113    pub negative_z_count: u32,
114    /// `(geometry generation, bounding box of the hoisted children)`, see
115    /// [`Self::hoisted_content_bbox`].
116    hoisted_content_bbox: Cell<(u64, Rect<f32>)>,
117}
118
119impl StackingContext {
120    /// Remove a retired box while retaining the surviving paint order for
121    /// hit testing before the next layout pass.
122    pub(crate) fn retire_child(&mut self, node_id: NodeId) {
123        self.children.retain(|child| child.node_id != node_id);
124        self.negative_z_count = self.children.iter().take_while(|c| c.z_index < 0).count() as u32;
125        self.hoisted_content_bbox.set((0, Rect::ZERO));
126    }
127
128    /// Build a sorted stacking context from the hoisted entries collected for it.
129    fn from_children(children: ThinVec<HoistedPaintChild>) -> Self {
130        let mut sc = Self {
131            children,
132            negative_z_count: 0,
133            hoisted_content_bbox: Cell::new((0, Rect::ZERO)),
134        };
135        sc.sort();
136        sc
137    }
138
139    /// Bounding box (in CSS pixels relative to the stacking-context root
140    /// `sc_root_id`, unscrolled) of the scrollable overflow of all hoisted
141    /// children, including their transforms. `scale` is the device pixel
142    /// ratio `scrollable_overflow` is stored in. Computed at most once per
143    /// geometry generation.
144    pub fn hoisted_content_bbox(
145        &self,
146        tree: &NodeTree,
147        sc_root_id: NodeId,
148        scale: f64,
149    ) -> Rect<f32> {
150        let generation = tree.geometry_generation();
151        let (stamp, cached) = self.hoisted_content_bbox.get();
152        if stamp == generation {
153            return cached;
154        }
155
156        let child_rect = |child: &HoistedPaintChild| -> Option<Rect<f32>> {
157            let node = tree.get(child.node_id)?;
158            let position = child.position(tree, sc_root_id);
159            let location = node.final_layout().location;
160            let mut overflow = *node.scrollable_overflow();
161            if let Some(transform) = node.transform().as_deref() {
162                overflow = transform.transform_rect_bbox(overflow);
163            }
164            let left = position.x + location.x;
165            let top = position.y + location.y;
166            Some(Rect {
167                left: left + (overflow.x0 / scale) as f32,
168                top: top + (overflow.y0 / scale) as f32,
169                right: left + (overflow.x1 / scale) as f32,
170                bottom: top + (overflow.y1 / scale) as f32,
171            })
172        };
173
174        // Children dropped by a DOM mutation since the last `resolve` yield
175        // `None` and are left out, like hit testing skips them.
176        let mut rects = self.children.iter().filter_map(child_rect);
177        let mut area = rects.next().unwrap_or(Rect::ZERO);
178        for pos in rects {
179            area.left = area.left.min(pos.left);
180            area.top = area.top.min(pos.top);
181            area.right = area.right.max(pos.right);
182            area.bottom = area.bottom.max(pos.bottom);
183        }
184        self.hoisted_content_bbox.set((generation, area));
185        area
186    }
187
188    pub fn sort(&mut self) {
189        self.children.sort_by_key(|c| c.z_index);
190        self.negative_z_count = self.children.iter().take_while(|c| c.z_index < 0).count() as u32;
191    }
192
193    pub fn neg_z_range(&self) -> Range<usize> {
194        0..(self.negative_z_count as usize)
195    }
196
197    pub fn pos_z_range(&self) -> Range<usize> {
198        (self.negative_z_count as usize)..self.children.len()
199    }
200
201    pub fn neg_z_hoisted_children(
202        &self,
203    ) -> impl ExactSizeIterator<Item = &HoistedPaintChild> + DoubleEndedIterator {
204        self.children[self.neg_z_range()].iter()
205    }
206
207    pub fn pos_z_hoisted_children(
208        &self,
209    ) -> impl ExactSizeIterator<Item = &HoistedPaintChild> + DoubleEndedIterator {
210        self.children[self.pos_z_range()].iter()
211    }
212}
213
214impl BaseDocument {
215    /// Rebuild `paint_children` and `stacking_context` for the tree rooted at
216    /// `root_id` from the laid-out box tree.
217    ///
218    /// Each node's layout children are placed either in its `paint_children`
219    /// (sorted by paint order) or, when they have a non-zero `z-index` and are
220    /// positioned or flex/grid items, in the nearest ancestor stacking context.
221    ///
222    /// Subtrees whose stored damage lacks `REBUILD_STACKING_CONTEXT` are skipped: their
223    /// own lists are still valid and the entries they contributed to the
224    /// enclosing stacking context are replayed from `Node::sc_contribution_cache`.
225    pub(crate) fn build_paint_tree(&mut self, root_id: NodeId) {
226        self.build_paint_tree_impl(root_id, None);
227    }
228
229    /// `stacking_context` is the entry list of the nearest ancestor stacking
230    /// context, or `None` when `node_id` is itself a stacking-context root.
231    fn build_paint_tree_impl(
232        &mut self,
233        node_id: NodeId,
234        stacking_context: Option<&mut ThinVec<HoistedPaintChild>>,
235    ) {
236        {
237            let node = &self.nodes[node_id];
238            // `propagate_damage_flags` stores the union of a node's own and
239            // its descendants' damage on the node, so the absence of
240            // `REBUILD_STACKING_CONTEXT` (which every heavier level implies)
241            // means nothing in the subtree can have changed a paint list.
242            // Anonymous boxes are never skipped: damage marking walks the DOM
243            // parent chain, which bypasses them.
244            //
245            // Whether a node is a stacking-context root also depends on its
246            // parent (z-indexed flex/grid items), which does not damage the
247            // node itself, so the role the caller resolved must match the one
248            // this node was last built with.
249            let role_unchanged = node.stacking_context.is_some() == stacking_context.is_none();
250            let clean = role_unchanged
251                && !node
252                    .damage()
253                    .unwrap_or(ALL_DAMAGE)
254                    .contains(RestyleDamage::REBUILD_STACKING_CONTEXT)
255                && !node.is_anonymous();
256            if clean {
257                if let Some(stacking_context) = stacking_context {
258                    stacking_context.extend(node.sc_contribution_cache.borrow().iter().cloned());
259                }
260                return;
261            }
262        }
263
264        let Some(display) = self.nodes[node_id].display_style() else {
265            return;
266        };
267
268        // Entries hoisted out of this subtree are collected directly into the
269        // storage that will hold them afterwards: the node's own stacking
270        // context for a root, otherwise its contribution cache (from which
271        // they are copied into the enclosing stacking context below).
272        let mut entries = {
273            let node = &mut self.nodes[node_id];
274            let mut entries = match stacking_context {
275                None => node
276                    .stacking_context
277                    .take()
278                    .map(|sc| sc.children)
279                    .unwrap_or_default(),
280                Some(_) => std::mem::take(&mut *node.sc_contribution_cache.borrow_mut()),
281            };
282            entries.clear();
283            entries
284        };
285
286        let children = self.nodes[node_id].layout_children.borrow_mut().take();
287        if let Some(children) = children {
288            let is_flex_or_grid =
289                matches!(display.inside(), DisplayInside::Flex | DisplayInside::Grid);
290
291            // Out-of-flow boxes are laid out (and painted) relative to their
292            // containing block, as recorded by Takia's out-of-flow pass in the
293            // containing block's `hoisted_children`. A box whose containing
294            // block is not its layout parent is owned by the containing block;
295            // its layout parent neither visits nor lists it.
296            let owned_here = |doc: &Self, child_id: NodeId| -> bool {
297                doc.nodes[child_id]
298                    .oof_containing_block
299                    .get()
300                    .is_none_or(|cb| cb == node_id)
301            };
302            // Boxes hoisted to this node from further down the tree, keyed by
303            // the index of the layout child they descend from: they are visited
304            // and listed in tree order, directly after that child (CSS 2.1
305            // Appendix E). Boxes whose layout-parent chain does not lead through
306            // a layout child of this node come after all of them.
307            let mut hoisted_past_parent: Vec<(usize, NodeId)> = self.nodes[node_id]
308                .hoisted_children
309                .borrow()
310                .iter()
311                .copied()
312                .filter(|&id| {
313                    self.nodes
314                        .get(id)
315                        .is_some_and(|n| n.layout_parent.get() != Some(node_id))
316                })
317                .map(|id| {
318                    let anchor = self
319                        .layout_child_containing(node_id, id)
320                        .and_then(|anchor| children.iter().position(|&c| c == anchor))
321                        .unwrap_or(children.len());
322                    (anchor, id)
323                })
324                .collect();
325            hoisted_past_parent.sort_by_key(|&(anchor, _)| anchor);
326            let mut next_hoisted = 0;
327
328            let mut paint_children = self.nodes[node_id]
329                .paint_children
330                .borrow_mut()
331                .take()
332                .unwrap_or_default();
333            paint_children.clear();
334            paint_children.reserve(children.len() + hoisted_past_parent.len());
335
336            for (index, &child_id) in children.iter().enumerate() {
337                if owned_here(self, child_id) {
338                    let is_sc_root = self.nodes[child_id].is_stacking_context_root(is_flex_or_grid);
339                    self.build_paint_tree_impl(
340                        child_id,
341                        match is_sc_root {
342                            true => None,
343                            false => Some(&mut entries),
344                        },
345                    );
346
347                    // Only stacking contexts with a non-zero z-index are lifted
348                    // out of tree order into the enclosing stacking context's
349                    // z-sorted list (CSS 2.1 Appendix E).
350                    let z_index = self.nodes[child_id].z_index();
351                    if is_sc_root && z_index != 0 {
352                        entries.push(HoistedPaintChild::new(child_id, z_index))
353                    } else {
354                        paint_children.push(child_id);
355                    }
356                }
357
358                while let Some(&(anchor, hoisted_id)) = hoisted_past_parent.get(next_hoisted)
359                    && anchor == index
360                {
361                    self.attach_hoisted_past_parent(hoisted_id, &mut paint_children, &mut entries);
362                    next_hoisted += 1;
363                }
364            }
365            for &(_, hoisted_id) in &hoisted_past_parent[next_hoisted..] {
366                self.attach_hoisted_past_parent(hoisted_id, &mut paint_children, &mut entries);
367            }
368
369            paint_children
370                .sort_by_cached_key(|id| node_to_paint_order(&self.nodes[*id], is_flex_or_grid));
371
372            let node = &self.nodes[node_id];
373            *node.paint_children.borrow_mut() = Some(paint_children);
374            *node.layout_children.borrow_mut() = Some(children);
375        }
376
377        let node = &mut self.nodes[node_id];
378        if let Some(stacking_context) = stacking_context {
379            node.stacking_context = None;
380            stacking_context.extend(entries.iter().cloned());
381            *node.sc_contribution_cache.borrow_mut() = entries;
382        } else {
383            node.sc_contribution_cache.borrow_mut().clear();
384            node.stacking_context = Some(Box::new(StackingContext::from_children(entries)));
385        }
386    }
387
388    /// The layout child of `node_id` whose subtree contains `id`, following
389    /// `layout_parent`.
390    fn layout_child_containing(&self, node_id: NodeId, id: NodeId) -> Option<NodeId> {
391        let mut current = id;
392        loop {
393            let parent = self.nodes.get(current)?.layout_parent.get()?;
394            if parent == node_id {
395                return Some(current);
396            }
397            current = parent;
398        }
399    }
400
401    /// Visit an out-of-flow box hoisted to its containing block past its
402    /// layout parent and place it in the paint lists: z-indexed boxes go to
403    /// the enclosing stacking context like any other z-indexed positioned
404    /// child, the rest join `paint_children` at the same paint level as other
405    /// positioned boxes (the caller's stable sort keeps tree order).
406    fn attach_hoisted_past_parent(
407        &mut self,
408        child_id: NodeId,
409        paint_children: &mut ThinVec<NodeId>,
410        entries: &mut ThinVec<HoistedPaintChild>,
411    ) {
412        self.build_paint_tree_impl(
413            child_id,
414            match self.nodes[child_id].is_stacking_context_root(false) {
415                true => None,
416                false => Some(entries),
417            },
418        );
419
420        let z_index = self.nodes[child_id].z_index();
421        if z_index != 0 {
422            entries.push(HoistedPaintChild::new(child_id, z_index));
423        } else {
424            paint_children.push(child_id);
425        }
426    }
427}
428
429#[inline(always)]
430fn position_to_order(pos: Position) -> i32 {
431    match pos {
432        Position::Static => 0,
433        // All positioned descendants with z-index: auto share one paint
434        // level (CSS 2.1 Appendix E step 8); the stable sort keeps them in
435        // tree order among themselves, above in-flow content and floats.
436        Position::Relative | Position::Sticky | Position::Absolute | Position::Fixed => 2,
437    }
438}
439#[inline(always)]
440fn float_to_order(pos: Float) -> i32 {
441    match pos {
442        Float::None => 0,
443        _ => 1,
444    }
445}
446
447/// Paint sort key: (paint level, order-modified position). Positioned
448/// (z-index: auto) descendants paint above in-flow content (CSS 2.1
449/// Appendix E step 8); within a level the stable sort preserves
450/// (order-modified) document order.
451#[inline(always)]
452fn node_to_paint_order(node: &Node, is_flex_or_grid: bool) -> (i32, i32) {
453    let Some(style) = node.primary_styles() else {
454        return (0, 0);
455    };
456    let position = style.clone_position();
457    if is_flex_or_grid {
458        match position {
459            Position::Static => (0, style.clone_order()),
460            Position::Relative | Position::Sticky => (2, style.clone_order()),
461            // Out-of-flow children are not flex/grid items: `order` does
462            // not apply; tree order does.
463            Position::Absolute | Position::Fixed => (2, 0),
464        }
465    } else {
466        (
467            position_to_order(position) + float_to_order(style.clone_float()),
468            0,
469        )
470    }
471}