Skip to main content

rlvgl_core/
renderer.rs

1//! Rendering interface used by widgets.
2//!
3//! Implementors of this trait can target displays, off-screen buffers or
4//! simulator windows.
5
6use alloc::vec::Vec;
7
8use crate::cmd::CommandList;
9use crate::draw::{GradientDesc, ShadowDesc};
10use crate::font::ShapedText;
11use crate::image::{BlitOpts, ImageDescriptor, PixelFormat};
12use crate::invalidation::InvalidationList;
13use crate::mask::AlphaMask;
14use crate::raster::{self, CoverageSink, Obb};
15use crate::widget::{Color, Rect};
16
17/// Target-agnostic drawing interface.
18///
19/// Renderers are supplied to widgets during the draw phase. Implementations
20/// may target a physical display, an off-screen buffer or a simulator window.
21pub trait Renderer {
22    /// Fill the given rectangle with a solid color.
23    fn fill_rect(&mut self, rect: Rect, color: Color);
24
25    /// Draw UTF‑8 text with its baseline anchored at the provided position using the color.
26    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color);
27
28    /// Draw a pre-shaped text run using glyph extent information.
29    ///
30    /// `origin` is an additional translation applied to every glyph placement.
31    /// In `shaped`, pass `(0, 0)` when glyph positions are already target-space
32    /// coordinates. The default implementation renders glyph coverage when the
33    /// run carries a font reference; otherwise it falls back to deterministic
34    /// extent-only rectangles.
35    ///
36    /// Backends can override this for hardware text acceleration while preserving
37    /// the same placement and clipping contract.
38    fn draw_text_shaped(&mut self, shaped: &ShapedText<'_>, origin: (i32, i32), color: Color) {
39        for glyph in &shaped.glyphs {
40            let mut extent = glyph.extent();
41            extent.x += origin.0;
42            extent.y += origin.1;
43            if extent.width <= 0 || extent.height <= 0 {
44                continue;
45            }
46            if let Some(font) = shaped.font
47                && draw_glyph_coverage(self, font, glyph.ch, extent, color)
48            {
49                continue;
50            }
51            self.blend_rect(extent, color);
52        }
53    }
54
55    /// Draw a single glyph's coverage with its pen origin on the baseline at
56    /// `origin` (the same anchor convention as [`draw_text`](Self::draw_text)).
57    ///
58    /// The glyph's tight bitmap extent is derived from `font`'s metrics for
59    /// `ch` (bearing + width/height), and coverage flows through
60    /// [`blend_row`](Self::blend_row) — identical to the per-glyph path inside
61    /// [`draw_text_shaped`](Self::draw_text_shaped). When `font` provides no
62    /// coverage for `ch`, the default falls back to a deterministic extent
63    /// rectangle via [`blend_rect`](Self::blend_rect); a glyph with no metrics
64    /// (or a zero-area extent, e.g. a space) draws nothing.
65    ///
66    /// This is a defaulted convenience for widgets that place glyphs
67    /// individually rather than along one baseline — e.g.
68    /// [`ArcLabel`](https://docs.rs/rlvgl-widgets) (FONT-00 §7.B). Shaped runs
69    /// should use [`draw_text_shaped`](Self::draw_text_shaped). Backends MAY
70    /// override this for glyph-blit acceleration (FONT-00 §8.B).
71    fn draw_glyph(
72        &mut self,
73        font: &dyn crate::font::FontMetrics,
74        ch: char,
75        origin: (i32, i32),
76        color: Color,
77    ) {
78        let Some(info) = font.glyph_metrics(ch) else {
79            return;
80        };
81        let extent = Rect {
82            x: origin.0 + info.bearing_x as i32,
83            y: origin.1 - info.bearing_y as i32,
84            width: info.width as i32,
85            height: info.height as i32,
86        };
87        if extent.width <= 0 || extent.height <= 0 {
88            return;
89        }
90        if draw_glyph_coverage(self, font, ch, extent, color) {
91            return;
92        }
93        self.blend_rect(extent, color);
94    }
95
96    /// Blend a rectangle onto the target, honoring the alpha channel of `color`
97    /// for source-over compositing.
98    ///
99    /// The default implementation ignores alpha and falls back to
100    /// [`fill_rect`](Self::fill_rect). Backends with blending support should
101    /// override this for correct anti-aliased rendering.
102    fn blend_rect(&mut self, rect: Rect, color: Color) {
103        self.fill_rect(rect, color);
104    }
105
106    /// Blit a buffer of pixels to the target at the given position.
107    ///
108    /// The default implementation falls back to per-pixel [`fill_rect`](Self::fill_rect)
109    /// calls. Backends with bulk-copy support (e.g. DMA2D) should override this.
110    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
111        for y in 0..height as i32 {
112            for x in 0..width as i32 {
113                let idx = (y as u32 * width + x as u32) as usize;
114                if let Some(&c) = pixels.get(idx) {
115                    self.fill_rect(
116                        Rect {
117                            x: position.0 + x,
118                            y: position.1 + y,
119                            width: 1,
120                            height: 1,
121                        },
122                        c,
123                    );
124                }
125            }
126        }
127    }
128
129    /// Blit an [`ImageDescriptor`] into `dest` using deterministic software sampling.
130    ///
131    /// `dest` is the output rectangle in framebuffer coordinates. Scaling
132    /// controls nearest-neighbor source sampling inside that output rectangle;
133    /// callers choose the destination dimensions they want to draw. Rotation
134    /// is snapped to the nearest cardinal orientation. The default software
135    /// path decodes one output row at a time and forwards it through
136    /// [`draw_pixels`](Self::draw_pixels), so clipping adapters and backend
137    /// pixel fast paths continue to apply.
138    fn blit_image(&mut self, dest: Rect, descriptor: &ImageDescriptor<'_>, opts: &BlitOpts) {
139        if dest.width <= 0
140            || dest.height <= 0
141            || descriptor.width == 0
142            || descriptor.height == 0
143            || opts.scale_x == 0
144            || opts.scale_y == 0
145        {
146            return;
147        }
148
149        let output = Rect {
150            x: 0,
151            y: 0,
152            width: dest.width,
153            height: dest.height,
154        };
155        let visible = match opts.clip {
156            Some(clip) => output.intersect(clip),
157            None => Some(output),
158        };
159        let Some(visible) = visible else {
160            return;
161        };
162
163        let mut row = Vec::new();
164        row.resize(visible.width as usize, Color(0, 0, 0, 0));
165        for local_y in visible.y..visible.y + visible.height {
166            for local_x in visible.x..visible.x + visible.width {
167                let out_idx = (local_x - visible.x) as usize;
168                row[out_idx] = sample_image_pixel(descriptor, opts, local_x, local_y)
169                    .unwrap_or(Color(0, 0, 0, 0));
170            }
171            self.draw_pixels(
172                (dest.x + visible.x, dest.y + local_y),
173                &row,
174                visible.width as u32,
175                1,
176            );
177        }
178    }
179
180    /// Blend a horizontal run of pixels with per-pixel anti-aliased coverage.
181    ///
182    /// `coverage[i]` modulates `color`'s alpha for the pixel at
183    /// `(x + i, y)`. This is the AA inner-loop primitive: every higher-level
184    /// AA method funnels coverage spans through here, so backends with
185    /// hardware blend support (e.g. DMA2D's blend mode with per-pixel alpha
186    /// modulation) should override this for performance.
187    ///
188    /// The default implementation walks the run via [`blend_rect`](Self::blend_rect)
189    /// per pixel — correct, slow, sufficient for non-hot paths.
190    fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
191        for (i, &cov) in coverage.iter().enumerate() {
192            if cov == 0 {
193                continue;
194            }
195            let alpha = ((color.3 as u16 * cov as u16) / 255) as u8;
196            self.blend_rect(
197                Rect {
198                    x: x + i as i32,
199                    y,
200                    width: 1,
201                    height: 1,
202                },
203                Color(color.0, color.1, color.2, alpha),
204            );
205        }
206    }
207
208    /// Fill `rect` with `color` modulated by an alpha mask.
209    ///
210    /// The default implementation evaluates the mask in fixed-size row
211    /// chunks and emits coverage through [`blend_row`](Self::blend_row).
212    /// Backends with hardware mask support may override this method, but the
213    /// software path is the reference behavior.
214    fn fill_masked(&mut self, rect: Rect, color: Color, mask: &dyn AlphaMask) {
215        if rect.width <= 0 || rect.height <= 0 || color.3 == 0 {
216            return;
217        }
218        let mut coverage = [0u8; 64];
219        for y in rect.y..rect.y + rect.height {
220            let mut x = rect.x;
221            let end = rect.x + rect.width;
222            while x < end {
223                let run = (end - x).min(coverage.len() as i32) as usize;
224                let row = &mut coverage[..run];
225                mask.row(x, y, row);
226                self.blend_row(x, y, color, row);
227                x += run as i32;
228            }
229        }
230    }
231
232    /// Fill `rect` with a deterministic software gradient.
233    ///
234    /// The default path samples one pixel at a time using integer color
235    /// interpolation. Opaque samples use [`fill_rect`](Self::fill_rect);
236    /// translucent samples use [`blend_rect`](Self::blend_rect).
237    fn fill_gradient(&mut self, rect: Rect, gradient: &GradientDesc<'_>) {
238        if rect.width <= 0 || rect.height <= 0 {
239            return;
240        }
241        for y in rect.y..rect.y + rect.height {
242            for x in rect.x..rect.x + rect.width {
243                let Some(color) = gradient.color_at(rect, x, y) else {
244                    continue;
245                };
246                if color.3 == 0 {
247                    continue;
248                }
249                let pixel = Rect {
250                    x,
251                    y,
252                    width: 1,
253                    height: 1,
254                };
255                if color.3 == 255 {
256                    self.fill_rect(pixel, color);
257                } else {
258                    self.blend_rect(pixel, color);
259                }
260            }
261        }
262    }
263
264    /// Draw a deterministic software box shadow below `rect`.
265    ///
266    /// The v1 fallback uses a rounded-rect-compatible rectangular blur
267    /// approximation. It is intentionally conservative and routes through
268    /// [`fill_masked`](Self::fill_masked), so clipping adapters inherit the
269    /// behavior through [`blend_row`](Self::blend_row).
270    fn draw_shadow(&mut self, rect: Rect, radius: u8, shadow: &ShadowDesc) {
271        if rect.width <= 0 || rect.height <= 0 || shadow.color.3 == 0 {
272            return;
273        }
274        let spread = shadow.spread as i32;
275        let blur = shadow.blur as i32;
276        let base = Rect {
277            x: rect.x + shadow.offset_x as i32 - spread,
278            y: rect.y + shadow.offset_y as i32 - spread,
279            width: rect.width + spread * 2,
280            height: rect.height + spread * 2,
281        };
282        if base.width <= 0 || base.height <= 0 {
283            return;
284        }
285        let draw_rect = Rect {
286            x: base.x - blur,
287            y: base.y - blur,
288            width: base.width + blur * 2,
289            height: base.height + blur * 2,
290        };
291        let mask = ShadowMask {
292            base,
293            blur,
294            radius: radius as i32 + spread,
295        };
296        self.fill_masked(draw_rect, shadow.color, &mask);
297    }
298
299    /// Fill an oriented bounding box with anti-aliased coverage.
300    ///
301    /// `obb`'s center is in absolute framebuffer coordinates with sub-pixel
302    /// precision; `theta` is supplied via pre-computed `(cos_t, sin_t)` on
303    /// the [`Obb`] itself. `color`'s alpha is multiplied by per-pixel
304    /// coverage before blending.
305    ///
306    /// The default implementation rasterizes via
307    /// [`raster::rasterize_obb`] and emits coverage spans through
308    /// [`blend_row`](Self::blend_row). Backends that have hardware OBB
309    /// rasterization can override this directly; backends with hardware
310    /// blend but software geometry should override `blend_row` instead and
311    /// inherit this default.
312    fn fill_obb_aa(&mut self, obb: Obb, color: Color) {
313        let clip = obb.aabb();
314        let mut sink = RowBlendSink { r: self, color };
315        raster::rasterize_obb(&obb, clip, &mut sink);
316    }
317
318    /// Fill a disc (filled circle) with anti-aliased coverage at the
319    /// boundary. Sub-pixel center; sqrt is restricted to the 1-pixel AA
320    /// ring so the inner-area fast-path stays integer-arithmetic only.
321    ///
322    /// The default implementation routes through
323    /// [`raster::rasterize_disc`] + [`blend_row`](Self::blend_row), so
324    /// any backend that has overridden `blend_row` for hardware blend
325    /// inherits the acceleration here automatically.
326    fn fill_disc_aa(&mut self, center: crate::raster::PointF, radius: f32, color: Color) {
327        let pad = radius + 1.0;
328        let clip = Rect {
329            x: (center.x - pad) as i32 - 1,
330            y: (center.y - pad) as i32 - 1,
331            width: (pad * 2.0) as i32 + 3,
332            height: (pad * 2.0) as i32 + 3,
333        };
334        let mut sink = RowBlendSink { r: self, color };
335        raster::rasterize_disc(center, radius, clip, &mut sink);
336    }
337
338    /// Stroke a line between `a` and `b` with given `width`, anti-aliased.
339    /// Endpoints are square-cut; see [`raster::rasterize_line`].
340    ///
341    /// Default implementation routes through
342    /// [`raster::rasterize_line`] + [`blend_row`](Self::blend_row), so
343    /// `blend_row` overrides apply automatically.
344    fn stroke_line_aa(
345        &mut self,
346        a: crate::raster::PointF,
347        b: crate::raster::PointF,
348        width: f32,
349        color: Color,
350    ) {
351        // Conservative AABB: full canvas span — `rasterize_line` clips
352        // internally to the OBB AABB anyway, so passing a permissive clip
353        // here only costs a single rect-intersect inside the kernel.
354        let clip = Rect {
355            x: i32::MIN / 2,
356            y: i32::MIN / 2,
357            width: i32::MAX / 2,
358            height: i32::MAX / 2,
359        };
360        let mut sink = RowBlendSink { r: self, color };
361        raster::rasterize_line(a, b, width, clip, &mut sink);
362    }
363
364    /// Fill an annular arc / pie slice with anti-aliased coverage.
365    /// See [`raster::rasterize_arc`] for the angle convention; in short,
366    /// `(start_cos, start_sin)` and `(end_cos, end_sin)` are pre-computed
367    /// boundary-ray unit vectors and `extent` is the *signed* angular
368    /// magnitude. `r_inner = 0.0` produces a pie slice; `r_inner > 0.0`
369    /// produces a ring segment.
370    ///
371    /// Default impl routes through [`raster::rasterize_arc`] +
372    /// [`blend_row`](Self::blend_row).
373    #[allow(clippy::too_many_arguments)]
374    fn fill_arc_aa(
375        &mut self,
376        center: crate::raster::PointF,
377        r_outer: f32,
378        r_inner: f32,
379        start_cos: f32,
380        start_sin: f32,
381        end_cos: f32,
382        end_sin: f32,
383        extent: f32,
384        color: Color,
385    ) {
386        let pad = r_outer + 1.0;
387        let clip = Rect {
388            x: (center.x - pad) as i32 - 1,
389            y: (center.y - pad) as i32 - 1,
390            width: (pad * 2.0) as i32 + 3,
391            height: (pad * 2.0) as i32 + 3,
392        };
393        let mut sink = RowBlendSink { r: self, color };
394        raster::rasterize_arc(
395            center, r_outer, r_inner, start_cos, start_sin, end_cos, end_sin, extent, clip,
396            &mut sink,
397        );
398    }
399
400    /// Execute a captured [`CommandList`] against this renderer.
401    ///
402    /// Default implementation walks the list and dispatches each
403    /// command via [`crate::cmd::Cmd::dispatch_to`] — equivalent to
404    /// having issued the corresponding trait calls directly. Backends
405    /// override this to apply pre-pass optimizations: occlusion
406    /// culling, opaque-cmd skip, hardware command-buffer chaining,
407    /// tile binning. Overrides must preserve byte-identical output to
408    /// the default path.
409    ///
410    /// This is the "graphics-language" entry point on the [`Renderer`]
411    /// trait — code holding `&mut dyn Renderer` can submit captured
412    /// command lists and pick up backend specializations
413    /// transparently. See [`crate::cmd`] for the language model.
414    fn submit(&mut self, list: &CommandList) {
415        list.replay(self);
416    }
417}
418
419struct RowBlendSink<'r, R: Renderer + ?Sized> {
420    r: &'r mut R,
421    color: Color,
422}
423
424impl<R: Renderer + ?Sized> CoverageSink for RowBlendSink<'_, R> {
425    fn row(&mut self, x: i32, y: i32, coverage: &[u8]) {
426        self.r.blend_row(x, y, self.color, coverage);
427    }
428}
429
430/// Renderer adapter that pushes draw-time dirty rectangles into a shared
431/// [`InvalidationList`], while forwarding all drawing calls to `inner`.
432pub struct DirtyTrackingRenderer<'a, 'b, R: Renderer + ?Sized, const N: usize> {
433    /// Wrapped renderer.
434    inner: &'a mut R,
435    /// Shared invalidation sink.
436    invalidation: &'b mut InvalidationList<N>,
437    /// Optional clipping contract, in renderer coordinates.
438    clip: Option<Rect>,
439}
440
441impl<'a, 'b, R: Renderer + ?Sized, const N: usize> DirtyTrackingRenderer<'a, 'b, R, N> {
442    /// Wrap `inner` and collect draw-time dirty rects into `invalidation`.
443    ///
444    /// The wrapper does **not** impose a rendering clip by default; callers
445    /// can provide one with [`Self::with_clip`] when needed.
446    pub fn new(inner: &'a mut R, invalidation: &'b mut InvalidationList<N>) -> Self {
447        Self {
448            inner,
449            invalidation,
450            clip: None,
451        }
452    }
453
454    /// Wrap `inner` and additionally clip every reported dirty rect to `clip`.
455    /// Degenerate clips drop all subsequent tracking.
456    pub fn with_clip(
457        inner: &'a mut R,
458        invalidation: &'b mut InvalidationList<N>,
459        clip: Rect,
460    ) -> Self {
461        Self {
462            inner,
463            invalidation,
464            clip: (clip.width > 0 && clip.height > 0).then_some(clip),
465        }
466    }
467
468    /// Access the wrapped renderer.
469    pub fn inner(&self) -> &R {
470        self.inner
471    }
472
473    /// Access the wrapped renderer mutably.
474    pub fn inner_mut(&mut self) -> &mut R {
475        self.inner
476    }
477
478    fn push_rect(&mut self, rect: Rect) {
479        if rect.width <= 0 || rect.height <= 0 {
480            return;
481        }
482        if let Some(clip) = self.clip {
483            if let Some(visible) = rect.intersect(clip) {
484                self.invalidation.push(visible);
485            }
486        } else {
487            self.invalidation.push(rect);
488        }
489    }
490
491    fn push_blend_row(&mut self, x: i32, y: i32, coverage: &[u8]) {
492        let Some(start) = coverage.iter().position(|c| *c != 0) else {
493            return;
494        };
495        let Some(end) = coverage.iter().rposition(|c| *c != 0) else {
496            return;
497        };
498
499        let width = i32::try_from(end - start + 1).unwrap_or(i32::MAX);
500        let rect = Rect {
501            x: x + i32::try_from(start).unwrap_or(i32::MAX),
502            y,
503            width,
504            height: 1,
505        };
506        self.push_rect(rect);
507    }
508
509    fn push_text_estimate(&mut self, position: (i32, i32), text: &str) {
510        if text.is_empty() {
511            return;
512        }
513        let glyph_count = text.len().min((i32::MAX as usize) / 8);
514        let width = (glyph_count as u32).saturating_mul(8) as i32;
515        self.push_rect(Rect {
516            x: position.0,
517            y: position.1 - TEXT_NOMINAL_LINE_PX,
518            width,
519            height: TEXT_NOMINAL_LINE_PX,
520        });
521    }
522}
523
524impl<R: Renderer + ?Sized, const N: usize> Renderer for DirtyTrackingRenderer<'_, '_, R, N> {
525    fn fill_rect(&mut self, rect: Rect, color: Color) {
526        self.push_rect(rect);
527        self.inner.fill_rect(rect, color);
528    }
529
530    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
531        self.push_text_estimate(position, text);
532        self.inner.draw_text(position, text, color);
533    }
534
535    fn draw_text_shaped(&mut self, shaped: &ShapedText<'_>, origin: (i32, i32), color: Color) {
536        let bounds = Rect {
537            x: shaped.bounds.x + origin.0,
538            y: shaped.bounds.y + origin.1,
539            width: shaped.bounds.width,
540            height: shaped.bounds.height,
541        };
542        self.push_rect(bounds);
543        self.inner.draw_text_shaped(shaped, origin, color);
544    }
545
546    fn blend_rect(&mut self, rect: Rect, color: Color) {
547        self.push_rect(rect);
548        self.inner.blend_rect(rect, color);
549    }
550
551    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
552        let width_i32 = i32::try_from(width).unwrap_or(i32::MAX);
553        let height_i32 = i32::try_from(height).unwrap_or(i32::MAX);
554        self.push_rect(Rect {
555            x: position.0,
556            y: position.1,
557            width: width_i32,
558            height: height_i32,
559        });
560        self.inner.draw_pixels(position, pixels, width, height);
561    }
562
563    fn blit_image(
564        &mut self,
565        dest: Rect,
566        descriptor: &crate::image::ImageDescriptor<'_>,
567        opts: &crate::image::BlitOpts,
568    ) {
569        if dest.width <= 0 || dest.height <= 0 {
570            return;
571        }
572        let output = Rect {
573            x: 0,
574            y: 0,
575            width: dest.width,
576            height: dest.height,
577        };
578        let visible = match opts.clip {
579            Some(clip) => output.intersect(clip),
580            None => Some(output),
581        };
582        let Some(visible) = visible else {
583            return;
584        };
585        self.push_rect(Rect {
586            x: dest.x + visible.x,
587            y: dest.y + visible.y,
588            width: visible.width,
589            height: visible.height,
590        });
591        self.inner.blit_image(dest, descriptor, opts);
592    }
593
594    fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
595        self.push_blend_row(x, y, coverage);
596        self.inner.blend_row(x, y, color, coverage);
597    }
598
599    fn fill_masked(&mut self, rect: Rect, color: Color, mask: &dyn crate::mask::AlphaMask) {
600        self.push_rect(rect);
601        self.inner.fill_masked(rect, color, mask);
602    }
603
604    fn fill_gradient(&mut self, rect: Rect, gradient: &crate::draw::GradientDesc<'_>) {
605        self.push_rect(rect);
606        self.inner.fill_gradient(rect, gradient);
607    }
608
609    fn draw_shadow(&mut self, rect: Rect, radius: u8, shadow: &crate::draw::ShadowDesc) {
610        let spread = shadow.spread as i32;
611        let blur = shadow.blur as i32;
612        let base = Rect {
613            x: rect.x + shadow.offset_x as i32 - spread,
614            y: rect.y + shadow.offset_y as i32 - spread,
615            width: rect.width + spread * 2,
616            height: rect.height + spread * 2,
617        };
618        let draw_rect = Rect {
619            x: base.x - blur,
620            y: base.y - blur,
621            width: base.width + blur * 2,
622            height: base.height + blur * 2,
623        };
624        self.push_rect(draw_rect);
625        self.inner.draw_shadow(rect, radius, shadow);
626    }
627
628    fn fill_obb_aa(&mut self, obb: crate::raster::Obb, color: Color) {
629        self.push_rect(obb.aabb());
630        self.inner.fill_obb_aa(obb, color);
631    }
632
633    fn fill_disc_aa(&mut self, center: crate::raster::PointF, radius: f32, color: Color) {
634        let pad = radius + 1.0;
635        self.push_rect(Rect {
636            x: (center.x - pad) as i32 - 1,
637            y: (center.y - pad) as i32 - 1,
638            width: (pad * 2.0) as i32 + 3,
639            height: (pad * 2.0) as i32 + 3,
640        });
641        self.inner.fill_disc_aa(center, radius, color);
642    }
643
644    fn stroke_line_aa(
645        &mut self,
646        a: crate::raster::PointF,
647        b: crate::raster::PointF,
648        width: f32,
649        color: Color,
650    ) {
651        let pad = width * 0.5 + 1.0;
652        self.push_rect(Rect {
653            x: (a.x.min(b.x) - pad) as i32 - 1,
654            y: (a.y.min(b.y) - pad) as i32 - 1,
655            width: ((a.x.max(b.x) + pad) as i32 - (a.x.min(b.x) - pad) as i32) + 2,
656            height: ((a.y.max(b.y) + pad) as i32 - (a.y.min(b.y) - pad) as i32) + 2,
657        });
658        self.inner.stroke_line_aa(a, b, width, color);
659    }
660
661    fn fill_arc_aa(
662        &mut self,
663        center: crate::raster::PointF,
664        r_outer: f32,
665        r_inner: f32,
666        start_cos: f32,
667        start_sin: f32,
668        end_cos: f32,
669        end_sin: f32,
670        extent: f32,
671        color: Color,
672    ) {
673        let pad = r_outer + 1.0;
674        self.push_rect(Rect {
675            x: (center.x - pad) as i32 - 1,
676            y: (center.y - pad) as i32 - 1,
677            width: (pad * 2.0) as i32 + 3,
678            height: (pad * 2.0) as i32 + 3,
679        });
680        self.inner.fill_arc_aa(
681            center, r_outer, r_inner, start_cos, start_sin, end_cos, end_sin, extent, color,
682        );
683    }
684
685    fn submit(&mut self, list: &crate::cmd::CommandList) {
686        if let Some(dirty) = list.dirty_union() {
687            self.push_rect(dirty);
688        }
689        self.inner.submit(list);
690    }
691}
692
693fn draw_glyph_coverage<R: Renderer + ?Sized>(
694    renderer: &mut R,
695    font: &dyn crate::font::FontMetrics,
696    ch: char,
697    extent: Rect,
698    color: Color,
699) -> bool {
700    let mut coverage = [0u8; 64];
701    let height = extent.height.min(u16::MAX as i32) as u16;
702    let width = extent.width.min(u16::MAX as i32) as u16;
703    for row in 0..height {
704        let mut x_offset = 0u16;
705        while x_offset < width {
706            let run = (width - x_offset).min(coverage.len() as u16) as usize;
707            let row_coverage = &mut coverage[..run];
708            if !font.glyph_coverage_row(ch, row, x_offset, row_coverage) {
709                return false;
710            }
711            renderer.blend_row(
712                extent.x + i32::from(x_offset),
713                extent.y + i32::from(row),
714                color,
715                row_coverage,
716            );
717            x_offset += run as u16;
718        }
719    }
720    true
721}
722
723fn sample_image_pixel(
724    descriptor: &ImageDescriptor<'_>,
725    opts: &BlitOpts,
726    local_x: i32,
727    local_y: i32,
728) -> Option<Color> {
729    let (pre_x, pre_y) = inverse_cardinal_transform(local_x, local_y, opts);
730    if pre_x < 0 || pre_y < 0 {
731        return None;
732    }
733
734    let src_x = (i64::from(pre_x) * 256 / i64::from(opts.scale_x)) as u32;
735    let src_y = (i64::from(pre_y) * 256 / i64::from(opts.scale_y)) as u32;
736    if src_x >= u32::from(descriptor.width) || src_y >= u32::from(descriptor.height) {
737        return None;
738    }
739
740    decode_image_pixel(descriptor, src_x as usize, src_y as usize)
741        .map(|color| recolor_image_pixel(color, opts.recolor, opts.recolor_alpha))
742}
743
744fn inverse_cardinal_transform(local_x: i32, local_y: i32, opts: &BlitOpts) -> (i32, i32) {
745    let pivot_x = i32::from(opts.pivot.0);
746    let pivot_y = i32::from(opts.pivot.1);
747    let x = local_x - pivot_x;
748    let y = local_y - pivot_y;
749    let (x, y) = match nearest_cardinal_rotation(opts.rotation_deg) {
750        0 => (x, y),
751        90 => (y, -x),
752        180 => (-x, -y),
753        _ => (-y, x),
754    };
755    (x + pivot_x, y + pivot_y)
756}
757
758fn nearest_cardinal_rotation(degrees: i16) -> i16 {
759    let degrees = i32::from(degrees).rem_euclid(360);
760    (((degrees + 45) / 90 * 90) % 360) as i16
761}
762
763fn decode_image_pixel(descriptor: &ImageDescriptor<'_>, x: usize, y: usize) -> Option<Color> {
764    if let Some(colors) = descriptor.data.as_color_slice() {
765        return colors
766            .get(y.checked_mul(descriptor.width as usize)?.checked_add(x)?)
767            .copied();
768    }
769
770    let bytes = descriptor.data.as_bytes()?;
771    let stride = descriptor.stride_bytes() as usize;
772    let offset = y
773        .checked_mul(stride)?
774        .checked_add(x.checked_mul(descriptor.format.bytes_per_pixel() as usize)?)?;
775    match descriptor.format {
776        PixelFormat::Rgb565 => {
777            let raw = u16::from_le_bytes([*bytes.get(offset)?, *bytes.get(offset + 1)?]);
778            let r = (((raw >> 11) & 0x1f) as u32 * 255 / 31) as u8;
779            let g = (((raw >> 5) & 0x3f) as u32 * 255 / 63) as u8;
780            let b = ((raw & 0x1f) as u32 * 255 / 31) as u8;
781            Some(Color(r, g, b, 255))
782        }
783        PixelFormat::Argb8888 => {
784            let raw = u32::from_le_bytes([
785                *bytes.get(offset)?,
786                *bytes.get(offset + 1)?,
787                *bytes.get(offset + 2)?,
788                *bytes.get(offset + 3)?,
789            ]);
790            Some(Color(
791                ((raw >> 16) & 0xff) as u8,
792                ((raw >> 8) & 0xff) as u8,
793                (raw & 0xff) as u8,
794                ((raw >> 24) & 0xff) as u8,
795            ))
796        }
797        PixelFormat::L8 => bytes.get(offset).map(|&luma| Color(luma, luma, luma, 255)),
798    }
799}
800
801fn recolor_image_pixel(color: Color, recolor: Option<Color>, alpha: u8) -> Color {
802    let Some(tint) = recolor else {
803        return color;
804    };
805    if alpha == 0 {
806        return color;
807    }
808    Color(
809        lerp_channel(color.0, tint.0, alpha),
810        lerp_channel(color.1, tint.1, alpha),
811        lerp_channel(color.2, tint.2, alpha),
812        color.3,
813    )
814}
815
816fn lerp_channel(from: u8, to: u8, alpha: u8) -> u8 {
817    let value = i32::from(from) + (i32::from(to) - i32::from(from)) * i32::from(alpha) / 255;
818    value.clamp(0, 255) as u8
819}
820
821struct ShadowMask {
822    base: Rect,
823    blur: i32,
824    radius: i32,
825}
826
827impl AlphaMask for ShadowMask {
828    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
829        for (offset, alpha) in coverage.iter_mut().enumerate() {
830            let px = x.saturating_add(i32::try_from(offset).unwrap_or(i32::MAX));
831            *alpha = self.alpha(px, y);
832        }
833    }
834}
835
836impl ShadowMask {
837    fn alpha(&self, x: i32, y: i32) -> u8 {
838        let x0 = self.base.x;
839        let y0 = self.base.y;
840        let x1 = self.base.x + self.base.width - 1;
841        let y1 = self.base.y + self.base.height - 1;
842        let outside_dx = if x < x0 {
843            x0 - x
844        } else if x > x1 {
845            x - x1
846        } else {
847            0
848        };
849        let outside_dy = if y < y0 {
850            y0 - y
851        } else if y > y1 {
852            y - y1
853        } else {
854            0
855        };
856        let dist = outside_dx.max(outside_dy);
857        if dist > self.blur {
858            return 0;
859        }
860
861        let rect_alpha = if self.blur == 0 {
862            255
863        } else {
864            (((self.blur + 1 - dist) * 255) / (self.blur + 1)) as u8
865        };
866        rect_alpha.min(self.rounded_corner_alpha(x, y))
867    }
868
869    fn rounded_corner_alpha(&self, x: i32, y: i32) -> u8 {
870        let r = self
871            .radius
872            .max(0)
873            .min(self.base.width / 2)
874            .min(self.base.height / 2);
875        if r <= 0 {
876            return 255;
877        }
878
879        let cx = if x < self.base.x + r {
880            self.base.x + r
881        } else if x >= self.base.x + self.base.width - r {
882            self.base.x + self.base.width - r - 1
883        } else {
884            return 255;
885        };
886        let cy = if y < self.base.y + r {
887            self.base.y + r
888        } else if y >= self.base.y + self.base.height - r {
889            self.base.y + self.base.height - r - 1
890        } else {
891            return 255;
892        };
893        let dx = (x - cx).unsigned_abs();
894        let dy = (y - cy).unsigned_abs();
895        let dist_sq = dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy));
896        let r_sq = (r as u32).saturating_mul(r as u32);
897        if dist_sq <= r_sq { 255 } else { 0 }
898    }
899}
900
901// ───────────────────────────────────────────────────────────────────────────
902// ClipRenderer (REND initiative)
903// ───────────────────────────────────────────────────────────────────────────
904
905/// Nominal line height (pixels above the baseline anchor) used to gate
906/// backend [`Renderer::draw_text`] calls against a clip region. See
907/// `docs/concepts/REND-00-CONCEPTS.md` §5.4 for the guarantee scope.
908pub const TEXT_NOMINAL_LINE_PX: i32 = 16;
909
910/// Translating, clipping [`Renderer`] adapter (REND-00 §5).
911///
912/// Wraps any renderer, shifts every draw by `(dx, dy)` (content space →
913/// screen space), and crops it to a screen-space clip rect before
914/// forwarding. Containers that render children clipped to their own
915/// bounds — [`ScrollView`] being the canonical example — construct one of
916/// these around the incoming renderer in their `draw()`; backends need no
917/// changes and cannot opt out by accident.
918///
919/// Per-method semantics (normative — REND-00 §5.4):
920///
921/// - [`fill_rect`](Renderer::fill_rect) / [`blend_rect`](Renderer::blend_rect):
922///   forwarded as the translated intersection with the clip; dropped when
923///   disjoint.
924/// - [`draw_pixels`](Renderer::draw_pixels): only the visible row segments
925///   are forwarded (per-row source slice) — cropping never reads outside
926///   the source buffer.
927/// - [`blend_row`](Renderer::blend_row): the coverage run is sliced to the
928///   clip's horizontal span; rows outside the vertical span are dropped.
929///   The AA primitives (`fill_obb_aa`, `fill_disc_aa`, `stroke_line_aa`,
930///   `fill_arc_aa`), [`fill_masked`](Renderer::fill_masked),
931///   [`fill_gradient`](Renderer::fill_gradient),
932///   [`draw_shadow`](Renderer::draw_shadow), and [`submit`](Renderer::submit)
933///   are deliberately NOT forwarded to the wrapped renderer: the trait
934///   defaults route them through this adapter's clipped primitives. A
935///   forwarding override would hand the call to a backend fast path that knows
936///   nothing of the clip.
937/// - [`draw_text`](Renderer::draw_text) (backend text — no extent
938///   information): forwarded iff the nominal line box
939///   ([`TEXT_NOMINAL_LINE_PX`] above the baseline) sits fully inside the
940///   clip's vertical span and the anchor is inside the horizontal span;
941///   otherwise dropped. No vertical bleed, but partially-visible lines
942///   vanish rather than crop, and horizontal overflow is not cropped.
943///   Per-pixel text (`bitmap_font` / `packed_font`) renders through
944///   `fill_rect` and clips exactly on both axes — prefer it for content
945///   that can straddle a viewport edge.
946/// - [`draw_text_shaped`](Renderer::draw_text_shaped): routes through the
947///   default shaped-text renderer, so glyph coverage is clipped by
948///   [`blend_row`](Renderer::blend_row) and extent fallbacks are clipped by
949///   [`blend_rect`](Renderer::blend_rect). The wrapped renderer's own
950///   shaped-text fast path is deliberately not forwarded, so clipping cannot
951///   be bypassed by an accelerated backend.
952///
953/// Nesting two `ClipRenderer`s composes by intersection with summed
954/// offsets (REND-00 §5.5).
955///
956/// `clip` is in *screen* (wrapped-renderer) coordinates. Construction with
957/// a degenerate clip is allowed; every draw is then dropped.
958pub struct ClipRenderer<'a> {
959    inner: &'a mut dyn Renderer,
960    /// Translation applied to incoming draws (content space → screen).
961    dx: i32,
962    dy: i32,
963    /// Screen-space clip region; `None` means degenerate (drop all).
964    clip: Option<Rect>,
965}
966
967impl<'a> ClipRenderer<'a> {
968    /// Wrap `inner`, clipping to the screen-space `clip` rect with no
969    /// translation.
970    pub fn new(inner: &'a mut dyn Renderer, clip: Rect) -> Self {
971        Self::with_offset(inner, clip, 0, 0)
972    }
973
974    /// Wrap `inner`, translating incoming draws by `(dx, dy)` and clipping
975    /// the result to the screen-space `clip` rect.
976    pub fn with_offset(inner: &'a mut dyn Renderer, clip: Rect, dx: i32, dy: i32) -> Self {
977        let clip = (clip.width > 0 && clip.height > 0).then_some(clip);
978        Self {
979            inner,
980            dx,
981            dy,
982            clip,
983        }
984    }
985
986    /// The screen-space clip rect, or `None` when degenerate.
987    pub fn clip(&self) -> Option<Rect> {
988        self.clip
989    }
990}
991
992impl Renderer for ClipRenderer<'_> {
993    fn fill_rect(&mut self, rect: Rect, color: Color) {
994        let Some(clip) = self.clip else { return };
995        let moved = Rect {
996            x: rect.x + self.dx,
997            y: rect.y + self.dy,
998            ..rect
999        };
1000        if let Some(visible) = moved.intersect(clip) {
1001            self.inner.fill_rect(visible, color);
1002        }
1003    }
1004
1005    fn blend_rect(&mut self, rect: Rect, color: Color) {
1006        let Some(clip) = self.clip else { return };
1007        let moved = Rect {
1008            x: rect.x + self.dx,
1009            y: rect.y + self.dy,
1010            ..rect
1011        };
1012        if let Some(visible) = moved.intersect(clip) {
1013            self.inner.blend_rect(visible, color);
1014        }
1015    }
1016
1017    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
1018        let Some(clip) = self.clip else { return };
1019        let (x, y) = (position.0 + self.dx, position.1 + self.dy);
1020        // Nominal-line-box gating (REND-00 §5.4): vertical containment of
1021        // the line above the baseline, horizontal containment of the
1022        // anchor. Drop rather than bleed.
1023        let line_top = y - TEXT_NOMINAL_LINE_PX;
1024        let inside_v = line_top >= clip.y && y <= clip.y + clip.height;
1025        let inside_h = x >= clip.x && x < clip.x + clip.width;
1026        if inside_v && inside_h {
1027            self.inner.draw_text((x, y), text, color);
1028        }
1029    }
1030
1031    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
1032        let Some(clip) = self.clip else { return };
1033        if width == 0 || height == 0 {
1034            return;
1035        }
1036        let dest = Rect {
1037            x: position.0 + self.dx,
1038            y: position.1 + self.dy,
1039            width: width as i32,
1040            height: height as i32,
1041        };
1042        let Some(visible) = dest.intersect(clip) else {
1043            return;
1044        };
1045        // Fast path: fully visible — forward unchanged.
1046        if visible == dest {
1047            self.inner
1048                .draw_pixels((dest.x, dest.y), pixels, width, height);
1049            return;
1050        }
1051        // Partial: forward each visible row's segment as a 1-row blit.
1052        let col0 = (visible.x - dest.x) as u32;
1053        let run = visible.width as u32;
1054        for row in 0..visible.height {
1055            let src_row = (visible.y - dest.y + row) as u32;
1056            let start = (src_row * width + col0) as usize;
1057            let Some(slice) = pixels.get(start..start + run as usize) else {
1058                return;
1059            };
1060            self.inner
1061                .draw_pixels((visible.x, visible.y + row), slice, run, 1);
1062        }
1063    }
1064
1065    fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
1066        let Some(clip) = self.clip else { return };
1067        let (x, y) = (x + self.dx, y + self.dy);
1068        if y < clip.y || y >= clip.y + clip.height || coverage.is_empty() {
1069            return;
1070        }
1071        let row = Rect {
1072            x,
1073            y,
1074            width: coverage.len() as i32,
1075            height: 1,
1076        };
1077        let Some(visible) = row.intersect(clip) else {
1078            return;
1079        };
1080        let start = (visible.x - x) as usize;
1081        self.inner.blend_row(
1082            visible.x,
1083            y,
1084            color,
1085            &coverage[start..start + visible.width as usize],
1086        );
1087    }
1088
1089    // fill_obb_aa / fill_disc_aa / stroke_line_aa / fill_arc_aa /
1090    // fill_masked / fill_gradient / draw_shadow / submit:
1091    // intentionally NOT overridden (REND-00 §5.3). The trait defaults
1092    // funnel them through `blend_row` / per-cmd dispatch on *this*
1093    // adapter, which clips. Forwarding them to `inner` would bypass the
1094    // clip on backends with hardware fast paths.
1095}