kozan_core/paint/display_item.rs
1//! Display items — the individual draw commands in a display list.
2//!
3//! Chrome equivalent: `DisplayItem` + `PaintOp` (cc/paint).
4//!
5//! Each display item represents one visual operation: draw a rect,
6//! draw text, draw an image, clip, transform, etc.
7//!
8//! # Chrome mapping
9//!
10//! | Chrome | Kozan |
11//! |--------|-------|
12//! | `DrawingDisplayItem` | `DisplayItem::Draw(DrawCommand)` |
13//! | `PaintOp::DrawRectOp` | `DrawCommand::Rect` |
14//! | `PaintOp::DrawTextBlobOp` | `DrawCommand::Text` |
15//! | `PaintOp::DrawImageRectOp` | `DrawCommand::Image` |
16//! | `ClipRectOp` | `DisplayItem::PushClip` |
17//! | `SaveLayerAlphaOp` | `DisplayItem::PushOpacity` |
18
19use kozan_primitives::color::Color;
20use kozan_primitives::geometry::Rect;
21
22/// How image content should be sized within its destination rectangle.
23///
24/// Chrome equivalent: `EObjectFit` from `ComputedStyleConstants.h`.
25/// Mirrors CSS `object-fit` property values.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum ObjectFit {
28 #[default]
29 Fill,
30 Contain,
31 Cover,
32 None,
33 ScaleDown,
34}
35
36/// A single display item in the paint list.
37///
38/// Chrome equivalent: `DisplayItem` (blink) + `PaintOp` (cc/paint).
39///
40/// Display items are produced by the painter walking the fragment tree.
41/// They are consumed by the renderer backend (vello, wgpu, etc.).
42#[derive(Debug, Clone)]
43pub enum DisplayItem {
44 /// A concrete draw operation.
45 /// Chrome: `DrawingDisplayItem` wrapping a `PaintRecord`.
46 Draw(DrawCommand),
47
48 /// Push a clip rectangle onto the clip stack.
49 /// Everything drawn until the matching `PopClip` is clipped to this rect.
50 /// Chrome: `ClipRectOp`.
51 PushClip(ClipData),
52
53 /// Pop the most recent clip from the stack.
54 PopClip,
55
56 /// Push a rounded clip onto the clip stack.
57 /// Chrome: `ClipRRectOp`.
58 PushRoundedClip(RoundedClipData),
59
60 /// Pop the most recent rounded clip from the stack.
61 PopRoundedClip,
62
63 /// Push an opacity layer.
64 /// Chrome: `SaveLayerAlphaOp`.
65 PushOpacity(f32),
66
67 /// Pop the opacity layer.
68 PopOpacity,
69
70 /// Push a 2D transform.
71 /// Chrome: `ConcatOp(matrix)`.
72 PushTransform(TransformData),
73
74 /// Pop the most recent transform.
75 PopTransform,
76
77 /// Composite an external GPU surface into the display list.
78 ///
79 /// Chrome equivalent: `ForeignLayerDisplayItem`.
80 /// This is how 3D content (wgpu scenes), video frames, and other GPU
81 /// textures are integrated into the 2D rendering pipeline.
82 ///
83 /// The compositor blends this surface with the surrounding 2D content,
84 /// respecting clipping, opacity, and z-ordering from the display list.
85 ///
86 /// # 2D/3D Unification
87 ///
88 /// A `<canvas>` element with a wgpu 3D scene produces this item.
89 /// The renderer composites it into the final frame alongside vello's
90 /// 2D output — enabling game viewports inside UI panels with correct
91 /// clipping, layering, and event handling.
92 ExternalSurface(ExternalSurfaceData),
93}
94
95/// A concrete draw command — the actual pixels to put on screen.
96///
97/// Chrome equivalent: the `PaintOp` variants that draw things
98/// (`DrawRect`, `DrawRRect`, `DrawTextBlob`, `DrawImageRect`, etc.).
99#[derive(Debug, Clone)]
100pub enum DrawCommand {
101 /// Fill a rectangle with a solid color.
102 /// Chrome: `PaintOp::DrawRectOp` + `PaintFlags::kFill`.
103 Rect { rect: Rect, color: Color },
104
105 /// Fill a rounded rectangle with a solid color.
106 /// Chrome: `PaintOp::DrawRRectOp`.
107 RoundedRect {
108 rect: Rect,
109 radii: BorderRadii,
110 color: Color,
111 },
112
113 /// Draw a rounded border ring (outer rounded rect minus inner rounded rect).
114 /// Chrome: `PaintOp::DrawDRRectOp` — draws the difference between two rounded rects.
115 /// Used for borders on elements with border-radius. Fills ONLY the ring area,
116 /// not the interior — so rgba backgrounds behind it show correctly.
117 RoundedBorderRing {
118 outer_rect: Rect,
119 outer_radii: BorderRadii,
120 inner_rect: Rect,
121 inner_radii: BorderRadii,
122 color: Color,
123 },
124
125 /// Draw a border (four edges, each with its own width, color, and style).
126 /// Chrome: `PaintOp::DrawDRRectOp` or individual edge drawing.
127 /// The `styles` field tells the renderer to draw solid/dashed/dotted/etc.
128 Border {
129 rect: Rect,
130 widths: BorderWidths,
131 colors: BorderColors,
132 styles: BorderStyles,
133 },
134
135 /// Draw pre-shaped glyph runs at a position.
136 /// Chrome: `PaintOp::DrawTextBlobOp` — carries a `SkTextBlob` (pre-shaped).
137 /// Glyphs shaped ONCE during layout (Parley + `HarfRust`).
138 /// Renderer just draws them — ZERO font logic in the GPU layer.
139 Text {
140 /// Position (x, y) — top-left of the text box.
141 x: f32,
142 y: f32,
143 /// Pre-shaped glyph runs from layout.
144 /// Each run has: font data, font size, glyph IDs + positions, color.
145 runs: Vec<crate::layout::inline::font_system::ShapedTextRun>,
146 },
147
148 /// Draw an image in a destination rectangle.
149 /// Chrome: `PaintOp::DrawImageRectOp`.
150 Image {
151 /// Source image identifier (future: image resource handle).
152 source_id: u64,
153 /// Destination rectangle in layout coordinates.
154 dest: Rect,
155 /// How the image content should be sized within the destination rect.
156 /// Chrome: `ComputedStyle::GetObjectFit()` read during paint.
157 // TODO: object-fit from Stylo (style::computed_values::object_fit::T)
158 // Placeholder until image rendering is implemented.
159 _object_fit: u8,
160 },
161
162 /// Draw a line (for underlines, strikethroughs, hr, etc.).
163 /// Chrome: `PaintOp::DrawLineOp`.
164 Line {
165 x0: f32,
166 y0: f32,
167 x1: f32,
168 y1: f32,
169 width: f32,
170 color: Color,
171 },
172
173 /// Draw an outline (like border but outside the box, doesn't affect layout).
174 /// Chrome: `PaintOp::DrawDRRectOp` for the outline ring.
175 /// CSS: `outline: 2px solid blue;` — painted outside the border-box.
176 Outline {
177 /// The border-box rect (outline is drawn OUTSIDE this).
178 rect: Rect,
179 /// Border radii (outline follows the element's border-radius).
180 radii: BorderRadii,
181 /// Outline width in pixels.
182 width: f32,
183 /// Outline offset (CSS `outline-offset`, can be negative).
184 offset: f32,
185 /// Outline color.
186 color: Color,
187 },
188
189 // TODO(M7): LinearGradient { rect, stops, angle } — Chrome: PaintOp::DrawPaintOp + cc::PaintShader::MakeLinearGradient.
190 // TODO(M7): RadialGradient { rect, stops, center, radius } — Chrome: PaintOp::DrawPaintOp + cc::PaintShader::MakeRadialGradient.
191 // TODO(M7): TextShadow { x, y, runs, offset_x, offset_y, blur, color } — Chrome: TextPainter::PaintTextWithShadows().
192 /// Draw a box shadow.
193 /// Chrome: painted via `PaintOp::DrawRRectOp` with blur filter.
194 BoxShadow {
195 /// The element's border box.
196 rect: Rect,
197 /// Horizontal offset.
198 offset_x: f32,
199 /// Vertical offset.
200 offset_y: f32,
201 /// Blur radius.
202 blur: f32,
203 /// Spread radius.
204 spread: f32,
205 /// Shadow color.
206 color: Color,
207 },
208}
209
210/// Data for compositing an external GPU surface.
211///
212/// Chrome equivalent: `ForeignLayerDisplayItem` + `cc::SurfaceLayer`.
213///
214/// The `surface_id` is an opaque handle to a GPU texture/surface
215/// owned by external code (3D game engine, video decoder, etc.).
216/// The renderer uses this ID to look up the actual GPU resource
217/// and composite it at the specified rectangle.
218#[derive(Debug, Clone, Copy)]
219pub struct ExternalSurfaceData {
220 /// Opaque identifier for the GPU surface.
221 /// The platform layer resolves this to an actual GPU texture.
222 pub surface_id: u64,
223 /// Rectangle where the surface should be composited (layout coordinates).
224 pub dest: Rect,
225}
226
227/// Clip data for `PushClip`.
228#[derive(Debug, Clone, Copy)]
229pub struct ClipData {
230 pub rect: Rect,
231}
232
233/// Rounded clip data for `PushRoundedClip`.
234#[derive(Debug, Clone, Copy)]
235pub struct RoundedClipData {
236 pub rect: Rect,
237 pub radii: BorderRadii,
238}
239
240/// 2D transform data for `PushTransform`.
241#[derive(Debug, Clone, Copy)]
242pub struct TransformData {
243 /// Translation in X.
244 pub translate_x: f32,
245 /// Translation in Y.
246 pub translate_y: f32,
247 /// If this transform is a scroll translate, the DOM node that owns it.
248 /// The compositor can override this with its own offset without repainting.
249 pub scroll_node: Option<u32>,
250}
251
252/// Border corner radii (all four corners).
253#[derive(Debug, Clone, Copy, Default)]
254pub struct BorderRadii {
255 pub top_left: f32,
256 pub top_right: f32,
257 pub bottom_right: f32,
258 pub bottom_left: f32,
259}
260
261/// Border widths (all four edges).
262#[derive(Debug, Clone, Copy, Default)]
263pub struct BorderWidths {
264 pub top: f32,
265 pub right: f32,
266 pub bottom: f32,
267 pub left: f32,
268}
269
270/// Border colors (all four edges).
271#[derive(Debug, Clone, Copy)]
272pub struct BorderColors {
273 pub top: Color,
274 pub right: Color,
275 pub bottom: Color,
276 pub left: Color,
277}
278
279/// Border styles (all four edges).
280#[derive(Debug, Clone, Copy)]
281pub struct BorderStyles {
282 pub top: style::values::specified::border::BorderStyle,
283 pub right: style::values::specified::border::BorderStyle,
284 pub bottom: style::values::specified::border::BorderStyle,
285 pub left: style::values::specified::border::BorderStyle,
286}
287
288impl Default for BorderStyles {
289 fn default() -> Self {
290 Self {
291 top: style::values::specified::border::BorderStyle::None,
292 right: style::values::specified::border::BorderStyle::None,
293 bottom: style::values::specified::border::BorderStyle::None,
294 left: style::values::specified::border::BorderStyle::None,
295 }
296 }
297}
298
299impl Default for BorderColors {
300 fn default() -> Self {
301 Self {
302 top: Color::BLACK,
303 right: Color::BLACK,
304 bottom: Color::BLACK,
305 left: Color::BLACK,
306 }
307 }
308}
309
310impl DisplayItem {
311 /// Whether this item is a draw command (produces pixels).
312 #[must_use]
313 pub fn is_draw(&self) -> bool {
314 matches!(self, DisplayItem::Draw(_))
315 }
316
317 /// Whether this item is a push operation (needs a matching pop).
318 #[must_use]
319 pub fn is_push(&self) -> bool {
320 matches!(
321 self,
322 DisplayItem::PushClip(_)
323 | DisplayItem::PushRoundedClip(_)
324 | DisplayItem::PushOpacity(_)
325 | DisplayItem::PushTransform(_)
326 )
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn display_item_classification() {
336 let rect = DisplayItem::Draw(DrawCommand::Rect {
337 rect: Rect::new(0.0, 0.0, 100.0, 50.0),
338 color: Color::RED,
339 });
340 assert!(rect.is_draw());
341 assert!(!rect.is_push());
342
343 let clip = DisplayItem::PushClip(ClipData {
344 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
345 });
346 assert!(!clip.is_draw());
347 assert!(clip.is_push());
348 }
349
350 #[test]
351 fn border_radii_default() {
352 let radii = BorderRadii::default();
353 assert_eq!(radii.top_left, 0.0);
354 assert_eq!(radii.bottom_right, 0.0);
355 }
356
357 #[test]
358 fn border_colors_default() {
359 let colors = BorderColors::default();
360 assert_eq!(colors.top, Color::BLACK);
361 }
362}