Skip to main content

martensite_render/
lib.rs

1//! Intermediate PaintList command stream and rendering backends.
2//!
3//! This crate provides the hardware-agnostic [`PaintList`] command stream
4//! produced by layout, plus two concrete implementations of the
5//! [`RenderBackend`] trait:
6//!
7//! - [`tinyskia_backend::TinySkiaBackend`] — a pure-CPU rasterizer built on
8//!   `tiny_skia`, suitable for headless CI and software fallback.
9//! - [`vello_backend::VelloRenderer`] — a GPU renderer that translates a
10//!   `PaintList` into a Vello scene (gated behind the `vello` feature).
11//!
12//! A perceptual diffing engine ([`diff`]) is provided for reftest-style
13//! verification of rendered output.
14
15#![forbid(unsafe_code)]
16
17pub mod diff;
18pub mod paint;
19pub mod presentation;
20pub mod tinyskia_backend;
21pub mod vello_backend;
22
23pub use paint::{
24    FontResource, GlyphInstance, GlyphRun, GradientStop, GradientStops, PaintCommand, PaintList,
25    PathBuilder,
26};
27pub use presentation::{
28    nonzero as presentation_nonzero, present_rgba_to_softbuffer, rgba_to_softbuffer,
29    PresentationError, SoftbufferPresenter,
30};
31pub use tinyskia_backend::TinySkiaBackend;
32pub use vello_backend::VelloRenderer;
33
34// Re-export the core rendering trait and supporting geometry types for
35// downstream convenience.
36pub use kurbo::{BezPath, Point, Rect};
37
38/// Abstraction over the concrete rendering target that consumes a [`PaintList`].
39///
40/// # Examples
41///
42/// ```
43/// use martensite_render::{PaintList, RenderBackend};
44/// use kurbo::Rect;
45///
46/// // A mock backend that records how many commands it received.
47/// struct CountingBackend {
48///     received: usize,
49/// }
50///
51/// impl RenderBackend for CountingBackend {
52///     fn render(&mut self, paint_list: &PaintList) {
53///         self.received = paint_list.len();
54///     }
55/// }
56///
57/// let mut list = PaintList::new();
58/// list.push_fill_rect(Rect::ZERO, [255, 0, 0, 255]);
59///
60/// let mut backend = CountingBackend { received: 0 };
61/// backend.render(&list);
62/// assert_eq!(backend.received, 1);
63/// ```
64pub trait RenderBackend: Send + 'static {
65    /// Renders the given [`PaintList`] to this backend's output surface.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use martensite_render::{PaintList, RenderBackend};
71    /// use kurbo::Rect;
72    ///
73    /// struct Recorder { count: usize }
74    /// impl RenderBackend for Recorder {
75    ///     fn render(&mut self, paint_list: &PaintList) {
76    ///         self.count = paint_list.len();
77    ///     }
78    /// }
79    ///
80    /// let mut list = PaintList::new();
81    /// list.push_fill_rect(Rect::ZERO, [255, 0, 0, 255]);
82    ///
83    /// let mut backend = Recorder { count: 0 };
84    /// backend.render(&list);
85    /// assert_eq!(backend.count, 1);
86    /// ```
87    fn render(&mut self, paint_list: &PaintList);
88}
89
90#[cfg(test)]
91mod tests {
92    use super::{PaintCommand, PaintList, RenderBackend};
93    use kurbo::{Point, Rect};
94
95    /// A mock backend that records the number of commands it received.
96    struct MockBackend {
97        received_count: usize,
98    }
99
100    impl MockBackend {
101        fn new() -> Self {
102            Self { received_count: 0 }
103        }
104    }
105
106    impl RenderBackend for MockBackend {
107        fn render(&mut self, paint_list: &PaintList) {
108            self.received_count = paint_list.commands.len();
109        }
110    }
111
112    #[test]
113    fn new_creates_empty_commands() {
114        let list = PaintList::new();
115        assert!(list.commands.is_empty());
116    }
117
118    #[test]
119    fn default_creates_empty_commands() {
120        let list = PaintList::default();
121        assert!(list.commands.is_empty());
122    }
123
124    #[test]
125    fn clear_empties_non_empty_list() {
126        let mut list = PaintList::new();
127        list.commands
128            .push(PaintCommand::FillRect(Rect::ZERO, [255, 0, 0, 255]));
129        assert_eq!(list.commands.len(), 1);
130        list.clear();
131        assert!(list.commands.is_empty());
132    }
133
134    #[test]
135    fn commands_appear_in_order() {
136        let mut list = PaintList::new();
137        list.commands
138            .push(PaintCommand::FillRect(Rect::ZERO, [255, 0, 0, 255]));
139        list.commands
140            .push(PaintCommand::StrokeRect(Rect::ZERO, 1.0, [0, 255, 0, 255]));
141        list.commands.push(PaintCommand::DrawText(
142            Point::ZERO,
143            "hi".to_string(),
144            12.0,
145            [0, 0, 255, 255],
146        ));
147        assert_eq!(list.commands.len(), 3);
148        assert!(matches!(list.commands[0], PaintCommand::FillRect(..)));
149        assert!(matches!(list.commands[1], PaintCommand::StrokeRect(..)));
150        assert!(matches!(list.commands[2], PaintCommand::DrawText(..)));
151    }
152
153    #[test]
154    fn mock_backend_receives_commands() {
155        let mut list = PaintList::new();
156        list.commands
157            .push(PaintCommand::FillRect(Rect::ZERO, [255, 0, 0, 255]));
158        list.commands
159            .push(PaintCommand::StrokeRect(Rect::ZERO, 1.0, [0, 255, 0, 255]));
160
161        let mut backend = MockBackend::new();
162        assert_eq!(backend.received_count, 0);
163        backend.render(&list);
164        assert_eq!(backend.received_count, 2);
165
166        list.clear();
167        backend.render(&list);
168        assert_eq!(backend.received_count, 0);
169    }
170}