Skip to main content

kozan_core/paint/
display_list.rs

1//! Display list — the ordered list of display items for one frame.
2//!
3//! Chrome equivalent: `PaintArtifact` = `DisplayItemList` + `PaintChunk`s.
4//!
5//! The display list is the output of the paint phase and the input
6//! to the renderer backend (vello, wgpu, etc.).
7//!
8//! # Structure
9//!
10//! ```text
11//! DisplayList
12//!   ├── items: Vec<DisplayItem>     (all items in paint order)
13//!   └── chunks: Vec<PaintChunk>     (groups of items sharing PropertyState)
14//! ```
15//!
16//! # `PaintChunk`
17//!
18//! Adjacent display items sharing the same `PropertyState` (same transform,
19//! clip, effect) are grouped into a `PaintChunk`. The compositor uses chunks
20//! to decide GPU layer boundaries.
21
22use super::display_item::DisplayItem;
23use super::property_state::PropertyState;
24
25/// A paint chunk — a group of display items sharing the same property state.
26///
27/// Chrome equivalent: `PaintChunk`.
28/// The compositor uses chunks to determine GPU layer boundaries.
29#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub struct PaintChunk {
32    /// The property state shared by all items in this chunk.
33    pub state: PropertyState,
34    /// Start index in the display list's items array (inclusive).
35    pub start: usize,
36    /// End index in the display list's items array (exclusive).
37    pub end: usize,
38}
39
40impl PaintChunk {
41    /// Number of display items in this chunk.
42    #[must_use]
43    pub fn len(&self) -> usize {
44        self.end - self.start
45    }
46
47    /// Whether this chunk is empty.
48    #[must_use]
49    pub fn is_empty(&self) -> bool {
50        self.start == self.end
51    }
52}
53
54/// The display list — all draw commands for one paint pass.
55///
56/// Chrome equivalent: `PaintArtifact` (items + chunks).
57///
58/// Built by the painter, consumed by the renderer backend.
59#[derive(Debug, Clone, Default)]
60pub struct DisplayList {
61    /// All display items in paint order.
62    items: Vec<DisplayItem>,
63    /// Paint chunks — groups of items sharing the same `PropertyState`.
64    chunks: Vec<PaintChunk>,
65}
66
67impl DisplayList {
68    /// Create a new empty display list.
69    #[must_use]
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Start building a new display list.
75    #[must_use]
76    pub fn builder() -> DisplayListBuilder {
77        DisplayListBuilder::new()
78    }
79
80    /// Total number of display items.
81    #[must_use]
82    pub fn len(&self) -> usize {
83        self.items.len()
84    }
85
86    /// Whether the display list is empty.
87    #[must_use]
88    pub fn is_empty(&self) -> bool {
89        self.items.is_empty()
90    }
91
92    /// Get all display items in paint order.
93    #[must_use]
94    pub fn items(&self) -> &[DisplayItem] {
95        &self.items
96    }
97
98    /// Get all paint chunks.
99    #[must_use]
100    pub fn chunks(&self) -> &[PaintChunk] {
101        &self.chunks
102    }
103
104    /// Iterate over display items.
105    pub fn iter(&self) -> impl Iterator<Item = &DisplayItem> {
106        self.items.iter()
107    }
108
109    /// Get items for a specific chunk.
110    #[must_use]
111    pub fn chunk_items(&self, chunk: &PaintChunk) -> &[DisplayItem] {
112        &self.items[chunk.start..chunk.end]
113    }
114}
115
116/// Builder for constructing a display list incrementally.
117///
118/// Chrome equivalent: `PaintController` (the state machine that
119/// tracks current property state and builds chunks).
120///
121/// # Usage
122///
123/// ```ignore
124/// let mut builder = DisplayList::builder();
125/// builder.push(DisplayItem::Draw(DrawCommand::Rect { ... }));
126/// builder.push_state(new_property_state);
127/// builder.push(DisplayItem::Draw(DrawCommand::Text { ... }));
128/// let display_list = builder.finish();
129/// ```
130pub struct DisplayListBuilder {
131    items: Vec<DisplayItem>,
132    chunks: Vec<PaintChunk>,
133    current_state: PropertyState,
134    current_chunk_start: usize,
135}
136
137impl DisplayListBuilder {
138    /// Create a new builder with root property state.
139    #[must_use]
140    pub fn new() -> Self {
141        Self {
142            items: Vec::new(),
143            chunks: Vec::new(),
144            current_state: PropertyState::root(),
145            current_chunk_start: 0,
146        }
147    }
148
149    /// Push a display item into the current chunk.
150    pub fn push(&mut self, item: DisplayItem) {
151        self.items.push(item);
152    }
153
154    /// Change the property state, starting a new chunk if different.
155    ///
156    /// Chrome equivalent: `PaintController::UpdateCurrentPaintChunkProperties()`.
157    pub fn set_state(&mut self, state: PropertyState) {
158        if state != self.current_state {
159            self.finish_chunk();
160            self.current_state = state;
161            self.current_chunk_start = self.items.len();
162        }
163    }
164
165    /// Finish building and return the display list.
166    #[must_use]
167    pub fn finish(mut self) -> DisplayList {
168        self.finish_chunk();
169        DisplayList {
170            items: self.items,
171            chunks: self.chunks,
172        }
173    }
174
175    /// Close the current chunk (if non-empty).
176    fn finish_chunk(&mut self) {
177        let end = self.items.len();
178        if end > self.current_chunk_start {
179            self.chunks.push(PaintChunk {
180                state: self.current_state.clone(),
181                start: self.current_chunk_start,
182                end,
183            });
184        }
185    }
186}
187
188impl Default for DisplayListBuilder {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::paint::display_item::{ClipData, DrawCommand};
198    use kozan_primitives::color::Color;
199    use kozan_primitives::geometry::Rect;
200
201    #[test]
202    fn empty_display_list() {
203        let list = DisplayList::builder().finish();
204        assert!(list.is_empty());
205        assert_eq!(list.len(), 0);
206        assert!(list.chunks().is_empty());
207    }
208
209    #[test]
210    fn single_item_single_chunk() {
211        let mut builder = DisplayList::builder();
212        builder.push(DisplayItem::Draw(DrawCommand::Rect {
213            rect: Rect::new(0.0, 0.0, 100.0, 50.0),
214            color: Color::RED,
215        }));
216        let list = builder.finish();
217
218        assert_eq!(list.len(), 1);
219        assert_eq!(list.chunks().len(), 1);
220        assert_eq!(list.chunks()[0].start, 0);
221        assert_eq!(list.chunks()[0].end, 1);
222    }
223
224    #[test]
225    fn same_state_groups_into_one_chunk() {
226        let mut builder = DisplayList::builder();
227        builder.push(DisplayItem::Draw(DrawCommand::Rect {
228            rect: Rect::new(0.0, 0.0, 100.0, 50.0),
229            color: Color::RED,
230        }));
231        builder.push(DisplayItem::Draw(DrawCommand::Rect {
232            rect: Rect::new(0.0, 50.0, 100.0, 50.0),
233            color: Color::BLUE,
234        }));
235        let list = builder.finish();
236
237        // Same property state → one chunk with 2 items.
238        assert_eq!(list.len(), 2);
239        assert_eq!(list.chunks().len(), 1);
240    }
241
242    #[test]
243    fn different_state_creates_new_chunk() {
244        let mut builder = DisplayList::builder();
245        builder.push(DisplayItem::Draw(DrawCommand::Rect {
246            rect: Rect::new(0.0, 0.0, 100.0, 50.0),
247            color: Color::RED,
248        }));
249
250        // Change state — new chunk starts.
251        builder.set_state(PropertyState {
252            opacity: 0.5,
253            ..PropertyState::default()
254        });
255
256        builder.push(DisplayItem::Draw(DrawCommand::Rect {
257            rect: Rect::new(0.0, 50.0, 100.0, 50.0),
258            color: Color::BLUE,
259        }));
260
261        let list = builder.finish();
262
263        assert_eq!(list.len(), 2);
264        assert_eq!(list.chunks().len(), 2);
265        assert_eq!(list.chunks()[0].state.opacity, 1.0);
266        assert_eq!(list.chunks()[1].state.opacity, 0.5);
267    }
268
269    #[test]
270    fn chunk_items_accessor() {
271        let mut builder = DisplayList::builder();
272        builder.push(DisplayItem::Draw(DrawCommand::Rect {
273            rect: Rect::new(0.0, 0.0, 100.0, 50.0),
274            color: Color::RED,
275        }));
276        builder.push(DisplayItem::PushClip(ClipData {
277            rect: Rect::new(0.0, 0.0, 50.0, 50.0),
278        }));
279        let list = builder.finish();
280
281        let chunk = &list.chunks()[0];
282        let items = list.chunk_items(chunk);
283        assert_eq!(items.len(), 2);
284    }
285
286    #[test]
287    fn empty_state_change_no_empty_chunk() {
288        let mut builder = DisplayList::builder();
289
290        // Change state without pushing items — no empty chunk.
291        builder.set_state(PropertyState {
292            opacity: 0.5,
293            ..PropertyState::default()
294        });
295
296        builder.push(DisplayItem::Draw(DrawCommand::Rect {
297            rect: Rect::new(0.0, 0.0, 100.0, 50.0),
298            color: Color::RED,
299        }));
300
301        let list = builder.finish();
302
303        // Only one chunk (the empty first chunk was skipped).
304        assert_eq!(list.chunks().len(), 1);
305        assert_eq!(list.chunks()[0].state.opacity, 0.5);
306    }
307}