Skip to main content

fission_render/
lib.rs

1//! Display list and rendering abstraction for the Fission UI framework.
2//!
3//! After the widget tree has been compiled to an IR and the layout engine has
4//! positioned every node, the framework flattens the result into a [`DisplayList`]
5//! -- an ordered sequence of [`DisplayOp`] commands that describe exactly what to
6//! draw and in what order.
7//!
8//! Platform backends implement the [`Renderer`] trait to consume a display list and
9//! produce pixels using whatever GPU or software rasterizer is available.
10//!
11//! # Pipeline
12//!
13//! ```text
14//! CoreIR  -->  LayoutSnapshot  -->  DisplayList  -->  Renderer (Metal, Vulkan, ...)
15//! ```
16//!
17//! # Graphics state model
18//!
19//! The display list uses a save/restore stack model (like HTML Canvas or
20//! CoreGraphics). [`DisplayOp::Save`] pushes the current clip and transform onto a
21//! stack, and [`DisplayOp::Restore`] pops it. Drawing operations between a
22//! save/restore pair are affected by the clips and transforms set within that scope.
23
24use fission_ir::NodeId;
25pub use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutUnit};
26use serde::{Deserialize, Serialize};
27
28/// An RGBA color with 8-bit channels.
29///
30/// This is the render-side mirror of [`fission_ir::op::Color`]. A separate type is
31/// defined here so that `fission-render` can be used without pulling in the full IR
32/// crate's type system.
33#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
34pub struct Color {
35    /// Red channel (0-255).
36    pub r: u8,
37    /// Green channel (0-255).
38    pub g: u8,
39    /// Blue channel (0-255).
40    pub b: u8,
41    /// Alpha channel (0 = fully transparent, 255 = fully opaque).
42    pub a: u8,
43}
44
45/// A solid color fill for shapes.
46#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
47pub struct Fill {
48    /// The fill color.
49    pub color: Color,
50}
51
52/// A colored stroke (outline) with a line width.
53#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
54pub struct Stroke {
55    /// The stroke color.
56    pub color: Color,
57    /// The stroke width in logical pixels.
58    pub width: LayoutUnit,
59}
60
61/// A drop shadow rendered behind a rectangle.
62#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
63pub struct BoxShadow {
64    /// The shadow color (typically semi-transparent black).
65    pub color: Color,
66    /// The Gaussian blur radius in logical pixels.
67    pub blur_radius: LayoutUnit,
68    /// The horizontal and vertical offset: `(dx, dy)`.
69    pub offset: (LayoutUnit, LayoutUnit),
70}
71
72/// How an image scales to fit its layout box.
73///
74/// Equivalent to the CSS `object-fit` property.
75#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
76pub enum ImageFit {
77    /// Scale uniformly to fit inside the box (may letter-box).
78    Contain,
79    /// Scale uniformly to cover the entire box (may clip).
80    Cover,
81    /// Stretch to fill exactly (ignores aspect ratio).
82    Fill,
83    /// Display at natural size, no scaling.
84    None,
85}
86
87/// Styling properties for a run of text.
88///
89/// Controls font size, color, underline, and optional background highlight
90/// (used for search matches, selections, etc.).
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct TextStyle {
93    /// Font size in logical pixels.
94    pub font_size: LayoutUnit,
95    /// Text foreground color.
96    pub color: Color,
97    /// Whether to draw an underline beneath the text.
98    pub underline: bool,
99    /// Optional background highlight color for this run.
100    pub background_color: Option<Color>,
101}
102
103/// A contiguous run of text with a uniform style.
104///
105/// Rich text is represented as a `Vec<TextRun>`. Each run carries its own
106/// [`TextStyle`], allowing mixed colors, sizes, and underlines within a single
107/// text block.
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct TextRun {
110    /// The text content of this run.
111    pub text: String,
112    /// The style applied to this run.
113    pub style: TextStyle,
114}
115
116/// A single rendering command in a [`DisplayList`].
117///
118/// Display ops are executed in order by a [`Renderer`] backend. The save/restore
119/// stack model means that clip and transform ops affect all subsequent draw ops
120/// until the matching `Restore`.
121///
122/// Most draw ops carry a `bounds` field (the layout rectangle of the owning node)
123/// and an optional `node_id` for hit-testing and debugging.
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub enum DisplayOp {
126    /// Pushes the current graphics state (clip region, transform) onto the stack.
127    Save,
128    /// Pops the graphics state, reverting clip and transform to the last `Save`.
129    Restore,
130    /// Sets a rectangular clip region. Drawing outside this rectangle is discarded.
131    ClipRect(LayoutRect),
132    /// Sets a rounded-rectangle clip region.
133    ClipRoundedRect {
134        /// The clip rectangle.
135        rect: LayoutRect,
136        /// The corner radius for the rounded clip.
137        radius: LayoutUnit,
138    },
139    /// Translates the coordinate origin by the given offset.
140    Translate(LayoutPoint),
141    /// Applies a 4x4 column-major transform matrix to the coordinate space.
142    Transform([LayoutUnit; 16]),
143    /// Draws a filled and/or stroked rectangle with optional rounded corners and shadow.
144    DrawRect {
145        /// The rectangle to draw.
146        rect: LayoutRect,
147        /// Optional interior fill.
148        fill: Option<Fill>,
149        /// Optional border stroke.
150        stroke: Option<Stroke>,
151        /// Corner radius (0.0 for sharp corners).
152        corner_radius: LayoutUnit,
153        /// Optional drop shadow.
154        shadow: Option<BoxShadow>,
155        /// The layout bounds of the owning node (used for hit-testing).
156        bounds: LayoutRect,
157        /// The IR node that produced this draw call (for debugging).
158        node_id: Option<NodeId>,
159    },
160    /// Draws a single-style text string.
161    DrawText {
162        /// The text to render.
163        text: String,
164        /// The top-left position of the text.
165        position: LayoutPoint,
166        /// Font size in logical pixels.
167        size: LayoutUnit,
168        /// Text foreground color.
169        color: Color,
170        /// The layout bounds of the owning node.
171        bounds: LayoutRect,
172        /// The IR node that produced this draw call.
173        node_id: Option<NodeId>,
174        /// Whether to underline the text.
175        underline: bool,
176        /// If set, a text cursor is drawn at this byte index.
177        caret_index: Option<usize>,
178    },
179    /// Draws multi-style (rich) text.
180    DrawRichText {
181        /// The styled text runs, in order.
182        runs: Vec<TextRun>,
183        /// The top-left position of the text block.
184        position: LayoutPoint,
185        /// The layout bounds of the owning node.
186        bounds: LayoutRect,
187        /// The IR node that produced this draw call.
188        node_id: Option<NodeId>,
189        /// If set, a text cursor is drawn at this byte index.
190        caret_index: Option<usize>,
191    },
192    /// Draws a raster image.
193    DrawImage {
194        /// The rectangle to draw the image into.
195        rect: LayoutRect,
196        /// Image source: file path, URL, or asset identifier.
197        source: String,
198        /// How the image scales to fit `rect`.
199        fit: ImageFit,
200        /// The layout bounds of the owning node.
201        bounds: LayoutRect,
202        /// The IR node that produced this draw call.
203        node_id: Option<NodeId>,
204    },
205    /// Draws an SVG-style path string.
206    DrawPath {
207        /// SVG path data (e.g., `"M 0 0 L 10 10 Z"`).
208        path: String,
209        /// Optional fill for the path interior.
210        fill: Option<Fill>,
211        /// Optional stroke for the path outline.
212        stroke: Option<Stroke>,
213        /// The layout bounds of the owning node.
214        bounds: LayoutRect,
215        /// The IR node that produced this draw call.
216        node_id: Option<NodeId>,
217    },
218    /// Draws inline SVG content.
219    DrawSvg {
220        /// The raw SVG markup.
221        content: String,
222        /// Optional fill color override.
223        fill: Option<Fill>,
224        /// Optional stroke color override.
225        stroke: Option<Stroke>,
226        /// The layout bounds of the owning node.
227        bounds: LayoutRect,
228        /// The IR node that produced this draw call.
229        node_id: Option<NodeId>,
230    },
231    /// Blits an external surface (video frame, embedded web view, etc.).
232    DrawSurface {
233        /// The rectangle to draw the surface into.
234        rect: LayoutRect,
235        /// Platform-specific surface identifier.
236        surface_id: u64,
237        /// Position/frame index within the surface (e.g., video frame number).
238        position: u64,
239        /// The layout bounds of the owning node.
240        bounds: LayoutRect,
241        /// The IR node that produced this draw call.
242        node_id: Option<NodeId>,
243    },
244}
245
246/// An ordered list of [`DisplayOp`]s ready to be consumed by a [`Renderer`].
247///
248/// The display list is the final output of the Fission pipeline before pixels.
249/// It is serializable, so it can be sent to a separate render thread or process.
250///
251/// # Example
252///
253/// ```rust
254/// use fission_render::*;
255///
256/// let bounds = LayoutRect::new(0.0, 0.0, 800.0, 600.0);
257/// let mut list = DisplayList::new(bounds);
258///
259/// list.push(DisplayOp::Save);
260/// list.push(DisplayOp::DrawRect {
261///     rect: LayoutRect::new(10.0, 10.0, 100.0, 50.0),
262///     fill: Some(Fill { color: Color { r: 0, g: 0, b: 0, a: 255 } }),
263///     stroke: None,
264///     corner_radius: 0.0,
265///     shadow: None,
266///     bounds: LayoutRect::new(10.0, 10.0, 100.0, 50.0),
267///     node_id: None,
268/// });
269/// list.push(DisplayOp::Restore);
270///
271/// assert_eq!(list.ops.len(), 3);
272/// ```
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub struct DisplayList {
275    /// The ordered sequence of rendering commands.
276    pub ops: Vec<DisplayOp>,
277    /// The bounding rectangle of the entire scene (typically the viewport).
278    pub bounds: LayoutRect,
279}
280
281impl DisplayList {
282    /// Creates an empty display list with the given scene bounds.
283    pub fn new(bounds: LayoutRect) -> Self {
284        Self {
285            ops: Vec::new(),
286            bounds,
287        }
288    }
289
290    /// Appends a display operation to the end of the list.
291    pub fn push(&mut self, op: DisplayOp) {
292        self.ops.push(op);
293    }
294}
295
296/// A backend that consumes a [`DisplayList`] and produces pixels.
297///
298/// Implement this trait for each platform rendering backend (Metal, Vulkan, wgpu,
299/// Skia, software rasterizer, etc.). The framework calls
300/// [`render`](Renderer::render) once per frame with the display list for the
301/// current scene.
302///
303/// # Example
304///
305/// ```rust,ignore
306/// use fission_render::{Renderer, DisplayList, DisplayOp};
307///
308/// struct SoftwareRenderer { buffer: Vec<u8> }
309///
310/// impl Renderer for SoftwareRenderer {
311///     fn render(&mut self, display_list: &DisplayList) -> anyhow::Result<()> {
312///         for op in &display_list.ops {
313///             match op {
314///                 DisplayOp::DrawRect { rect, fill, .. } => { /* rasterize */ }
315///                 _ => {}
316///             }
317///         }
318///         Ok(())
319///     }
320/// }
321/// ```
322pub trait Renderer {
323    /// Renders the given display list. Called once per frame.
324    fn render(&mut self, display_list: &DisplayList) -> anyhow::Result<()>;
325}