Skip to main content

kozan_core/compositor/
mod.rs

1//! Compositor — GPU-thread owner of the layer tree and scroll state.
2//!
3//! Chrome: `cc::LayerTreeHostImpl` on the compositor thread.
4//!
5//! The view thread builds frames (DisplayList + LayerTree) and commits them.
6//! The compositor owns the committed state and produces `CompositorFrame`
7//! for the GPU each vsync. Scroll is handled here — no view thread round-trip.
8//!
9//! # Scroll ownership
10//!
11//! The compositor **exclusively owns** scroll offsets. The view thread never
12//! sends scroll positions to the compositor. Instead:
13//!
14//! - Compositor → view: `ScrollSync(offsets)` after each scroll event
15//! - View thread paints with those offsets (read-only copy)
16//! - Commit sends topology (ScrollTree) but NOT positions (ScrollOffsets)
17//!
18//! This eliminates the stale-offset problem: no matter how many frames the
19//! view thread is behind, the compositor's scroll state is always current.
20
21pub mod frame;
22pub(crate) mod layer;
23pub(crate) mod layer_builder;
24pub mod layer_tree;
25
26use std::sync::Arc;
27
28use kozan_primitives::geometry::{Offset, Point};
29
30use crate::paint::DisplayList;
31use crate::scroll::{ScrollController, ScrollOffsets, ScrollTree};
32
33use self::frame::CompositorFrame;
34use self::layer_tree::LayerTree;
35
36/// Receives committed frames from the view thread and produces
37/// `CompositorFrame` for the GPU.
38///
39/// Owns a copy of the scroll tree + offsets so it can handle wheel
40/// events at vsync rate without waiting for the view thread.
41pub struct Compositor {
42    display_list: Option<Arc<DisplayList>>,
43    layer_tree: Option<LayerTree>,
44    scroll_tree: ScrollTree,
45    scroll_offsets: ScrollOffsets,
46}
47
48impl Compositor {
49    pub fn new() -> Self {
50        Self {
51            display_list: None,
52            layer_tree: None,
53            scroll_tree: ScrollTree::new(),
54            scroll_offsets: ScrollOffsets::new(),
55        }
56    }
57
58    /// Accept a committed frame from the view thread.
59    ///
60    /// Chrome: `LayerTreeHostImpl::FinishCommit()`.
61    ///
62    /// Updates the display list, layer tree, and scroll tree topology.
63    /// Scroll offsets are **not** accepted — the compositor owns them
64    /// exclusively and syncs them to the view thread via `ScrollSync`.
65    pub fn commit(
66        &mut self,
67        display_list: Arc<DisplayList>,
68        layer_tree: LayerTree,
69        scroll_tree: ScrollTree,
70    ) {
71        self.display_list = Some(display_list);
72        self.layer_tree = Some(layer_tree);
73        self.scroll_tree = scroll_tree;
74    }
75
76    /// Handle a scroll event directly — no view thread round-trip.
77    ///
78    /// Chrome: `InputHandlerProxy::RouteToTypeSpecificHandler()`.
79    pub fn try_scroll(&mut self, target: u32, delta: Offset) -> bool {
80        !ScrollController::new(&self.scroll_tree, &mut self.scroll_offsets)
81            .scroll(target, delta)
82            .is_empty()
83    }
84
85    /// Produce a frame for the GPU.
86    ///
87    /// The scroll offsets are passed directly — the renderer uses them
88    /// to override tagged scroll transforms. Zero allocation, O(1) lookup.
89    pub fn produce_frame(&self) -> Option<CompositorFrame> {
90        let display_list = self.display_list.as_ref()?;
91        Some(CompositorFrame {
92            display_list: Arc::clone(display_list),
93            scroll_offsets: self.scroll_offsets.clone(),
94        })
95    }
96
97    /// Current scroll offsets — for syncing back to the view thread.
98    pub fn scroll_offsets(&self) -> &ScrollOffsets {
99        &self.scroll_offsets
100    }
101
102    pub fn scroll_tree(&self) -> &ScrollTree {
103        &self.scroll_tree
104    }
105
106    pub fn has_content(&self) -> bool {
107        self.display_list.is_some()
108    }
109
110    /// Find the deepest scrollable layer at a screen point.
111    ///
112    /// Chrome: `InputHandler::HitTestScrollNode()` — compositor-side hit test
113    /// against the layer tree to determine which scrollable container should
114    /// receive the scroll delta. Returns the DOM node ID of the scroll target.
115    ///
116    /// Walks the layer tree depth-first, checking bounds. Returns the deepest
117    /// scrollable layer whose bounds contain the point. Falls back to
118    /// `root_scroller()` if no scrollable layer is hit.
119    pub fn hit_test_scroll_target(&self, point: Point) -> Option<u32> {
120        let tree = self.layer_tree.as_ref()?;
121        let root = tree.root()?;
122
123        let mut best: Option<u32> = None;
124        self.hit_test_layer(tree, root, point, &mut best);
125
126        best.or_else(|| self.scroll_tree.root_scroller())
127    }
128
129    fn hit_test_layer(
130        &self,
131        tree: &LayerTree,
132        layer_id: layer::LayerId,
133        point: Point,
134        best: &mut Option<u32>,
135    ) {
136        let layer = tree.layer(layer_id);
137
138        if !layer.bounds.contains_point(point) {
139            return;
140        }
141
142        // If this layer is scrollable and has a scroll node, it's a candidate.
143        if layer.is_scrollable {
144            if let Some(dom_id) = layer.dom_node {
145                if self.scroll_tree.contains(dom_id) {
146                    *best = Some(dom_id);
147                }
148            }
149        }
150
151        // Check children — deeper layers override (last match wins).
152        for &child_id in &layer.children {
153            self.hit_test_layer(tree, child_id, point, best);
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::paint::display_list::DisplayListBuilder;
162    use crate::scroll::node::ScrollNode;
163    use kozan_primitives::geometry::Size;
164
165    fn _assert_send<T: Send>() {}
166    fn _assert_sync<T: Sync>() {}
167    #[test]
168    fn send_sync_bounds() {
169        _assert_send::<Compositor>();
170        _assert_send::<CompositorFrame>();
171        _assert_send::<LayerTree>();
172        _assert_sync::<CompositorFrame>();
173    }
174
175    fn empty_display_list() -> Arc<DisplayList> {
176        Arc::new(DisplayListBuilder::new().finish())
177    }
178
179    fn test_scroll_state() -> (ScrollTree, ScrollOffsets) {
180        let mut tree = ScrollTree::new();
181        tree.set(
182            1,
183            ScrollNode {
184                dom_id: 1,
185                parent: None,
186                container: Size::new(800.0, 600.0),
187                content: Size::new(800.0, 2000.0),
188                scrollable_x: false,
189                scrollable_y: true,
190            },
191        );
192        let mut offsets = ScrollOffsets::new();
193        offsets.set_offset(1, Offset::ZERO);
194        (tree, offsets)
195    }
196
197    #[test]
198    fn no_content_before_commit() {
199        let c = Compositor::new();
200        assert!(!c.has_content());
201        assert!(c.produce_frame().is_none());
202    }
203
204    #[test]
205    fn commit_updates_display_list_and_tree() {
206        let mut c = Compositor::new();
207        let dl = empty_display_list();
208        let (tree, _offsets) = test_scroll_state();
209        c.commit(Arc::clone(&dl), LayerTree::new(), tree);
210        assert!(c.has_content());
211        assert!(Arc::ptr_eq(
212            &c.produce_frame().expect("frame").display_list,
213            &dl
214        ));
215    }
216
217    #[test]
218    fn commit_does_not_overwrite_compositor_scroll_offsets() {
219        let mut c = Compositor::new();
220        let (tree, _offsets) = test_scroll_state();
221        c.commit(empty_display_list(), LayerTree::new(), tree);
222
223        // Compositor scrolls to 120px
224        c.try_scroll(1, Offset::new(0.0, 120.0));
225        assert_eq!(c.scroll_offsets().offset(1).dy, 120.0);
226
227        // View thread commits again — compositor's 120px must survive
228        let (tree2, _offsets2) = test_scroll_state();
229        c.commit(empty_display_list(), LayerTree::new(), tree2);
230        assert_eq!(c.scroll_offsets().offset(1).dy, 120.0);
231    }
232
233    #[test]
234    fn try_scroll_updates_offsets() {
235        let mut c = Compositor::new();
236        let (tree, _offsets) = test_scroll_state();
237        c.commit(empty_display_list(), LayerTree::new(), tree);
238
239        assert!(c.try_scroll(1, Offset::new(0.0, 100.0)));
240        assert_eq!(c.scroll_offsets().offset(1).dy, 100.0);
241    }
242
243    #[test]
244    fn try_scroll_clamps() {
245        let mut c = Compositor::new();
246        let (tree, _offsets) = test_scroll_state();
247        c.commit(empty_display_list(), LayerTree::new(), tree);
248        c.try_scroll(1, Offset::new(0.0, 99999.0));
249        assert_eq!(c.scroll_offsets().offset(1).dy, 1400.0);
250    }
251
252    #[test]
253    fn try_scroll_unknown_node() {
254        let mut c = Compositor::new();
255        let (tree, _offsets) = test_scroll_state();
256        c.commit(empty_display_list(), LayerTree::new(), tree);
257        assert!(!c.try_scroll(99, Offset::new(0.0, 100.0)));
258    }
259
260    #[test]
261    fn produce_frame_has_scroll_offsets() {
262        let mut c = Compositor::new();
263        let (tree, _offsets) = test_scroll_state();
264        c.commit(empty_display_list(), LayerTree::new(), tree);
265        c.try_scroll(1, Offset::new(0.0, 200.0));
266
267        let frame = c.produce_frame().expect("frame");
268        assert_eq!(frame.scroll_offsets.offset(1).dy, 200.0);
269    }
270}