Skip to main content

kozan_core/scroll/
tree.rs

1//! Scroll tree — topology of scrollable nodes.
2//!
3//! Chrome: `cc/trees/scroll_tree.h`.
4//! Knows parent-child scroll chain. No offsets, no events, no paint.
5
6use kozan_primitives::arena::Storage;
7use kozan_primitives::geometry::Size;
8
9use crate::layout::fragment::{Fragment, FragmentKind};
10
11use super::node::ScrollNode;
12
13/// Directed graph of scrollable containers, keyed by DOM node index.
14///
15/// The parent chain defines how unconsumed scroll delta bubbles up.
16/// Rebuilt after each layout pass from the fragment tree.
17#[derive(Clone)]
18pub struct ScrollTree {
19    nodes: Storage<ScrollNode>,
20    root: Option<u32>,
21}
22
23impl ScrollTree {
24    pub fn new() -> Self {
25        Self {
26            nodes: Storage::new(),
27            root: None,
28        }
29    }
30
31    pub fn set(&mut self, dom_id: u32, node: ScrollNode) {
32        if node.parent.is_none() {
33            self.root = Some(dom_id);
34        }
35        self.nodes.set(dom_id, node);
36    }
37
38    pub fn get(&self, dom_id: u32) -> Option<&ScrollNode> {
39        self.nodes.get(dom_id)
40    }
41
42    /// Remove all nodes. Called before re-syncing from a new fragment tree.
43    pub fn clear(&mut self) {
44        self.nodes.clear();
45        self.root = None;
46    }
47
48    pub fn contains(&self, dom_id: u32) -> bool {
49        self.nodes.get(dom_id).is_some()
50    }
51
52    /// The root scroller — cached during `set()` / `sync()`. O(1).
53    pub fn root_scroller(&self) -> Option<u32> {
54        self.root
55    }
56
57    /// Iterate the scroll chain from `start` toward the root scroller.
58    pub fn chain(&self, start: u32) -> ScrollChain<'_> {
59        ScrollChain {
60            nodes: &self.nodes,
61            current: Some(start),
62        }
63    }
64
65    /// Rebuild from the fragment tree. Finds all scrollable boxes and
66    /// builds the parent-child scroll chain.
67    ///
68    /// Chrome: `ScrollTree::UpdateScrollTree()` — runs after layout.
69    pub fn sync(&mut self, root: &Fragment) {
70        self.clear();
71        self.sync_recursive(root, None);
72    }
73
74    fn sync_recursive(&mut self, fragment: &Fragment, parent_scroll: Option<u32>) {
75        let FragmentKind::Box(ref data) = fragment.kind else {
76            return;
77        };
78
79        let dom_id = fragment.dom_node;
80        let scrollable_x = data.overflow_x.is_user_scrollable();
81        let scrollable_y = data.overflow_y.is_user_scrollable();
82        let is_scrollable = scrollable_x || scrollable_y;
83
84        // Container = padding box (fragment size minus borders).
85        let container = Size::new(
86            (fragment.size.width - data.border.left - data.border.right).max(0.0),
87            (fragment.size.height - data.border.top - data.border.bottom).max(0.0),
88        );
89
90        let next_parent = if is_scrollable {
91            if let Some(id) = dom_id {
92                // scrollable_overflow is relative to the border box origin,
93                // but max_offset = content - container uses the padding box.
94                // Shift to padding-box-relative so the math is consistent.
95                let content = Size::new(
96                    (data.scrollable_overflow.width - data.border.left).max(0.0),
97                    (data.scrollable_overflow.height - data.border.top).max(0.0),
98                );
99                self.set(
100                    id,
101                    ScrollNode {
102                        dom_id: id,
103                        parent: parent_scroll,
104                        container,
105                        content,
106                        scrollable_x,
107                        scrollable_y,
108                    },
109                );
110                Some(id)
111            } else {
112                parent_scroll
113            }
114        } else {
115            parent_scroll
116        };
117
118        for child in &data.children {
119            self.sync_recursive(&child.fragment, next_parent);
120        }
121    }
122}
123
124/// Iterator over the ancestor scroll chain.
125pub struct ScrollChain<'a> {
126    nodes: &'a Storage<ScrollNode>,
127    current: Option<u32>,
128}
129
130impl Iterator for ScrollChain<'_> {
131    type Item = u32;
132
133    fn next(&mut self) -> Option<u32> {
134        let id = self.current?;
135        let node = self.nodes.get(id)?;
136        self.current = node.parent;
137        Some(id)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use kozan_primitives::geometry::Size;
145
146    fn scrollable(id: u32, parent: Option<u32>) -> ScrollNode {
147        ScrollNode {
148            dom_id: id,
149            parent,
150            container: Size::new(200.0, 400.0),
151            content: Size::new(200.0, 1200.0),
152            scrollable_x: false,
153            scrollable_y: true,
154        }
155    }
156
157    #[test]
158    fn chain_walks_to_root() {
159        let mut tree = ScrollTree::new();
160        tree.set(1, scrollable(1, None));
161        tree.set(5, scrollable(5, Some(1)));
162        tree.set(9, scrollable(9, Some(5)));
163
164        let chain: Vec<u32> = tree.chain(9).collect();
165        assert_eq!(chain, vec![9, 5, 1]);
166    }
167
168    #[test]
169    fn chain_single_node() {
170        let mut tree = ScrollTree::new();
171        tree.set(1, scrollable(1, None));
172
173        let chain: Vec<u32> = tree.chain(1).collect();
174        assert_eq!(chain, vec![1]);
175    }
176
177    #[test]
178    fn root_scroller_returns_parentless_node() {
179        let mut tree = ScrollTree::new();
180        tree.set(1, scrollable(1, None));
181        tree.set(5, scrollable(5, Some(1)));
182        assert_eq!(tree.root_scroller(), Some(1));
183    }
184
185    #[test]
186    fn root_scroller_empty_tree() {
187        let tree = ScrollTree::new();
188        assert_eq!(tree.root_scroller(), None);
189    }
190
191    #[test]
192    fn chain_unknown_start_is_empty() {
193        let tree = ScrollTree::new();
194        let chain: Vec<u32> = tree.chain(99).collect();
195        assert!(chain.is_empty());
196    }
197}