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    a.union(b)
329}
330
331/// Backend trait for executing a [`CommandList`]. The blanket impl is
332/// not provided because that would prevent backends from supplying
333/// optimized implementations; instead, wrap any [`Renderer`] in
334/// [`RendererSink`] for default-dispatch semantics, or implement
335/// `CommandSink` directly for native batching.
336pub trait CommandSink {
337    /// Execute the entire command list. The list is consumed by
338    /// reference — backends that need owned cmds should clone.
339    fn submit(&mut self, list: &CommandList);
340}
341
342/// Default-dispatch adapter: wraps any [`Renderer`] and submits cmds by
343/// calling the matching trait method per command. Use this when you
344/// want command-list capture/replay without per-backend optimization.
345pub struct RendererSink<'a, R: Renderer + ?Sized> {
346    /// Inner renderer.
347    pub inner: &'a mut R,
348}
349
350impl<'a, R: Renderer + ?Sized> RendererSink<'a, R> {
351    /// Wrap a renderer. The result acts as a [`CommandSink`].
352    pub fn new(inner: &'a mut R) -> Self {
353        Self { inner }
354    }
355}
356
357impl<R: Renderer + ?Sized> CommandSink for RendererSink<'_, R> {
358    fn submit(&mut self, list: &CommandList) {
359        list.replay(self.inner);
360    }
361}
362
363/// `Renderer` adapter that records structured AA draws into a
364/// [`CommandList`]. Unstructured ops (`draw_text`, `draw_pixels`) and
365/// `blend_row` forward immediately to a passthrough renderer rather
366/// than recording — see module docs for the lifetime/alloc rationale.
367///
368/// Typical usage:
369///
370/// ```ignore
371/// let mut list = CommandList::new();
372/// {
373///     let mut sink = NullRenderer::default(); // or any "do nothing" renderer
374///     let mut recorder = Recorder::new(&mut list, &mut sink);
375///     widget.draw(&mut recorder);
376/// }
377/// // list now contains the structured draws
378/// ```
379pub struct Recorder<'a> {
380    list: &'a mut CommandList,
381    passthrough: &'a mut dyn Renderer,
382}
383
384impl<'a> Recorder<'a> {
385    /// Create a recorder writing structured draws into `list` and
386    /// forwarding unstructured ops to `passthrough`.
387    pub fn new(list: &'a mut CommandList, passthrough: &'a mut dyn Renderer) -> Self {
388        Self { list, passthrough }
389    }
390}
391
392impl Renderer for Recorder<'_> {
393    fn fill_rect(&mut self, rect: Rect, color: Color) {
394        self.list.push(Cmd::FillRect {
395            rect,
396            color,
397            blend: BlendMode::Replace,
398        });
399    }
400
401    fn blend_rect(&mut self, rect: Rect, color: Color) {
402        self.list.push(Cmd::FillRect {
403            rect,
404            color,
405            blend: BlendMode::SrcOver,
406        });
407    }
408
409    fn fill_obb_aa(&mut self, obb: Obb, color: Color) {
410        self.list.push(Cmd::FillObb {
411            obb,
412            color,
413            blend: BlendMode::SrcOver,
414        });
415    }
416
417    fn fill_disc_aa(&mut self, center: PointF, radius: f32, color: Color) {
418        self.list.push(Cmd::FillDisc {
419            center,
420            radius,
421            color,
422            blend: BlendMode::SrcOver,
423        });
424    }
425
426    #[allow(clippy::too_many_arguments)]
427    fn fill_arc_aa(
428        &mut self,
429        center: PointF,
430        r_outer: f32,
431        r_inner: f32,
432        start_cos: f32,
433        start_sin: f32,
434        end_cos: f32,
435        end_sin: f32,
436        extent: f32,
437        color: Color,
438    ) {
439        self.list.push(Cmd::FillArc {
440            center,
441            r_outer,
442            r_inner,
443            start_cos,
444            start_sin,
445            end_cos,
446            end_sin,
447            extent,
448            color,
449            blend: BlendMode::SrcOver,
450        });
451    }
452
453    fn stroke_line_aa(&mut self, a: PointF, b: PointF, width: f32, color: Color) {
454        self.list.push(Cmd::StrokeLine {
455            a,
456            b,
457            width,
458            color,
459            blend: BlendMode::SrcOver,
460        });
461    }
462
463    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
464        // Unstructured: forward immediately. The recorded list misses
465        // this op; document the limitation. v2 may add owned-string
466        // capture if profiling shows it matters.
467        self.passthrough.draw_text(position, text, color);
468    }
469
470    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
471        // Same passthrough rationale as draw_text.
472        self.passthrough
473            .draw_pixels(position, pixels, width, height);
474    }
475
476    fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
477        // blend_row is the AA inner-loop primitive. Forward to passthrough
478        // since recording per-row coverage spans would explode the cmd
479        // count and defeat the language layer's batching purpose. Higher
480        // -level methods (fill_obb_aa etc.) record correctly above.
481        self.passthrough.blend_row(x, y, color, coverage);
482    }
483}