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