Skip to main content

rlvgl_core/
cmd.rs

1//! Graphics-language layer: structured drawing commands as data, separable
2//! from execution.
3//!
4//! The [`Renderer`] trait is a *drawing API* — verbs invoked one at a time.
5//! [`Cmd`] + [`CommandList`] define a *graphics language* — a noun
6//! vocabulary where each draw is a value the backend consumes. The shift
7//! lets backends:
8//!
9//! - Inspect the whole frame before executing any of it (occlusion
10//!   analysis, opaque-cmd skip, dirty-rect union).
11//! - Coalesce adjacent compatible ops (sequential same-color fills,
12//!   batched OBB rasterizations).
13//! - Chain hardware transfers (configure DMA2D for cmd N+1 while N runs).
14//! - Bin commands by tile for cache-resident rendering.
15//! - Replay a recorded list to a different surface (e.g. snapshot tests,
16//!   off-screen pre-render of static content).
17//!
18//! See [`Recorder`] for capturing an existing widget's [`Renderer`] calls
19//! into a [`CommandList`], and [`RendererSink`] for the default-dispatch
20//! adapter that any [`Renderer`] can be wrapped in to act as a
21//! [`CommandSink`].
22//!
23//! # Lifetime model
24//!
25//! v1 [`Cmd`] is `Clone` (owns its data — heap-allocated `String` /
26//! `Vec<Color>` for text and pixel ops). This trades alloc per
27//! unstructured op for a simpler, lifetime-free API. Future arena-backed
28//! variant can be added behind a feature flag if alloc cost becomes
29//! material.
30
31use alloc::string::String;
32use alloc::vec::Vec;
33
34use crate::raster::{Obb, PointF};
35use crate::renderer::Renderer;
36use crate::widget::{Color, Rect};
37
38/// Composition mode for a draw command. Backends that have hardware
39/// blend modes can branch on this; software backends may collapse Add /
40/// Multiply down to per-pixel arithmetic.
41#[derive(Copy, Clone, Debug, PartialEq, Eq)]
42pub enum BlendMode {
43    /// Replace destination pixels (alpha-ignoring fast path).
44    Replace,
45    /// Standard source-over with `color.alpha * coverage`.
46    SrcOver,
47    /// Additive blend: dst = clamp(dst + src * alpha).
48    Add,
49    /// Multiplicative blend: dst = dst * src (alpha modulates).
50    Multiply,
51}
52
53/// A single drawing command. Z-order is list position in the
54/// [`CommandList`] that contains it.
55#[derive(Clone, Debug)]
56pub enum Cmd {
57    /// Filled axis-aligned rectangle.
58    FillRect {
59        /// Rectangle bounds.
60        rect: Rect,
61        /// Fill color.
62        color: Color,
63        /// Composition mode.
64        blend: BlendMode,
65    },
66    /// Anti-aliased filled oriented bounding box (rotated rectangle).
67    FillObb {
68        /// Pre-computed OBB geometry.
69        obb: Obb,
70        /// Fill color.
71        color: Color,
72        /// Composition mode.
73        blend: BlendMode,
74    },
75    /// Anti-aliased filled disc (filled circle).
76    FillDisc {
77        /// Sub-pixel center.
78        center: PointF,
79        /// Radius in pixels.
80        radius: f32,
81        /// Fill color.
82        color: Color,
83        /// Composition mode.
84        blend: BlendMode,
85    },
86    /// Anti-aliased annular arc / pie slice. See
87    /// [`crate::raster::rasterize_arc`] for the angle convention.
88    FillArc {
89        /// Sub-pixel center.
90        center: PointF,
91        /// Outer radius in pixels.
92        r_outer: f32,
93        /// Inner radius in pixels (0.0 for a pie slice).
94        r_inner: f32,
95        /// Cosine of the start ray direction.
96        start_cos: f32,
97        /// Sine of the start ray direction.
98        start_sin: f32,
99        /// Cosine of the end ray direction.
100        end_cos: f32,
101        /// Sine of the end ray direction.
102        end_sin: f32,
103        /// Signed angular extent in radians.
104        extent: f32,
105        /// Fill color.
106        color: Color,
107        /// Composition mode.
108        blend: BlendMode,
109    },
110    /// Anti-aliased stroked line of given width (square caps).
111    StrokeLine {
112        /// Line start.
113        a: PointF,
114        /// Line end.
115        b: PointF,
116        /// Line width in pixels.
117        width: f32,
118        /// Stroke color.
119        color: Color,
120        /// Composition mode.
121        blend: BlendMode,
122    },
123    /// Text draw — owned `String` so the cmd can outlive the original
124    /// borrow. See module docs for the alloc trade-off.
125    DrawText {
126        /// Anchor position (baseline origin).
127        position: (i32, i32),
128        /// UTF-8 text.
129        text: String,
130        /// Text color.
131        color: Color,
132    },
133    /// Pixel-buffer blit — owned `Vec<Color>` for the same reason as
134    /// [`Cmd::DrawText`]'s string.
135    DrawPixels {
136        /// Top-left destination position.
137        position: (i32, i32),
138        /// Source pixels, row-major.
139        pixels: Vec<Color>,
140        /// Source width in pixels.
141        width: u32,
142        /// Source height in pixels.
143        height: u32,
144    },
145    /// Clip stack marker. `Some(rect)` pushes a clip; `None` clears.
146    /// Backends should bound subsequent draws to the active clip.
147    SetClip {
148        /// Clip rectangle, or `None` to clear.
149        rect: Option<Rect>,
150    },
151    /// Backend flush boundary (compositor swap, telemetry checkpoint).
152    /// Optimizing backends may not reorder cmds across a Barrier.
153    Barrier,
154}
155
156impl Cmd {
157    /// Dispatch this cmd onto a [`Renderer`], using the matching trait
158    /// method for each variant. This is the default execution semantics
159    /// — backends override [`CommandSink`] to specialize.
160    pub fn dispatch_to<R: Renderer + ?Sized>(&self, r: &mut R) {
161        match self {
162            Cmd::FillRect { rect, color, blend } => match blend {
163                BlendMode::Replace => r.fill_rect(*rect, *color),
164                BlendMode::SrcOver => r.blend_rect(*rect, *color),
165                // Add / Multiply have no Renderer-trait equivalent yet;
166                // fall through to SrcOver for v1. Backends with native
167                // support override CommandSink directly.
168                BlendMode::Add | BlendMode::Multiply => r.blend_rect(*rect, *color),
169            },
170            Cmd::FillObb { obb, color, .. } => r.fill_obb_aa(*obb, *color),
171            Cmd::FillDisc {
172                center,
173                radius,
174                color,
175                ..
176            } => r.fill_disc_aa(*center, *radius, *color),
177            Cmd::FillArc {
178                center,
179                r_outer,
180                r_inner,
181                start_cos,
182                start_sin,
183                end_cos,
184                end_sin,
185                extent,
186                color,
187                ..
188            } => r.fill_arc_aa(
189                *center, *r_outer, *r_inner, *start_cos, *start_sin, *end_cos, *end_sin, *extent,
190                *color,
191            ),
192            Cmd::StrokeLine {
193                a, b, width, color, ..
194            } => r.stroke_line_aa(*a, *b, *width, *color),
195            Cmd::DrawText {
196                position,
197                text,
198                color,
199            } => r.draw_text(*position, text, *color),
200            Cmd::DrawPixels {
201                position,
202                pixels,
203                width,
204                height,
205            } => r.draw_pixels(*position, pixels, *width, *height),
206            // Clip and Barrier are advisory for default dispatch — the
207            // base Renderer trait has no clip stack. Backends that
208            // implement clipping override CommandSink::submit directly.
209            Cmd::SetClip { .. } | Cmd::Barrier => {}
210        }
211    }
212
213    /// AABB of pixels this command might write, in absolute framebuffer
214    /// coords. Returns `None` for non-drawing markers (Barrier, SetClip).
215    pub fn aabb(&self) -> Option<Rect> {
216        match self {
217            Cmd::FillRect { rect, .. } => Some(*rect),
218            Cmd::FillObb { obb, .. } => Some(obb.aabb()),
219            Cmd::FillDisc { center, radius, .. }
220            | Cmd::FillArc {
221                center,
222                r_outer: radius,
223                ..
224            } => {
225                let pad = radius + 1.0;
226                Some(Rect {
227                    x: (center.x - pad) as i32 - 1,
228                    y: (center.y - pad) as i32 - 1,
229                    width: (pad * 2.0) as i32 + 3,
230                    height: (pad * 2.0) as i32 + 3,
231                })
232            }
233            Cmd::StrokeLine { a, b, width, .. } => {
234                let pad = width * 0.5 + 1.0;
235                let x0 = (a.x.min(b.x) - pad) as i32 - 1;
236                let y0 = (a.y.min(b.y) - pad) as i32 - 1;
237                let x1 = (a.x.max(b.x) + pad) as i32 + 2;
238                let y1 = (a.y.max(b.y) + pad) as i32 + 2;
239                Some(Rect {
240                    x: x0,
241                    y: y0,
242                    width: x1 - x0,
243                    height: y1 - y0,
244                })
245            }
246            Cmd::DrawText { .. } => None, // text bbox depends on font metrics
247            Cmd::DrawPixels {
248                position,
249                width,
250                height,
251                ..
252            } => Some(Rect {
253                x: position.0,
254                y: position.1,
255                width: *width as i32,
256                height: *height as i32,
257            }),
258            Cmd::SetClip { .. } | Cmd::Barrier => None,
259        }
260    }
261}
262
263/// Ordered list of drawing commands. Z-order is list position (later =
264/// on top). Backends consume the list via [`CommandSink::submit`].
265#[derive(Clone, Debug, Default)]
266pub struct CommandList {
267    cmds: Vec<Cmd>,
268}
269
270impl CommandList {
271    /// Create an empty command list.
272    pub fn new() -> Self {
273        Self::default()
274    }
275
276    /// Append a command. Z-order is list position.
277    pub fn push(&mut self, cmd: Cmd) {
278        self.cmds.push(cmd);
279    }
280
281    /// Number of commands in the list.
282    pub fn len(&self) -> usize {
283        self.cmds.len()
284    }
285
286    /// Whether the list contains no commands.
287    pub fn is_empty(&self) -> bool {
288        self.cmds.is_empty()
289    }
290
291    /// Iterate over commands in z-order (back to front).
292    pub fn iter(&self) -> core::slice::Iter<'_, Cmd> {
293        self.cmds.iter()
294    }
295
296    /// Drop all commands. Reuses the internal allocation.
297    pub fn clear(&mut self) {
298        self.cmds.clear();
299    }
300
301    /// Replay every command on `r` via [`Cmd::dispatch_to`]. This is the
302    /// "default-dispatch" execution semantics — backends that want
303    /// optimization implement [`CommandSink`] directly on themselves.
304    pub fn replay<R: Renderer + ?Sized>(&self, r: &mut R) {
305        for cmd in &self.cmds {
306            cmd.dispatch_to(r);
307        }
308    }
309
310    /// Union AABB of all drawing commands' write regions, computed via
311    /// [`Cmd::aabb`]. Returns `None` if the list contains no draw cmds
312    /// or only cmds with unknown bboxes (text).
313    pub fn dirty_union(&self) -> Option<Rect> {
314        let mut union: Option<Rect> = None;
315        for cmd in &self.cmds {
316            if let Some(bb) = cmd.aabb() {
317                union = Some(match union {
318                    None => bb,
319                    Some(u) => rect_union(u, bb),
320                });
321            }
322        }
323        union
324    }
325}
326
327fn rect_union(a: Rect, b: Rect) -> Rect {
328    let x0 = a.x.min(b.x);
329    let y0 = a.y.min(b.y);
330    let x1 = (a.x + a.width).max(b.x + b.width);
331    let y1 = (a.y + a.height).max(b.y + b.height);
332    Rect {
333        x: x0,
334        y: y0,
335        width: x1 - x0,
336        height: y1 - y0,
337    }
338}
339
340/// Backend trait for executing a [`CommandList`]. The blanket impl is
341/// not provided because that would prevent backends from supplying
342/// optimized implementations; instead, wrap any [`Renderer`] in
343/// [`RendererSink`] for default-dispatch semantics, or implement
344/// `CommandSink` directly for native batching.
345pub trait CommandSink {
346    /// Execute the entire command list. The list is consumed by
347    /// reference — backends that need owned cmds should clone.
348    fn submit(&mut self, list: &CommandList);
349}
350
351/// Default-dispatch adapter: wraps any [`Renderer`] and submits cmds by
352/// calling the matching trait method per command. Use this when you
353/// want command-list capture/replay without per-backend optimization.
354pub struct RendererSink<'a, R: Renderer + ?Sized> {
355    /// Inner renderer.
356    pub inner: &'a mut R,
357}
358
359impl<'a, R: Renderer + ?Sized> RendererSink<'a, R> {
360    /// Wrap a renderer. The result acts as a [`CommandSink`].
361    pub fn new(inner: &'a mut R) -> Self {
362        Self { inner }
363    }
364}
365
366impl<R: Renderer + ?Sized> CommandSink for RendererSink<'_, R> {
367    fn submit(&mut self, list: &CommandList) {
368        list.replay(self.inner);
369    }
370}
371
372/// `Renderer` adapter that records structured AA draws into a
373/// [`CommandList`]. Unstructured ops (`draw_text`, `draw_pixels`) and
374/// `blend_row` forward immediately to a passthrough renderer rather
375/// than recording — see module docs for the lifetime/alloc rationale.
376///
377/// Typical usage:
378///
379/// ```ignore
380/// let mut list = CommandList::new();
381/// {
382///     let mut sink = NullRenderer::default(); // or any "do nothing" renderer
383///     let mut recorder = Recorder::new(&mut list, &mut sink);
384///     widget.draw(&mut recorder);
385/// }
386/// // list now contains the structured draws
387/// ```
388pub struct Recorder<'a> {
389    list: &'a mut CommandList,
390    passthrough: &'a mut dyn Renderer,
391}
392
393impl<'a> Recorder<'a> {
394    /// Create a recorder writing structured draws into `list` and
395    /// forwarding unstructured ops to `passthrough`.
396    pub fn new(list: &'a mut CommandList, passthrough: &'a mut dyn Renderer) -> Self {
397        Self { list, passthrough }
398    }
399}
400
401impl Renderer for Recorder<'_> {
402    fn fill_rect(&mut self, rect: Rect, color: Color) {
403        self.list.push(Cmd::FillRect {
404            rect,
405            color,
406            blend: BlendMode::Replace,
407        });
408    }
409
410    fn blend_rect(&mut self, rect: Rect, color: Color) {
411        self.list.push(Cmd::FillRect {
412            rect,
413            color,
414            blend: BlendMode::SrcOver,
415        });
416    }
417
418    fn fill_obb_aa(&mut self, obb: Obb, color: Color) {
419        self.list.push(Cmd::FillObb {
420            obb,
421            color,
422            blend: BlendMode::SrcOver,
423        });
424    }
425
426    fn fill_disc_aa(&mut self, center: PointF, radius: f32, color: Color) {
427        self.list.push(Cmd::FillDisc {
428            center,
429            radius,
430            color,
431            blend: BlendMode::SrcOver,
432        });
433    }
434
435    #[allow(clippy::too_many_arguments)]
436    fn fill_arc_aa(
437        &mut self,
438        center: PointF,
439        r_outer: f32,
440        r_inner: f32,
441        start_cos: f32,
442        start_sin: f32,
443        end_cos: f32,
444        end_sin: f32,
445        extent: f32,
446        color: Color,
447    ) {
448        self.list.push(Cmd::FillArc {
449            center,
450            r_outer,
451            r_inner,
452            start_cos,
453            start_sin,
454            end_cos,
455            end_sin,
456            extent,
457            color,
458            blend: BlendMode::SrcOver,
459        });
460    }
461
462    fn stroke_line_aa(&mut self, a: PointF, b: PointF, width: f32, color: Color) {
463        self.list.push(Cmd::StrokeLine {
464            a,
465            b,
466            width,
467            color,
468            blend: BlendMode::SrcOver,
469        });
470    }
471
472    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
473        // Unstructured: forward immediately. The recorded list misses
474        // this op; document the limitation. v2 may add owned-string
475        // capture if profiling shows it matters.
476        self.passthrough.draw_text(position, text, color);
477    }
478
479    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
480        // Same passthrough rationale as draw_text.
481        self.passthrough
482            .draw_pixels(position, pixels, width, height);
483    }
484
485    fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
486        // blend_row is the AA inner-loop primitive. Forward to passthrough
487        // since recording per-row coverage spans would explode the cmd
488        // count and defeat the language layer's batching purpose. Higher
489        // -level methods (fill_obb_aa etc.) record correctly above.
490        self.passthrough.blend_row(x, y, color, coverage);
491    }
492}