Skip to main content

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}