Skip to main content

orinium_browser/engine/renderer_model/
draw_command.rs

1//! Draw command model: the rendering instructions produced from layout.
2//!
3//! The geometry primitives live in [`crate::engine::renderer_model::geom`],
4//! paths in [`crate::engine::renderer_model::path`], and the layout → command
5//! generation in [`crate::engine::renderer_model::box_model`].
6
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use anyhow::{Context, Result};
11use smol_str::SmolStr;
12
13use crate::engine::layouter::types::{Color, Gradient, TextFlowStyle, TextStyle};
14use crate::engine::renderer_model::geom::{AffineTransform, Rect};
15use crate::engine::renderer_model::path::Path;
16
17/// Fill rule for path filling.
18///
19/// The GPU rasterizer currently only uses this to select the winding mode of
20/// the ear-clipping triangulation; a fully correct stencil-based fill for
21/// arbitrary self-intersecting paths is not implemented.
22#[derive(Debug, Clone, Copy)]
23pub enum FillRule {
24    NonZero,
25    EvenOdd,
26}
27
28/// A fill source: a solid color or a gradient.
29#[derive(Debug, Clone)]
30pub enum Brush {
31    Solid(Color),
32    Gradient(Gradient),
33    /// A decoded RGBA image sampled across the fill path's bounds.
34    Image(Image),
35}
36
37/// Decoded image pixels shared between the engine and platform renderer.
38#[derive(Debug, Clone, PartialEq)]
39pub struct Image {
40    id: u64,
41    width: u32,
42    height: u32,
43    rgba: Arc<[u8]>,
44}
45
46impl Image {
47    /// Decodes encoded image bytes into RGBA8 pixels.
48    ///
49    /// Returns an error when the bytes are not a supported image format.
50    pub fn decode(bytes: &[u8]) -> Result<Self> {
51        if let Ok(decoded) = image::load_from_memory(bytes) {
52            let rgba = decoded.to_rgba8();
53            let (width, height) = rgba.dimensions();
54            return Self::from_rgba(width, height, rgba.into_raw());
55        }
56
57        Self::decode_svg(bytes).context("failed to decode image")
58    }
59
60    fn decode_svg(bytes: &[u8]) -> Result<Self> {
61        let options = resvg::usvg::Options::default();
62        let tree = resvg::usvg::Tree::from_data(bytes, &options).context("invalid SVG image")?;
63        let size = tree.size();
64        let width = size.width().ceil().max(1.0) as u32;
65        let height = size.height().ceil().max(1.0) as u32;
66        let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height)
67            .context("failed to allocate SVG image pixels")?;
68        let transform = resvg::tiny_skia::Transform::from_scale(
69            width as f32 / size.width(),
70            height as f32 / size.height(),
71        );
72        resvg::render(&tree, transform, &mut pixmap.as_mut());
73
74        // tiny-skia returns premultiplied RGBA; the renderer samples
75        // straight-alpha pixels.
76        let mut rgba = pixmap.data().to_vec();
77        for pixel in rgba.as_chunks_mut::<4>().0 {
78            let alpha = pixel[3] as u16;
79            if alpha == 0 {
80                pixel[0] = 0;
81                pixel[1] = 0;
82                pixel[2] = 0;
83            } else if alpha < 255 {
84                pixel[0] = ((pixel[0] as u16 * 255 + alpha / 2) / alpha).min(255) as u8;
85                pixel[1] = ((pixel[1] as u16 * 255 + alpha / 2) / alpha).min(255) as u8;
86                pixel[2] = ((pixel[2] as u16 * 255 + alpha / 2) / alpha).min(255) as u8;
87            }
88        }
89        Self::from_rgba(width, height, rgba)
90    }
91
92    /// Creates an image from decoded RGBA8 pixels.
93    ///
94    /// Returns an error when the byte length does not match the dimensions.
95    pub fn from_rgba(width: u32, height: u32, rgba: Vec<u8>) -> Result<Self> {
96        static NEXT_IMAGE_ID: AtomicU64 = AtomicU64::new(1);
97
98        let expected_len = (width as usize)
99            .checked_mul(height as usize)
100            .and_then(|pixels| pixels.checked_mul(4))
101            .context("image dimensions exceed addressable memory")?;
102        if rgba.len() != expected_len {
103            anyhow::bail!(
104                "invalid RGBA byte length: expected {expected_len}, got {}",
105                rgba.len()
106            );
107        }
108        Ok(Self {
109            id: NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed),
110            width,
111            height,
112            rgba: Arc::from(rgba),
113        })
114    }
115
116    /// Returns the renderer-unique image identifier.
117    pub fn id(&self) -> u64 {
118        self.id
119    }
120
121    /// Returns the decoded image width in pixels.
122    pub fn width(&self) -> u32 {
123        self.width
124    }
125
126    /// Returns the decoded image height in pixels.
127    pub fn height(&self) -> u32 {
128        self.height
129    }
130
131    /// Returns the decoded RGBA8 pixel bytes.
132    pub fn rgba(&self) -> &[u8] {
133        &self.rgba
134    }
135}
136
137#[cfg(test)]
138mod image_tests {
139    use super::Image;
140
141    #[test]
142    fn image_decode_rasterizes_svg_assets() {
143        let image = Image::decode(
144            br##"<svg xmlns="http://www.w3.org/2000/svg" width="12" height="8">
145                <rect width="12" height="8" fill="#ff0000"/>
146            </svg>"##,
147        )
148        .expect("SVG decodes");
149        assert_eq!(image.width(), 12);
150        assert_eq!(image.height(), 8);
151        assert_eq!(&image.rgba()[0..4], &[255, 0, 0, 255]);
152    }
153}
154
155/// How a path is painted: the brush plus an opacity multiplier.
156#[derive(Debug, Clone)]
157pub struct Paint {
158    pub brush: Brush,
159    pub opacity: f32,
160}
161
162/// A drawing instruction for the GPU renderer.
163#[derive(Debug, Clone)]
164pub enum DrawCommand {
165    /// Fill a path.
166    ///
167    /// The `opacity` is applied to solid color fills.
168    Fill {
169        path: Path,
170        paint: Paint,
171        rule: FillRule,
172    },
173    /// Draw a text run at `(x, y)`.
174    DrawText {
175        x: f32,
176        y: f32,
177        text: SmolStr,
178        style: TextStyle,
179        flow_style: TextFlowStyle,
180    },
181    /// Push a clip region given by a path.
182    ///
183    /// Non-rectangular paths are approximated by their bounding box.
184    PushClip {
185        path: Path,
186        rule: FillRule,
187    },
188    PopClip,
189    /// Push a coordinate transform.
190    PushTransform {
191        transform: AffineTransform,
192    },
193    PopTransform,
194
195    /// Delegate rendering to a platform-native system UI element.
196    ///
197    /// The renderer composites or renders the element identified by
198    /// [`SystemUiKind`] at the given rectangle within the current
199    /// coordinate space.
200    SystemUi {
201        kind: SystemUiKind,
202        rect: Rect,
203    },
204}
205
206/// Discriminator for [`DrawCommand::SystemUi`].
207/// Stub
208#[derive(Debug, Clone)]
209pub enum SystemUiKind {
210    /// Composite an external surface (WebView, iframe, …).
211    WebView { surface_id: usize },
212    /// Render a platform-native input widget.
213    Input {
214        value: SmolStr,
215        placeholder: SmolStr,
216    },
217}