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}