Skip to main content

embedded_gui/
render.rs

1use core::marker::PhantomData;
2
3use embedded_graphics_core::{
4    Pixel,
5    draw_target::DrawTarget,
6    geometry::Point,
7    pixelcolor::{Rgb565, RgbColor},
8};
9
10#[cfg(not(feature = "std"))]
11use crate::math::F32Ext as _;
12use crate::{
13    font::{FontId, glyph_rows},
14    geometry::Rect,
15    image::{ImageFit, ImageRef, TileMode, TileRef},
16    palette::{DisplayPalette, InkRole},
17    style::{AlphaLinearGradient, AlphaRadialGradient, Border, GradientDirection, LinearGradient},
18    text,
19};
20
21pub const CHAR_WIDTH: u32 = 4;
22pub const CHAR_HEIGHT: u32 = 6;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum TextAlign {
26    Left,
27    Center,
28    Right,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum VerticalAlign {
33    Top,
34    Middle,
35    Bottom,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum TextWrap {
40    None,
41    Character,
42    Word,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum TextOverflow {
47    Clip,
48    Ellipsis,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum EllipsisMode {
53    ThreeDots,
54    SingleGlyph,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum TextOverflowPolicy {
59    Global(TextOverflow),
60    WrapThenEllipsis { max_lines: u8 },
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct TextStyle {
65    pub color: Rgb565,
66    pub font: FontId,
67    pub opacity: u8,
68    pub align: TextAlign,
69    pub vertical_align: VerticalAlign,
70    pub wrap: TextWrap,
71    pub overflow: TextOverflow,
72    pub overflow_policy: TextOverflowPolicy,
73    pub kerning: bool,
74    pub max_lines: Option<u8>,
75    pub ellipsis: EllipsisMode,
76    pub line_spacing: u8,
77}
78
79impl TextStyle {
80    pub const fn new(color: Rgb565) -> Self {
81        Self {
82            color,
83            font: FontId::Tiny3x5,
84            opacity: 255,
85            align: TextAlign::Left,
86            vertical_align: VerticalAlign::Top,
87            wrap: TextWrap::None,
88            overflow: TextOverflow::Clip,
89            overflow_policy: TextOverflowPolicy::Global(TextOverflow::Clip),
90            kerning: false,
91            max_lines: None,
92            ellipsis: EllipsisMode::ThreeDots,
93            line_spacing: 1,
94        }
95    }
96
97    pub const fn centered(mut self) -> Self {
98        self.align = TextAlign::Center;
99        self.vertical_align = VerticalAlign::Middle;
100        self
101    }
102
103    pub const fn with_align(mut self, align: TextAlign) -> Self {
104        self.align = align;
105        self
106    }
107
108    pub const fn with_vertical_align(mut self, align: VerticalAlign) -> Self {
109        self.vertical_align = align;
110        self
111    }
112
113    pub const fn with_wrap(mut self, wrap: TextWrap) -> Self {
114        self.wrap = wrap;
115        self
116    }
117
118    pub const fn with_line_spacing(mut self, spacing: u8) -> Self {
119        self.line_spacing = spacing;
120        self
121    }
122
123    pub const fn with_overflow(mut self, overflow: TextOverflow) -> Self {
124        self.overflow = overflow;
125        self.overflow_policy = TextOverflowPolicy::Global(overflow);
126        self
127    }
128
129    pub const fn with_kerning(mut self, kerning: bool) -> Self {
130        self.kerning = kerning;
131        self
132    }
133
134    pub const fn with_max_lines(mut self, max_lines: Option<u8>) -> Self {
135        self.max_lines = max_lines;
136        self
137    }
138
139    pub const fn with_ellipsis_mode(mut self, ellipsis: EllipsisMode) -> Self {
140        self.ellipsis = ellipsis;
141        self
142    }
143
144    pub const fn with_overflow_policy(mut self, policy: TextOverflowPolicy) -> Self {
145        self.overflow_policy = policy;
146        self
147    }
148
149    pub const fn with_opacity(mut self, opacity: u8) -> Self {
150        self.opacity = opacity;
151        self
152    }
153
154    pub const fn with_font_id(mut self, font: FontId) -> Self {
155        self.font = font;
156        self
157    }
158
159    pub fn with_font(mut self, font: impl Into<FontId>) -> Self {
160        self.font = font.into();
161        self
162    }
163}
164
165#[cfg(feature = "embedded-graphics")]
166impl From<&embedded_graphics::mono_font::MonoTextStyle<'static, Rgb565>> for TextStyle {
167    fn from(mono_style: &embedded_graphics::mono_font::MonoTextStyle<'static, Rgb565>) -> Self {
168        let mut style = TextStyle::new(mono_style.text_color.unwrap_or(Rgb565::WHITE));
169        style.font = FontId::MonoFont(mono_style.font);
170        style
171    }
172}
173
174#[cfg(feature = "embedded-graphics")]
175impl From<embedded_graphics::mono_font::MonoTextStyle<'static, Rgb565>> for TextStyle {
176    fn from(mono_style: embedded_graphics::mono_font::MonoTextStyle<'static, Rgb565>) -> Self {
177        Self::from(&mono_style)
178    }
179}
180
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182pub struct TextMetrics {
183    pub width: u32,
184    pub height: u32,
185}
186
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub enum RenderQuality {
189    Low,
190    Medium,
191    High,
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub enum AntiAliasMode {
196    None,
197    Coverage,
198    Subpixel,
199}
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202pub struct StrokeStyle {
203    pub color: Rgb565,
204    pub width: u8,
205    pub antialias: bool,
206    pub antialias_mode: AntiAliasMode,
207    pub cap: StrokeCap,
208    pub join: StrokeJoin,
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum StrokeCap {
213    Butt,
214    Round,
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218pub enum StrokeJoin {
219    Miter,
220    Round,
221}
222
223#[derive(Clone, Copy, Debug, PartialEq)]
224pub struct Transform2D {
225    pub m11: f32,
226    pub m12: f32,
227    pub m21: f32,
228    pub m22: f32,
229    pub tx: f32,
230    pub ty: f32,
231}
232
233impl Transform2D {
234    pub const IDENTITY: Self = Self {
235        m11: 1.0,
236        m12: 0.0,
237        m21: 0.0,
238        m22: 1.0,
239        tx: 0.0,
240        ty: 0.0,
241    };
242
243    pub const fn translation(x: f32, y: f32) -> Self {
244        Self {
245            tx: x,
246            ty: y,
247            ..Self::IDENTITY
248        }
249    }
250
251    pub const fn scale(x: f32, y: f32) -> Self {
252        Self {
253            m11: x,
254            m22: y,
255            ..Self::IDENTITY
256        }
257    }
258
259    pub fn rotation(deg: f32) -> Self {
260        let r = deg.to_radians();
261        Self {
262            m11: r.cos(),
263            m12: -r.sin(),
264            m21: r.sin(),
265            m22: r.cos(),
266            ..Self::IDENTITY
267        }
268    }
269
270    pub fn skew(x_deg: f32, y_deg: f32) -> Self {
271        Self {
272            m12: x_deg.to_radians().tan(),
273            m21: y_deg.to_radians().tan(),
274            ..Self::IDENTITY
275        }
276    }
277
278    pub fn then(self, rhs: Self) -> Self {
279        Self {
280            m11: self.m11 * rhs.m11 + self.m12 * rhs.m21,
281            m12: self.m11 * rhs.m12 + self.m12 * rhs.m22,
282            m21: self.m21 * rhs.m11 + self.m22 * rhs.m21,
283            m22: self.m21 * rhs.m12 + self.m22 * rhs.m22,
284            tx: self.m11 * rhs.tx + self.m12 * rhs.ty + self.tx,
285            ty: self.m21 * rhs.tx + self.m22 * rhs.ty + self.ty,
286        }
287    }
288
289    #[inline(always)]
290    pub fn is_identity(self) -> bool {
291        self.m11 == 1.0
292            && self.m12 == 0.0
293            && self.m21 == 0.0
294            && self.m22 == 1.0
295            && self.tx == 0.0
296            && self.ty == 0.0
297    }
298
299    #[inline(always)]
300    pub fn apply(self, x: i32, y: i32) -> (i32, i32) {
301        if self.is_identity() {
302            (x, y)
303        } else {
304            let xf = x as f32;
305            let yf = y as f32;
306            (
307                (self.m11 * xf + self.m12 * yf + self.tx).round() as i32,
308                (self.m21 * xf + self.m22 * yf + self.ty).round() as i32,
309            )
310        }
311    }
312
313    #[inline(always)]
314    pub fn apply_f32(self, x: f32, y: f32) -> (f32, f32) {
315        if self.is_identity() {
316            (x, y)
317        } else {
318            (
319                self.m11 * x + self.m12 * y + self.tx,
320                self.m21 * x + self.m22 * y + self.ty,
321            )
322        }
323    }
324
325    pub fn inverse(self) -> Option<Self> {
326        let det = self.m11 * self.m22 - self.m12 * self.m21;
327        if det.abs() < 1e-7 {
328            return None;
329        }
330        let inv_det = 1.0 / det;
331        let m11 = self.m22 * inv_det;
332        let m12 = -self.m12 * inv_det;
333        let m21 = -self.m21 * inv_det;
334        let m22 = self.m11 * inv_det;
335        let tx = (self.m12 * self.ty - self.m22 * self.tx) * inv_det;
336        let ty = (self.m21 * self.tx - self.m11 * self.ty) * inv_det;
337        Some(Self {
338            m11,
339            m12,
340            m21,
341            m22,
342            tx,
343            ty,
344        })
345    }
346}
347
348#[derive(Clone, Copy, Debug, PartialEq, Eq)]
349pub enum BlendMode {
350    Normal,
351    Add,
352    Multiply,
353    Screen,
354}
355
356/// Capability trait for draw targets that can read back a pixel they've
357/// already written. Optional: the default opacity/blend APIs on
358/// [`RenderCtx`] only require [`DrawTarget`] and approximate translucency
359/// with ordered dithering, so they keep working on write-only displays.
360/// Implementing `PixelRead` additionally unlocks the `*_true_alpha` methods,
361/// which composite against the destination's actual current contents
362/// instead of dithering.
363///
364/// Re-exported from [`embedded_draw_target`] so that a buffer implementing it
365/// once is accepted by every crate in the ecosystem that needs readback,
366/// rather than by this one alone.
367pub use embedded_draw_target::PixelRead;
368
369/// Capability trait for hardware display controllers (e.g. ST7789, ILI9341, SSD1306)
370/// supporting direct column/row address window setting (`set_address_window`).
371/// Allows rendering dirty regions by transmitting SPI/DMA transfers exclusively to
372/// the target sub-window instead of the full screen.
373///
374/// Re-exported from [`embedded_draw_target`].
375pub use embedded_draw_target::WindowedDrawTarget;
376
377/// Pixel-plotting policy for a [`RenderCtx`]. Selected by the ctx's `C` type
378/// parameter so the *same* drawing calls composite differently depending on
379/// the target's capabilities — with no runtime branch and no specialization.
380///
381/// - [`Dither`] (the default) approximates translucency with ordered dithering
382///   and works on any write-only [`DrawTarget`].
383/// - [`Blend`] performs true per-pixel alpha compositing and requires a
384///   readback-capable target ([`PixelRead`]), e.g. a
385///   [`Framebuffer`](crate::Framebuffer).
386///
387/// `plot` receives screen-space coords already transformed and clipped, the
388/// layer-combined `opacity`, and the active layer blend mode + backdrop.
389pub trait Compositor<D: DrawTarget<Color = Rgb565>> {
390    fn plot(
391        target: &mut D,
392        x: i32,
393        y: i32,
394        color: Rgb565,
395        opacity: u8,
396        blend: BlendMode,
397        backdrop: Rgb565,
398    ) -> Result<(), D::Error>;
399}
400
401/// Ordered-dither compositor (default). No readback required.
402#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
403pub struct Dither;
404
405/// True alpha-blending compositor. Requires a [`PixelRead`] target.
406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
407pub struct Blend;
408
409impl<D: DrawTarget<Color = Rgb565>> Compositor<D> for Dither {
410    fn plot(
411        target: &mut D,
412        x: i32,
413        y: i32,
414        color: Rgb565,
415        opacity: u8,
416        blend: BlendMode,
417        backdrop: Rgb565,
418    ) -> Result<(), D::Error> {
419        if !should_draw_at_opacity(x, y, opacity) {
420            return Ok(());
421        }
422        let color = apply_blend_mode(color, blend, backdrop);
423        target.draw_iter([Pixel(Point::new(x, y), color)])
424    }
425}
426
427impl<D: DrawTarget<Color = Rgb565> + PixelRead> Compositor<D> for Blend {
428    fn plot(
429        target: &mut D,
430        x: i32,
431        y: i32,
432        color: Rgb565,
433        opacity: u8,
434        blend: BlendMode,
435        backdrop: Rgb565,
436    ) -> Result<(), D::Error> {
437        if opacity == 0 {
438            return Ok(());
439        }
440        let bg = target.get_pixel(Point::new(x, y));
441        let blended = lerp_rgb565(bg, color, opacity);
442        let blended = apply_blend_mode(blended, blend, backdrop);
443        target.draw_iter([Pixel(Point::new(x, y), blended)])
444    }
445}
446
447#[derive(Clone, Copy, Debug, PartialEq, Eq)]
448pub enum ColorFormat {
449    Rgb565,
450    Rgb888,
451    Argb8888,
452}
453
454#[derive(Clone, Copy, Debug, PartialEq, Eq)]
455pub struct RenderBackendCaps {
456    pub color_format: ColorFormat,
457    pub supports_layers: bool,
458    pub supports_subpixel: bool,
459}
460
461impl RenderBackendCaps {
462    pub const fn software_rgb565() -> Self {
463        Self {
464            color_format: ColorFormat::Rgb565,
465            supports_layers: true,
466            supports_subpixel: false,
467        }
468    }
469}
470
471#[derive(Clone, Copy, Debug, PartialEq, Eq)]
472pub struct LayerState {
473    pub opacity: u8,
474    pub blend: BlendMode,
475    pub backdrop: Rgb565,
476}
477
478impl LayerState {
479    pub const fn normal() -> Self {
480        Self {
481            opacity: 255,
482            blend: BlendMode::Normal,
483            backdrop: Rgb565::BLACK,
484        }
485    }
486}
487
488impl StrokeStyle {
489    pub const fn new(color: Rgb565) -> Self {
490        Self {
491            color,
492            width: 1,
493            antialias: false,
494            antialias_mode: AntiAliasMode::None,
495            cap: StrokeCap::Butt,
496            join: StrokeJoin::Miter,
497        }
498    }
499
500    pub const fn with_width(mut self, width: u8) -> Self {
501        self.width = if width == 0 { 1 } else { width };
502        self
503    }
504
505    pub const fn with_antialias(mut self, antialias: bool) -> Self {
506        self.antialias = antialias;
507        if antialias {
508            if let AntiAliasMode::None = self.antialias_mode {
509                self.antialias_mode = AntiAliasMode::Coverage;
510            }
511        }
512        if !antialias {
513            self.antialias_mode = AntiAliasMode::None;
514        }
515        self
516    }
517
518    pub const fn with_antialias_mode(mut self, mode: AntiAliasMode) -> Self {
519        self.antialias_mode = mode;
520        self.antialias = !matches!(mode, AntiAliasMode::None);
521        self
522    }
523
524    pub const fn with_cap(mut self, cap: StrokeCap) -> Self {
525        self.cap = cap;
526        self
527    }
528
529    pub const fn with_join(mut self, join: StrokeJoin) -> Self {
530        self.join = join;
531        self
532    }
533}
534
535pub struct RenderCtx<'a, D, C = Dither>
536where
537    D: DrawTarget<Color = Rgb565>,
538{
539    target: &'a mut D,
540    clip: Rect,
541    dirty: Option<Rect>,
542    quality: RenderQuality,
543    backend_caps: RenderBackendCaps,
544    transform_stack: [Transform2D; 8],
545    transform_len: usize,
546    layer_stack: [LayerState; 8],
547    layer_len: usize,
548    palette: Option<DisplayPalette>,
549    _compositor: PhantomData<C>,
550}
551
552impl<'a, D> RenderCtx<'a, D, Dither>
553where
554    D: DrawTarget<Color = Rgb565>,
555{
556    pub fn new(target: &'a mut D, viewport: Rect) -> Self {
557        Self {
558            target,
559            clip: viewport,
560            dirty: None,
561            quality: RenderQuality::High,
562            backend_caps: RenderBackendCaps::software_rgb565(),
563            transform_stack: [Transform2D::IDENTITY; 8],
564            transform_len: 1,
565            layer_stack: [LayerState::normal(); 8],
566            layer_len: 1,
567            palette: None,
568            _compositor: PhantomData,
569        }
570    }
571
572    pub fn with_palette(mut self, palette: DisplayPalette) -> Self {
573        self.palette = Some(palette);
574        self
575    }
576
577    /// Resolve a semantic ink role through the active palette, if set.
578    pub fn ink(&self, role: InkRole) -> Rgb565 {
579        self.palette
580            .map(|palette| palette.resolve(role))
581            .unwrap_or(Rgb565::WHITE)
582    }
583
584    pub fn with_dirty(target: &'a mut D, viewport: Rect, dirty: Rect) -> Self {
585        Self {
586            target,
587            clip: viewport,
588            dirty: Some(dirty),
589            quality: RenderQuality::High,
590            backend_caps: RenderBackendCaps::software_rgb565(),
591            transform_stack: [Transform2D::IDENTITY; 8],
592            transform_len: 1,
593            layer_stack: [LayerState::normal(); 8],
594            layer_len: 1,
595            palette: None,
596            _compositor: PhantomData,
597        }
598    }
599}
600
601impl<'a, D> RenderCtx<'a, D, Blend>
602where
603    D: DrawTarget<Color = Rgb565> + PixelRead,
604{
605    /// Like [`RenderCtx::new`], but every drawing call alpha-composites against
606    /// the target's current contents (true blending) instead of dithering.
607    /// Requires a readback-capable target ([`PixelRead`]), e.g. a
608    /// [`Framebuffer`](crate::Framebuffer).
609    pub fn compositing(target: &'a mut D, viewport: Rect) -> Self {
610        Self {
611            target,
612            clip: viewport,
613            dirty: None,
614            quality: RenderQuality::High,
615            backend_caps: RenderBackendCaps::software_rgb565(),
616            transform_stack: [Transform2D::IDENTITY; 8],
617            transform_len: 1,
618            layer_stack: [LayerState::normal(); 8],
619            layer_len: 1,
620            palette: None,
621            _compositor: PhantomData,
622        }
623    }
624
625    /// Apply Fast IIR Blur to a sub-region `rect` on the destination target.
626    pub fn blur_rect(&mut self, rect: Rect, blur_degree: u8) -> Result<(), D::Error> {
627        let draw = self.visible_rect(rect);
628        if draw.is_empty() || blur_degree == 0 {
629            return Ok(());
630        }
631        let x0 = draw.x;
632        let y0 = draw.y;
633        let x1 = draw.right();
634        let y1 = draw.bottom();
635        let alpha = 256 - (blur_degree as i32);
636
637        let mut row_buf = [Rgb565::BLACK; 1024];
638        let w_buf = ((x1 - x0) as usize).min(1024);
639
640        // Horizontal forward & reverse passes (row by row)
641        for y in y0..y1 {
642            let r_len = ((x1 - x0) as usize).min(w_buf);
643            if r_len == 0 {
644                continue;
645            }
646            for (i, x) in (x0..x1).take(r_len).enumerate() {
647                row_buf[i] = self.target.get_pixel(Point::new(x, y));
648            }
649
650            // Forward H pass
651            let p0 = row_buf[0];
652            let (r5, g6, b5) = (p0.r(), p0.g(), p0.b());
653            let mut acc_r = (((r5 << 3) | (r5 >> 2)) as i32) << 8;
654            let mut acc_g = (((g6 << 2) | (g6 >> 4)) as i32) << 8;
655            let mut acc_b = (((b5 << 3) | (b5 >> 2)) as i32) << 8;
656
657            for p in row_buf[..r_len].iter_mut() {
658                let (r5, g6, b5) = (p.r(), p.g(), p.b());
659                let r8 = ((r5 << 3) | (r5 >> 2)) as i32;
660                let g8 = ((g6 << 2) | (g6 >> 4)) as i32;
661                let b8 = ((b5 << 3) | (b5 >> 2)) as i32;
662                acc_r += (((r8 << 8) - acc_r) * alpha) >> 8;
663                acc_g += (((g8 << 8) - acc_g) * alpha) >> 8;
664                acc_b += (((b8 << 8) - acc_b) * alpha) >> 8;
665                *p = Rgb565::new(
666                    ((acc_r >> 8).clamp(0, 255) as u8) >> 3,
667                    ((acc_g >> 8).clamp(0, 255) as u8) >> 2,
668                    ((acc_b >> 8).clamp(0, 255) as u8) >> 3,
669                );
670            }
671
672            // Reverse H pass
673            let p_last = row_buf[r_len - 1];
674            let (r5, g6, b5) = (p_last.r(), p_last.g(), p_last.b());
675            let mut acc_r = (((r5 << 3) | (r5 >> 2)) as i32) << 8;
676            let mut acc_g = (((g6 << 2) | (g6 >> 4)) as i32) << 8;
677            let mut acc_b = (((b5 << 3) | (b5 >> 2)) as i32) << 8;
678
679            for p in row_buf[..r_len].iter_mut().rev() {
680                let (r5, g6, b5) = (p.r(), p.g(), p.b());
681                let r8 = ((r5 << 3) | (r5 >> 2)) as i32;
682                let g8 = ((g6 << 2) | (g6 >> 4)) as i32;
683                let b8 = ((b5 << 3) | (b5 >> 2)) as i32;
684                acc_r += (((r8 << 8) - acc_r) * alpha) >> 8;
685                acc_g += (((g8 << 8) - acc_g) * alpha) >> 8;
686                acc_b += (((b8 << 8) - acc_b) * alpha) >> 8;
687                *p = Rgb565::new(
688                    ((acc_r >> 8).clamp(0, 255) as u8) >> 3,
689                    ((acc_g >> 8).clamp(0, 255) as u8) >> 2,
690                    ((acc_b >> 8).clamp(0, 255) as u8) >> 3,
691                );
692            }
693
694            for (i, x) in (x0..x1).take(r_len).enumerate() {
695                self.target
696                    .draw_iter([Pixel(Point::new(x, y), row_buf[i])])?;
697            }
698        }
699
700        // Vertical forward & reverse passes (column by column)
701        let mut col_buf = [Rgb565::BLACK; 1024];
702        let h_buf = ((y1 - y0) as usize).min(1024);
703
704        for x in x0..x1 {
705            let c_len = ((y1 - y0) as usize).min(h_buf);
706            if c_len == 0 {
707                continue;
708            }
709            for (i, y) in (y0..y1).take(c_len).enumerate() {
710                col_buf[i] = self.target.get_pixel(Point::new(x, y));
711            }
712
713            // Forward V pass
714            let p0 = col_buf[0];
715            let (r5, g6, b5) = (p0.r(), p0.g(), p0.b());
716            let mut acc_r = (((r5 << 3) | (r5 >> 2)) as i32) << 8;
717            let mut acc_g = (((g6 << 2) | (g6 >> 4)) as i32) << 8;
718            let mut acc_b = (((b5 << 3) | (b5 >> 2)) as i32) << 8;
719
720            for p in col_buf[..c_len].iter_mut() {
721                let (r5, g6, b5) = (p.r(), p.g(), p.b());
722                let r8 = ((r5 << 3) | (r5 >> 2)) as i32;
723                let g8 = ((g6 << 2) | (g6 >> 4)) as i32;
724                let b8 = ((b5 << 3) | (b5 >> 2)) as i32;
725                acc_r += (((r8 << 8) - acc_r) * alpha) >> 8;
726                acc_g += (((g8 << 8) - acc_g) * alpha) >> 8;
727                acc_b += (((b8 << 8) - acc_b) * alpha) >> 8;
728                *p = Rgb565::new(
729                    ((acc_r >> 8).clamp(0, 255) as u8) >> 3,
730                    ((acc_g >> 8).clamp(0, 255) as u8) >> 2,
731                    ((acc_b >> 8).clamp(0, 255) as u8) >> 3,
732                );
733            }
734
735            // Reverse V pass
736            let p_last = col_buf[c_len - 1];
737            let (r5, g6, b5) = (p_last.r(), p_last.g(), p_last.b());
738            let mut acc_r = (((r5 << 3) | (r5 >> 2)) as i32) << 8;
739            let mut acc_g = (((g6 << 2) | (g6 >> 4)) as i32) << 8;
740            let mut acc_b = (((b5 << 3) | (b5 >> 2)) as i32) << 8;
741
742            for p in col_buf[..c_len].iter_mut().rev() {
743                let (r5, g6, b5) = (p.r(), p.g(), p.b());
744                let r8 = ((r5 << 3) | (r5 >> 2)) as i32;
745                let g8 = ((g6 << 2) | (g6 >> 4)) as i32;
746                let b8 = ((b5 << 3) | (b5 >> 2)) as i32;
747                acc_r += (((r8 << 8) - acc_r) * alpha) >> 8;
748                acc_g += (((g8 << 8) - acc_g) * alpha) >> 8;
749                acc_b += (((b8 << 8) - acc_b) * alpha) >> 8;
750                *p = Rgb565::new(
751                    ((acc_r >> 8).clamp(0, 255) as u8) >> 3,
752                    ((acc_g >> 8).clamp(0, 255) as u8) >> 2,
753                    ((acc_b >> 8).clamp(0, 255) as u8) >> 3,
754                );
755            }
756
757            for (i, y) in (y0..y1).take(c_len).enumerate() {
758                self.target
759                    .draw_iter([Pixel(Point::new(x, y), col_buf[i])])?;
760            }
761        }
762
763        Ok(())
764    }
765
766    /// Apply reverse colour (color inversion) filter on `rect` (PixelRead target).
767    pub fn reverse_colour_rect(&mut self, rect: Rect) -> Result<(), D::Error> {
768        let bounds = self.clip.intersection(rect);
769        if bounds.is_empty() {
770            return Ok(());
771        }
772        let x0 = bounds.x;
773        let y0 = bounds.y;
774        let x1 = bounds.right();
775        let y1 = bounds.bottom();
776
777        for y in y0..y1 {
778            for x in x0..x1 {
779                let pt = Point::new(x, y);
780                let c = self.target.get_pixel(pt);
781                let inv = Rgb565::new(31 - c.r(), 63 - c.g(), 31 - c.b());
782                self.target.draw_iter([Pixel(pt, inv)])?;
783            }
784        }
785        Ok(())
786    }
787
788    /// Fill `rect` using a horizontal 1D line mask array.
789    pub fn fill_rect_horizontal_line_mask(
790        &mut self,
791        rect: Rect,
792        mask: &[u8],
793        color: Rgb565,
794        opacity: u8,
795    ) -> Result<(), D::Error> {
796        if mask.is_empty() || opacity == 0 {
797            return Ok(());
798        }
799        let bounds = self.clip.intersection(rect);
800        if bounds.is_empty() {
801            return Ok(());
802        }
803
804        for y in bounds.y..bounds.bottom() {
805            for x in bounds.x..bounds.right() {
806                let mask_x = ((x - rect.x) as usize) % mask.len();
807                let alpha = ((mask[mask_x] as u32 * opacity as u32) >> 8) as u8;
808                if alpha == 0 {
809                    continue;
810                }
811                let pt = Point::new(x, y);
812                let bg = self.target.get_pixel(pt);
813                let blended = lerp_rgb565(bg, color, alpha);
814                self.target.draw_iter([Pixel(pt, blended)])?;
815            }
816        }
817        Ok(())
818    }
819
820    /// Fill `rect` using a vertical 1D line mask array.
821    pub fn fill_rect_vertical_line_mask(
822        &mut self,
823        rect: Rect,
824        mask: &[u8],
825        color: Rgb565,
826        opacity: u8,
827    ) -> Result<(), D::Error> {
828        if mask.is_empty() || opacity == 0 {
829            return Ok(());
830        }
831        let bounds = self.clip.intersection(rect);
832        if bounds.is_empty() {
833            return Ok(());
834        }
835
836        for y in bounds.y..bounds.bottom() {
837            let mask_y = ((y - rect.y) as usize) % mask.len();
838            let alpha = ((mask[mask_y] as u32 * opacity as u32) >> 8) as u8;
839            if alpha == 0 {
840                continue;
841            }
842            for x in bounds.x..bounds.right() {
843                let pt = Point::new(x, y);
844                let bg = self.target.get_pixel(pt);
845                let blended = lerp_rgb565(bg, color, alpha);
846                self.target.draw_iter([Pixel(pt, blended)])?;
847            }
848        }
849        Ok(())
850    }
851}
852
853impl<'a, D, C> RenderCtx<'a, D, C>
854where
855    D: DrawTarget<Color = Rgb565>,
856    C: Compositor<D>,
857{
858    pub const fn clip(&self) -> Rect {
859        self.clip
860    }
861
862    pub fn set_clip(&mut self, clip: Rect) {
863        self.clip = clip;
864    }
865
866    /// Draws any `embedded_graphics::Drawable` (e.g. an `embedded_text::TextBox`
867    /// built via [`crate::interop::text::text_box`], or an arranged
868    /// `embedded_layout` view group) onto this context's target, clipped to
869    /// the current [`clip`](Self::clip) rect.
870    #[cfg(any(
871        feature = "embedded-text",
872        feature = "embedded-layout",
873        feature = "embedded-graphics"
874    ))]
875    pub fn draw_embedded_graphics<T>(&mut self, drawable: &T) -> Result<T::Output, D::Error>
876    where
877        T: embedded_graphics::Drawable<Color = Rgb565>,
878    {
879        use embedded_graphics::draw_target::DrawTargetExt;
880        use embedded_graphics::geometry::{Point, Size};
881        use embedded_graphics::primitives::Rectangle;
882
883        let clip_rect = Rectangle::new(
884            Point::new(self.clip.x, self.clip.y),
885            Size::new(self.clip.w, self.clip.h),
886        );
887        let mut clipped = self.target.clipped(&clip_rect);
888        drawable.draw(&mut clipped)
889    }
890
891    pub const fn quality(&self) -> RenderQuality {
892        self.quality
893    }
894
895    pub fn set_quality(&mut self, quality: RenderQuality) {
896        self.quality = quality;
897    }
898
899    pub const fn backend_caps(&self) -> RenderBackendCaps {
900        self.backend_caps
901    }
902
903    pub fn set_backend_caps(&mut self, caps: RenderBackendCaps) {
904        self.backend_caps = caps;
905    }
906
907    pub fn push_transform(&mut self, transform: Transform2D) {
908        if self.transform_len >= self.transform_stack.len() {
909            return;
910        }
911        let current = self.current_transform();
912        self.transform_stack[self.transform_len] = current.then(transform);
913        self.transform_len += 1;
914    }
915
916    pub fn pop_transform(&mut self) {
917        if self.transform_len > 1 {
918            self.transform_len -= 1;
919        }
920    }
921
922    pub fn translate(&mut self, x: f32, y: f32) {
923        self.push_transform(Transform2D::translation(x, y));
924    }
925
926    pub fn scale(&mut self, x: f32, y: f32) {
927        self.push_transform(Transform2D::scale(x, y));
928    }
929
930    pub fn rotate(&mut self, deg: f32) {
931        self.push_transform(Transform2D::rotation(deg));
932    }
933
934    pub fn skew(&mut self, x_deg: f32, y_deg: f32) {
935        self.push_transform(Transform2D::skew(x_deg, y_deg));
936    }
937
938    pub fn push_layer(&mut self, layer: LayerState) {
939        if self.layer_len >= self.layer_stack.len() {
940            return;
941        }
942        let current = self.current_layer();
943        self.layer_stack[self.layer_len] = LayerState {
944            opacity: ((current.opacity as u16 * layer.opacity as u16) / 255) as u8,
945            blend: layer.blend,
946            backdrop: layer.backdrop,
947        };
948        self.layer_len += 1;
949    }
950
951    pub fn pop_layer(&mut self) {
952        if self.layer_len > 1 {
953            self.layer_len -= 1;
954        }
955    }
956
957    pub const fn shadow_spread_for(&self, spread: u8) -> u8 {
958        match self.quality {
959            RenderQuality::Low => 0,
960            RenderQuality::Medium => {
961                if spread > 1 {
962                    1
963                } else {
964                    spread
965                }
966            }
967            RenderQuality::High => spread,
968        }
969    }
970
971    pub fn fill_rect(&mut self, rect: impl Into<Rect>, color: Rgb565) -> Result<(), D::Error> {
972        self.fill_rect_alpha(rect, color, 255)
973    }
974
975    pub fn fill_rect_alpha(
976        &mut self,
977        rect: impl Into<Rect>,
978        color: Rgb565,
979        opacity: u8,
980    ) -> Result<(), D::Error> {
981        self.fill_rounded_rect_alpha(rect, 0, color, opacity)
982    }
983
984    pub fn fill_rounded_rect(
985        &mut self,
986        rect: impl Into<Rect>,
987        radius: u8,
988        color: Rgb565,
989    ) -> Result<(), D::Error> {
990        self.fill_rounded_rect_alpha(rect, radius, color, 255)
991    }
992
993    pub fn fill_rounded_rect_alpha(
994        &mut self,
995        rect: impl Into<Rect>,
996        radius: u8,
997        color: Rgb565,
998        opacity: u8,
999    ) -> Result<(), D::Error> {
1000        let rect = rect.into();
1001        let draw = self.visible_rect(rect);
1002        if draw.is_empty() || opacity == 0 {
1003            return Ok(());
1004        }
1005        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
1006
1007        let layer = self.current_layer();
1008        let combined_opacity = ((opacity as u16 * layer.opacity as u16) / 255) as u8;
1009
1010        // Fast path for solid un-transformed rectangular fills:
1011        // Leverages hardware fill_solid on the display target instead of per-pixel loops
1012        if radius == 0
1013            && combined_opacity == 255
1014            && self.current_transform().is_identity()
1015            && layer.blend == BlendMode::Normal
1016        {
1017            let eg_rect = embedded_graphics_core::primitives::Rectangle::new(
1018                embedded_graphics_core::geometry::Point::new(draw.x, draw.y),
1019                embedded_graphics_core::geometry::Size::new(draw.w, draw.h),
1020            );
1021            return self.target.fill_solid(&eg_rect, color);
1022        }
1023
1024        for y in draw.y..draw.bottom() {
1025            for x in draw.x..draw.right() {
1026                if !in_rounded_rect(x, y, rect, radius) {
1027                    continue;
1028                }
1029                self.pixel(x, y, color, opacity)?;
1030            }
1031        }
1032        Ok(())
1033    }
1034
1035    pub fn fill_rounded_rect_gradient_alpha(
1036        &mut self,
1037        rect: impl Into<Rect>,
1038        radius: u8,
1039        gradient: LinearGradient,
1040        opacity: u8,
1041    ) -> Result<(), D::Error> {
1042        let rect = rect.into();
1043        let draw = self.visible_rect(rect);
1044        if draw.is_empty() || opacity == 0 {
1045            return Ok(());
1046        }
1047        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
1048        let denom = match gradient.direction {
1049            GradientDirection::Horizontal => rect.w.saturating_sub(1).max(1),
1050            GradientDirection::Vertical => rect.h.saturating_sub(1).max(1),
1051        };
1052
1053        for y in draw.y..draw.bottom() {
1054            for x in draw.x..draw.right() {
1055                if !in_rounded_rect(x, y, rect, radius) {
1056                    continue;
1057                }
1058                let numer = match gradient.direction {
1059                    GradientDirection::Horizontal => (x - rect.x).max(0) as u32,
1060                    GradientDirection::Vertical => (y - rect.y).max(0) as u32,
1061                }
1062                .min(denom);
1063                let mut t = ((numer * 255) / denom) as u8;
1064                t = match self.quality {
1065                    RenderQuality::Low => 128,
1066                    RenderQuality::Medium => (t / 64) * 64,
1067                    RenderQuality::High => t,
1068                };
1069                let color = lerp_rgb565(gradient.start, gradient.end, t);
1070                self.pixel(x, y, color, opacity)?;
1071            }
1072        }
1073        Ok(())
1074    }
1075
1076    pub fn stroke_rect(&mut self, rect: impl Into<Rect>, border: Border) -> Result<(), D::Error> {
1077        self.stroke_rect_alpha(rect, border, 255)
1078    }
1079
1080    pub fn stroke_rect_alpha(
1081        &mut self,
1082        rect: impl Into<Rect>,
1083        border: Border,
1084        opacity: u8,
1085    ) -> Result<(), D::Error> {
1086        let rect = rect.into();
1087        if border.width == 0 || rect.is_empty() {
1088            return Ok(());
1089        }
1090
1091        for i in 0..border.width as i32 {
1092            let w = rect.w.saturating_sub((i as u32).saturating_mul(2));
1093            let h = rect.h.saturating_sub((i as u32).saturating_mul(2));
1094            if w == 0 || h == 0 {
1095                break;
1096            }
1097            let r = Rect::new(rect.x + i, rect.y + i, w, h);
1098            self.fill_rect_alpha(Rect::new(r.x, r.y, r.w, 1), border.color, opacity)?;
1099            if r.h > 1 {
1100                self.fill_rect_alpha(
1101                    Rect::new(r.x, r.bottom() - 1, r.w, 1),
1102                    border.color,
1103                    opacity,
1104                )?;
1105            }
1106            if r.h > 2 {
1107                self.fill_rect_alpha(Rect::new(r.x, r.y + 1, 1, r.h - 2), border.color, opacity)?;
1108                if r.w > 1 {
1109                    self.fill_rect_alpha(
1110                        Rect::new(r.right() - 1, r.y + 1, 1, r.h - 2),
1111                        border.color,
1112                        opacity,
1113                    )?;
1114                }
1115            }
1116        }
1117        Ok(())
1118    }
1119
1120    pub fn stroke_rounded_rect(
1121        &mut self,
1122        rect: impl Into<Rect>,
1123        radius: u8,
1124        border: Border,
1125    ) -> Result<(), D::Error> {
1126        self.stroke_rounded_rect_alpha(rect, radius, border, 255)
1127    }
1128
1129    pub fn stroke_rounded_rect_alpha(
1130        &mut self,
1131        rect: impl Into<Rect>,
1132        radius: u8,
1133        border: Border,
1134        opacity: u8,
1135    ) -> Result<(), D::Error> {
1136        let rect = rect.into();
1137        if border.width == 0 || rect.is_empty() || opacity == 0 {
1138            return Ok(());
1139        }
1140
1141        let draw = self.visible_rect(rect);
1142        if draw.is_empty() {
1143            return Ok(());
1144        }
1145
1146        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
1147        for y in draw.y..draw.bottom() {
1148            for x in draw.x..draw.right() {
1149                if !in_rounded_rect(x, y, rect, radius) {
1150                    continue;
1151                }
1152
1153                let mut inner_hit = false;
1154                let mut i = 1u8;
1155                while i < border.width {
1156                    let inset = i as i32;
1157                    let inner = Rect::new(
1158                        rect.x + inset,
1159                        rect.y + inset,
1160                        rect.w.saturating_sub((i as u32) * 2),
1161                        rect.h.saturating_sub((i as u32) * 2),
1162                    );
1163                    let inner_radius = radius.saturating_sub(i);
1164                    if !inner.is_empty() && in_rounded_rect(x, y, inner, inner_radius) {
1165                        inner_hit = true;
1166                        break;
1167                    }
1168                    i += 1;
1169                }
1170
1171                if !inner_hit {
1172                    self.pixel(x, y, border.color, opacity)?;
1173                }
1174            }
1175        }
1176        Ok(())
1177    }
1178
1179    pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Rgb565) -> Result<(), D::Error> {
1180        self.draw_text_with_font(x, y, text, color, FontId::Tiny3x5)
1181    }
1182
1183    pub fn draw_text_with_font(
1184        &mut self,
1185        x: i32,
1186        y: i32,
1187        text: &str,
1188        color: Rgb565,
1189        font: impl Into<FontId>,
1190    ) -> Result<(), D::Error> {
1191        let font = font.into();
1192        let advance = font.advance() as i32;
1193        let line_h = font.line_height() as i32;
1194        let mut cursor_x = x;
1195        let mut cursor_y = y;
1196        for ch in text.chars() {
1197            if ch == '\n' {
1198                cursor_x = x;
1199                cursor_y += line_h;
1200                continue;
1201            }
1202            self.draw_char_with_font(cursor_x, cursor_y, ch, color, 255, font)?;
1203            cursor_x += advance;
1204        }
1205        Ok(())
1206    }
1207
1208    pub fn draw_text_in(
1209        &mut self,
1210        rect: impl Into<Rect>,
1211        text: &str,
1212        style: TextStyle,
1213    ) -> Result<(), D::Error> {
1214        self.draw_text_in_with_font(rect, text, style, style.font)
1215    }
1216
1217    pub fn draw_text_shaped_in<S, const N: usize>(
1218        &mut self,
1219        rect: Rect,
1220        text: &str,
1221        style: TextStyle,
1222        shaper: &S,
1223        config: crate::text::ShapingConfig,
1224    ) -> Result<(), D::Error>
1225    where
1226        S: crate::text::TextShaper,
1227    {
1228        if rect.is_empty() {
1229            return Ok(());
1230        }
1231        let mut shaped = heapless::Vec::<crate::text::ShapedGlyph, N>::new();
1232        shaper.shape(text, config, &mut shaped);
1233        if shaped.is_empty() {
1234            return Ok(());
1235        }
1236        let mut x = rect.x;
1237        let y = rect.y + rect.h.saturating_sub(style.font.line_height()) as i32 / 2;
1238        for glyph in shaped {
1239            self.draw_char_with_font(x, y, glyph.ch, style.color, style.opacity, style.font)?;
1240            x += (glyph.x_advance as i32).max(1) * style.font.advance() as i32;
1241            if x >= rect.right() {
1242                break;
1243            }
1244        }
1245        Ok(())
1246    }
1247
1248    pub fn draw_text_in_with_font(
1249        &mut self,
1250        rect: impl Into<Rect>,
1251        text: &str,
1252        style: TextStyle,
1253        font: impl Into<FontId>,
1254    ) -> Result<(), D::Error> {
1255        let rect = rect.into();
1256        let font = font.into();
1257        if rect.is_empty() {
1258            return Ok(());
1259        }
1260
1261        let advance = font.advance();
1262        let line_h = font.line_height();
1263        let max_chars = (rect.w / advance).max(1) as usize;
1264        let char_count = text.chars().count();
1265        let line_count = count_lines(text, max_chars, style.wrap).max(1);
1266        let line_step = line_h + style.line_spacing as u32;
1267        let total_h = line_count as u32 * line_h
1268            + line_count.saturating_sub(1) as u32 * style.line_spacing as u32;
1269        let mut y = match style.vertical_align {
1270            VerticalAlign::Top => rect.y,
1271            VerticalAlign::Middle => rect.y + rect.h.saturating_sub(total_h) as i32 / 2,
1272            VerticalAlign::Bottom => rect.y + rect.h.saturating_sub(total_h) as i32,
1273        };
1274
1275        let mut start = 0;
1276        let mut rendered_lines = 0u8;
1277        let max_lines = match style.overflow_policy {
1278            TextOverflowPolicy::WrapThenEllipsis { max_lines } => max_lines.max(1),
1279            TextOverflowPolicy::Global(_) => style.max_lines.unwrap_or(u8::MAX),
1280        };
1281        while start < char_count {
1282            if rendered_lines >= max_lines {
1283                break;
1284            }
1285            let (len, consumed_newline) = line_len_at(text, start, max_chars, style.wrap);
1286            let mut draw_len = len;
1287            let is_last_allowed_line = rendered_lines.saturating_add(1) >= max_lines;
1288            let use_ellipsis = match style.overflow_policy {
1289                TextOverflowPolicy::WrapThenEllipsis { .. } => is_last_allowed_line,
1290                TextOverflowPolicy::Global(mode) => mode == TextOverflow::Ellipsis,
1291            };
1292            if use_ellipsis
1293                && ((!consumed_newline && start + len < char_count) || is_last_allowed_line)
1294            {
1295                let ellipsis_width = match style.ellipsis {
1296                    EllipsisMode::ThreeDots => 3usize,
1297                    EllipsisMode::SingleGlyph => 1usize,
1298                };
1299                if len > ellipsis_width {
1300                    draw_len = len - ellipsis_width;
1301                }
1302            }
1303            let line_w = self.substring_width(text, start, draw_len, font, style.kerning);
1304            let x = match style.align {
1305                TextAlign::Left => rect.x,
1306                TextAlign::Center => rect.x + rect.w.saturating_sub(line_w) as i32 / 2,
1307                TextAlign::Right => rect.x + rect.w.saturating_sub(line_w) as i32,
1308            };
1309            self.draw_chars_with_font(
1310                x,
1311                y,
1312                text,
1313                start,
1314                draw_len,
1315                style.color,
1316                style.opacity,
1317                font,
1318                style.kerning,
1319            )?;
1320            if draw_len < len && use_ellipsis {
1321                let token = match style.ellipsis {
1322                    EllipsisMode::ThreeDots => "...",
1323                    EllipsisMode::SingleGlyph => ".",
1324                };
1325                self.draw_text_with_font(x + line_w as i32, y, token, style.color, font)?;
1326            }
1327            y += line_step as i32;
1328            rendered_lines = rendered_lines.saturating_add(1);
1329            start += len + usize::from(consumed_newline);
1330            if style.wrap == TextWrap::Word && start < char_count {
1331                while text.chars().nth(start).is_some_and(|ch| ch == ' ') {
1332                    start += 1;
1333                }
1334            }
1335            if len == 0 && !consumed_newline {
1336                break;
1337            }
1338        }
1339
1340        Ok(())
1341    }
1342
1343    pub fn draw_line_in(&mut self, rect: Rect, line: text::Line<'_>) -> Result<(), D::Error> {
1344        if rect.is_empty() {
1345            return Ok(());
1346        }
1347
1348        self.draw_line_segment_in(rect, line, 0, line.width_chars())
1349    }
1350
1351    pub fn draw_line(
1352        &mut self,
1353        x0: i32,
1354        y0: i32,
1355        x1: i32,
1356        y1: i32,
1357        color: Rgb565,
1358    ) -> Result<(), D::Error> {
1359        self.draw_line_styled(x0, y0, x1, y1, StrokeStyle::new(color))
1360    }
1361
1362    pub fn draw_line_styled(
1363        &mut self,
1364        x0: i32,
1365        y0: i32,
1366        x1: i32,
1367        y1: i32,
1368        style: StrokeStyle,
1369    ) -> Result<(), D::Error> {
1370        let mut x = x0;
1371        let mut y = y0;
1372        let dx = (x1 - x0).abs();
1373        let sx = if x0 < x1 { 1 } else { -1 };
1374        let dy = -(y1 - y0).abs();
1375        let sy = if y0 < y1 { 1 } else { -1 };
1376        let mut err = dx + dy;
1377        let half = (style.width as i32 / 2).max(0);
1378        let opacity = self.stroke_opacity(style);
1379
1380        loop {
1381            for oy in -half..=half {
1382                for ox in -half..=half {
1383                    self.pixel(x + ox, y + oy, style.color, opacity)?;
1384                }
1385            }
1386            if style.cap == StrokeCap::Round {
1387                self.fill_circle(x0, y0, half.max(1) as u32, style.color)?;
1388                self.fill_circle(x1, y1, half.max(1) as u32, style.color)?;
1389            }
1390            if x == x1 && y == y1 {
1391                break;
1392            }
1393            let e2 = 2 * err;
1394            if e2 >= dy {
1395                err += dy;
1396                x += sx;
1397            }
1398            if e2 <= dx {
1399                err += dx;
1400                y += sy;
1401            }
1402        }
1403        Ok(())
1404    }
1405
1406    pub fn fill_circle(
1407        &mut self,
1408        center_x: i32,
1409        center_y: i32,
1410        radius: u32,
1411        color: Rgb565,
1412    ) -> Result<(), D::Error> {
1413        let radius = radius as i32;
1414        if radius <= 0 {
1415            return Ok(());
1416        }
1417        let r_sq = radius * radius;
1418        for dy in -radius..=radius {
1419            let dx = ((r_sq - dy * dy) as f32).sqrt() as i32;
1420            if dx >= 0 {
1421                let w = (dx * 2 + 1) as u32;
1422                self.fill_rect(Rect::new(center_x - dx, center_y + dy, w, 1), color)?;
1423            }
1424        }
1425        Ok(())
1426    }
1427
1428    pub fn stroke_circle(
1429        &mut self,
1430        center_x: i32,
1431        center_y: i32,
1432        radius: u32,
1433        color: Rgb565,
1434    ) -> Result<(), D::Error> {
1435        let radius = radius as i32;
1436        if radius <= 0 {
1437            return Ok(());
1438        }
1439        let mut x = radius;
1440        let mut y = 0;
1441        let mut err = 1 - x;
1442        while x >= y {
1443            self.pixel(center_x + x, center_y + y, color, 255)?;
1444            self.pixel(center_x + y, center_y + x, color, 255)?;
1445            self.pixel(center_x - y, center_y + x, color, 255)?;
1446            self.pixel(center_x - x, center_y + y, color, 255)?;
1447            self.pixel(center_x - x, center_y - y, color, 255)?;
1448            self.pixel(center_x - y, center_y - x, color, 255)?;
1449            self.pixel(center_x + y, center_y - x, color, 255)?;
1450            self.pixel(center_x + x, center_y - y, color, 255)?;
1451            y += 1;
1452            if err < 0 {
1453                err += 2 * y + 1;
1454            } else {
1455                x -= 1;
1456                err += 2 * (y - x) + 1;
1457            }
1458        }
1459        Ok(())
1460    }
1461
1462    pub fn stroke_arc(
1463        &mut self,
1464        center_x: i32,
1465        center_y: i32,
1466        radius: u32,
1467        start_deg: i32,
1468        end_deg: i32,
1469        color: Rgb565,
1470    ) -> Result<(), D::Error> {
1471        self.stroke_arc_styled(
1472            center_x,
1473            center_y,
1474            radius,
1475            start_deg,
1476            end_deg,
1477            StrokeStyle::new(color),
1478        )
1479    }
1480
1481    pub fn stroke_arc_styled(
1482        &mut self,
1483        center_x: i32,
1484        center_y: i32,
1485        radius: u32,
1486        start_deg: i32,
1487        end_deg: i32,
1488        style: StrokeStyle,
1489    ) -> Result<(), D::Error> {
1490        let mut start = start_deg;
1491        let mut end = end_deg;
1492        if end < start {
1493            core::mem::swap(&mut start, &mut end);
1494        }
1495        let mut deg = start;
1496        let step = match self.quality {
1497            RenderQuality::Low => 8,
1498            RenderQuality::Medium => 4,
1499            RenderQuality::High => 2,
1500        };
1501        while deg <= end {
1502            let rad = (deg as f32).to_radians();
1503            let x = center_x + (radius as f32 * rad.cos()) as i32;
1504            let y = center_y + (radius as f32 * rad.sin()) as i32;
1505            let half = (style.width as i32 / 2).max(0);
1506            let opacity = self.stroke_opacity(style);
1507            for oy in -half..=half {
1508                for ox in -half..=half {
1509                    self.pixel(x + ox, y + oy, style.color, opacity)?;
1510                }
1511            }
1512            if style.join == StrokeJoin::Round {
1513                self.fill_circle(x, y, half.max(1) as u32, style.color)?;
1514            }
1515            deg += step;
1516        }
1517        Ok(())
1518    }
1519
1520    /// Fill a sector ("pie slice") using a start angle and sweep angle in degrees.
1521    ///
1522    /// Positive sweep draws counterclockwise, negative sweep clockwise.
1523    pub fn fill_sector_sweep(
1524        &mut self,
1525        center_x: i32,
1526        center_y: i32,
1527        radius: u32,
1528        start_deg: f32,
1529        sweep_deg: f32,
1530        color: Rgb565,
1531    ) -> Result<(), D::Error> {
1532        if radius == 0 {
1533            return Ok(());
1534        }
1535
1536        let draw = self.visible_rect(Rect::new(
1537            center_x - radius as i32,
1538            center_y - radius as i32,
1539            radius.saturating_mul(2).saturating_add(1),
1540            radius.saturating_mul(2).saturating_add(1),
1541        ));
1542        if draw.is_empty() {
1543            return Ok(());
1544        }
1545
1546        let max_sweep = sweep_deg.abs().min(360.0);
1547        if max_sweep <= 0.0 {
1548            return Ok(());
1549        }
1550
1551        let rr = (radius as i32) * (radius as i32);
1552        let start = normalize_angle_deg(start_deg);
1553        let ccw = sweep_deg >= 0.0;
1554
1555        // The sector is the arc of length `max_sweep`, in degrees, that
1556        // starts at `lo_deg` and ends at `hi_deg` (both expressed in the
1557        // same increasing-angle direction `atan2` would report). Reduce
1558        // this to two boundary direction vectors so the per-pixel test is
1559        // a couple of multiply-subtracts instead of an `atan2` + degrees
1560        // conversion for every pixel in the circle -- `atan2` is a
1561        // software-emulated call on MCUs without a hardware FPU trig unit,
1562        // and this loop used to run it for every pixel inside the radius,
1563        // every frame.
1564        let (lo_deg, hi_deg) = if ccw {
1565            (start, start + max_sweep)
1566        } else {
1567            (start - max_sweep, start)
1568        };
1569        let (lo_c, lo_s) = cardinal_unit(lo_deg)
1570            .unwrap_or_else(|| (lo_deg.to_radians().cos(), lo_deg.to_radians().sin()));
1571        let (hi_c, hi_s) = cardinal_unit(hi_deg)
1572            .unwrap_or_else(|| (hi_deg.to_radians().cos(), hi_deg.to_radians().sin()));
1573        // A sweep over half a circle or less is a convex wedge, testable
1574        // directly with two half-plane (cross-product) checks. A sweep
1575        // past 180 degrees is non-convex, but its complement (the
1576        // untouched slice) is convex and always < 180 degrees, so test
1577        // for exclusion from that instead.
1578        let full_circle = max_sweep >= 360.0;
1579        let reflex = max_sweep > 180.0;
1580
1581        for y in draw.y..draw.bottom() {
1582            for x in draw.x..draw.right() {
1583                let dx = x - center_x;
1584                let dy = y - center_y;
1585                let d2 = dx * dx + dy * dy;
1586                if d2 > rr {
1587                    continue;
1588                }
1589
1590                let in_sweep = if full_circle {
1591                    true
1592                } else {
1593                    let (fx, fy) = (dx as f32, dy as f32);
1594                    if !reflex {
1595                        cross(lo_c, lo_s, fx, fy) >= 0.0 && cross(fx, fy, hi_c, hi_s) >= 0.0
1596                    } else {
1597                        !(cross(hi_c, hi_s, fx, fy) >= 0.0 && cross(fx, fy, lo_c, lo_s) >= 0.0)
1598                    }
1599                };
1600                if in_sweep {
1601                    self.pixel(x, y, color, 255)?;
1602                }
1603            }
1604        }
1605        Ok(())
1606    }
1607
1608    pub fn fill_polygon(&mut self, points: &[Point], color: Rgb565) -> Result<(), D::Error> {
1609        if points.len() < 3 {
1610            return Ok(());
1611        }
1612        let min_y = points.iter().map(|p| p.y).min().unwrap_or(0);
1613        let max_y = points.iter().map(|p| p.y).max().unwrap_or(-1);
1614        for y in min_y..=max_y {
1615            let mut intersections = [i32::MIN; 16];
1616            let mut count = 0usize;
1617            for i in 0..points.len() {
1618                let p1 = points[i];
1619                let p2 = points[(i + 1) % points.len()];
1620                let (y1, y2) = if p1.y <= p2.y {
1621                    (p1.y, p2.y)
1622                } else {
1623                    (p2.y, p1.y)
1624                };
1625                if y < y1 || y >= y2 || y1 == y2 {
1626                    continue;
1627                }
1628                if count >= intersections.len() {
1629                    break;
1630                }
1631                let x = p1.x + ((y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y);
1632                intersections[count] = x;
1633                count += 1;
1634            }
1635            intersections[..count].sort_unstable();
1636            let mut i = 0;
1637            while i + 1 < count {
1638                let x0 = intersections[i];
1639                let x1 = intersections[i + 1];
1640                for x in x0..=x1 {
1641                    self.pixel(x, y, color, 255)?;
1642                }
1643                i += 2;
1644            }
1645        }
1646        Ok(())
1647    }
1648
1649    pub fn draw_image(
1650        &mut self,
1651        rect: Rect,
1652        image: ImageRef<'_>,
1653        fit: ImageFit,
1654    ) -> Result<(), D::Error> {
1655        self.draw_image_region(rect, image, fit, Rect::new(0, 0, image.width, image.height))
1656    }
1657
1658    pub fn draw_image_region(
1659        &mut self,
1660        rect: Rect,
1661        image: ImageRef<'_>,
1662        fit: ImageFit,
1663        src_rect: Rect,
1664    ) -> Result<(), D::Error> {
1665        let bounds = image.bounds_at(rect, fit);
1666        if bounds.is_empty() || image.width == 0 || image.height == 0 {
1667            return Ok(());
1668        }
1669        let src_w = image.width as usize;
1670        for y in 0..bounds.h {
1671            let src_y = match fit {
1672                ImageFit::Stretch => {
1673                    src_rect.y.max(0) as usize
1674                        + ((y as u64 * src_rect.h as u64) / bounds.h as u64) as usize
1675                }
1676                ImageFit::Center => src_rect.y.max(0) as usize + y as usize,
1677            };
1678            for x in 0..bounds.w {
1679                let src_x = match fit {
1680                    ImageFit::Stretch => {
1681                        src_rect.x.max(0) as usize
1682                            + ((x as u64 * src_rect.w as u64) / bounds.w as u64) as usize
1683                    }
1684                    ImageFit::Center => src_rect.x.max(0) as usize + x as usize,
1685                };
1686                let idx = src_y.saturating_mul(src_w).saturating_add(src_x);
1687                if let Some(raw) = image.pixels.get(idx) {
1688                    let color = Rgb565::new(
1689                        ((raw >> 11) & 0x1F) as u8,
1690                        ((raw >> 5) & 0x3F) as u8,
1691                        (raw & 0x1F) as u8,
1692                    );
1693                    self.pixel(bounds.x + x as i32, bounds.y + y as i32, color, 255)?;
1694                }
1695            }
1696        }
1697        Ok(())
1698    }
1699
1700    pub fn draw_image_transformed(
1701        &mut self,
1702        rect: Rect,
1703        image: ImageRef<'_>,
1704        scale: f32,
1705        rotation_deg: f32,
1706    ) -> Result<(), D::Error> {
1707        if rect.is_empty() || image.width == 0 || image.height == 0 || scale <= 0.0 {
1708            return Ok(());
1709        }
1710        let cx = rect.x + rect.w as i32 / 2;
1711        let cy = rect.y + rect.h as i32 / 2;
1712        let rad = rotation_deg.to_radians();
1713        let cos_r = rad.cos();
1714        let sin_r = rad.sin();
1715        let src_w = image.width as usize;
1716        let src_cx = image.width as f32 / 2.0;
1717        let src_cy = image.height as f32 / 2.0;
1718        for y in rect.y..rect.bottom() {
1719            for x in rect.x..rect.right() {
1720                let dx = (x - cx) as f32 / scale;
1721                let dy = (y - cy) as f32 / scale;
1722                let sx = cos_r * dx + sin_r * dy + src_cx;
1723                let sy = -sin_r * dx + cos_r * dy + src_cy;
1724                if sx < 0.0 || sy < 0.0 || sx >= image.width as f32 || sy >= image.height as f32 {
1725                    continue;
1726                }
1727                let idx = (sy as usize)
1728                    .saturating_mul(src_w)
1729                    .saturating_add(sx as usize);
1730                if let Some(raw) = image.pixels.get(idx) {
1731                    let color = Rgb565::new(
1732                        ((raw >> 11) & 0x1F) as u8,
1733                        ((raw >> 5) & 0x3F) as u8,
1734                        (raw & 0x1F) as u8,
1735                    );
1736                    self.pixel(x, y, color, 255)?;
1737                }
1738            }
1739        }
1740        Ok(())
1741    }
1742
1743    pub fn fill_rect_masked(
1744        &mut self,
1745        rect: Rect,
1746        color: Rgb565,
1747        mask: fn(i32, i32) -> bool,
1748    ) -> Result<(), D::Error> {
1749        let draw = self.visible_rect(rect);
1750        if draw.is_empty() {
1751            return Ok(());
1752        }
1753        for y in draw.y..draw.bottom() {
1754            for x in draw.x..draw.right() {
1755                if mask(x, y) {
1756                    self.pixel(x, y, color, 255)?;
1757                }
1758            }
1759        }
1760        Ok(())
1761    }
1762
1763    pub fn draw_text_model_in(&mut self, rect: Rect, text: text::Text<'_>) -> Result<(), D::Error> {
1764        if rect.is_empty() || text.lines.is_empty() {
1765            return Ok(());
1766        }
1767
1768        let metrics = text.metrics(rect.w);
1769        let max_line_height = text
1770            .lines
1771            .iter()
1772            .map(|line| line.max_line_height())
1773            .max()
1774            .unwrap_or(CHAR_HEIGHT);
1775        let line_step = max_line_height + text.line_spacing as u32;
1776        let mut y = match text.vertical_align {
1777            VerticalAlign::Top => rect.y,
1778            VerticalAlign::Middle => rect.y + rect.h.saturating_sub(metrics.height) as i32 / 2,
1779            VerticalAlign::Bottom => rect.y + rect.h.saturating_sub(metrics.height) as i32,
1780        };
1781        for line in text.lines {
1782            let align = if line.align == TextAlign::Left {
1783                text.align
1784            } else {
1785                line.align
1786            };
1787            let line = text::Line { align, ..*line };
1788
1789            let mut start = 0;
1790            let char_count = line.char_count();
1791            if char_count == 0 {
1792                y += line_step as i32;
1793                continue;
1794            }
1795            while start < char_count {
1796                if y >= rect.bottom() {
1797                    return Ok(());
1798                }
1799                let (len, consumed_newline) = line.segment_len_at(start, rect.w, text.wrap);
1800                self.draw_line_segment_in(
1801                    Rect::new(rect.x, y, rect.w, max_line_height),
1802                    line,
1803                    start,
1804                    len,
1805                )?;
1806                y += line_step as i32;
1807                start += len + usize::from(consumed_newline);
1808                if len == 0 && !consumed_newline {
1809                    break;
1810                }
1811            }
1812        }
1813
1814        Ok(())
1815    }
1816
1817    pub fn text_metrics(text: &str) -> TextMetrics {
1818        Self::text_metrics_with_font(text, FontId::Tiny3x5)
1819    }
1820
1821    pub fn text_metrics_with_font(text: &str, font: impl Into<FontId>) -> TextMetrics {
1822        let font = font.into();
1823        TextMetrics {
1824            width: text.chars().count() as u32 * font.advance(),
1825            height: font.line_height(),
1826        }
1827    }
1828
1829    pub fn text_metrics_wrapped(text: &str, max_width: u32, wrap: TextWrap) -> TextMetrics {
1830        Self::text_metrics_wrapped_with_font(text, max_width, wrap, FontId::Tiny3x5)
1831    }
1832
1833    pub fn text_metrics_wrapped_with_font(
1834        text: &str,
1835        max_width: u32,
1836        wrap: TextWrap,
1837        font: impl Into<FontId>,
1838    ) -> TextMetrics {
1839        let font = font.into();
1840        let max_chars = (max_width / font.advance()).max(1) as usize;
1841        let lines = count_lines(text, max_chars, wrap).max(1);
1842        let widest = widest_line(text, max_chars, wrap) as u32 * font.advance();
1843        TextMetrics {
1844            width: widest.min(max_width),
1845            height: lines as u32 * font.line_height() + lines.saturating_sub(1) as u32,
1846        }
1847    }
1848
1849    #[allow(clippy::too_many_arguments)]
1850    fn draw_chars_with_font(
1851        &mut self,
1852        x: i32,
1853        y: i32,
1854        text: &str,
1855        start: usize,
1856        len: usize,
1857        color: Rgb565,
1858        opacity: u8,
1859        font: FontId,
1860        kerning: bool,
1861    ) -> Result<(), D::Error> {
1862        let advance = font.advance() as i32;
1863        let mut cursor_x = x;
1864        let mut prev: Option<char> = None;
1865        for ch in text.chars().skip(start).take(len) {
1866            self.draw_char_with_font(cursor_x, y, ch, color, opacity, font)?;
1867            cursor_x += advance + kerning_adjust(prev, ch, kerning);
1868            prev = Some(ch);
1869        }
1870        Ok(())
1871    }
1872
1873    fn substring_width(
1874        &self,
1875        text: &str,
1876        start: usize,
1877        len: usize,
1878        font: FontId,
1879        kerning: bool,
1880    ) -> u32 {
1881        let mut width = 0u32;
1882        let mut prev = None;
1883        for ch in text.chars().skip(start).take(len) {
1884            width = width.saturating_add(font.advance());
1885            let adjust = kerning_adjust(prev, ch, kerning);
1886            if adjust < 0 {
1887                width = width.saturating_sub((-adjust) as u32);
1888            } else {
1889                width = width.saturating_add(adjust as u32);
1890            }
1891            prev = Some(ch);
1892        }
1893        width
1894    }
1895
1896    fn draw_line_segment_in(
1897        &mut self,
1898        rect: Rect,
1899        line: text::Line<'_>,
1900        start: usize,
1901        len: usize,
1902    ) -> Result<(), D::Error> {
1903        if rect.is_empty() || len == 0 {
1904            return Ok(());
1905        }
1906
1907        let line_w = self.line_segment_width(line, start, len);
1908        let x = match line.align {
1909            TextAlign::Left => rect.x,
1910            TextAlign::Center => rect.x + rect.w.saturating_sub(line_w) as i32 / 2,
1911            TextAlign::Right => rect.x + rect.w.saturating_sub(line_w) as i32,
1912        };
1913
1914        let old_clip = self.clip;
1915        self.clip = self.clip.intersection(rect);
1916        let result = self.draw_span_chars(x, rect.y, line, start, len);
1917        self.clip = old_clip;
1918        result
1919    }
1920
1921    fn draw_span_chars(
1922        &mut self,
1923        x: i32,
1924        y: i32,
1925        line: text::Line<'_>,
1926        start: usize,
1927        len: usize,
1928    ) -> Result<(), D::Error> {
1929        let mut cursor_x = x;
1930        for (idx, (ch, style)) in line
1931            .spans
1932            .iter()
1933            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style)))
1934            .enumerate()
1935        {
1936            if idx < start {
1937                continue;
1938            }
1939            if idx >= start + len {
1940                break;
1941            }
1942            if ch != '\n' {
1943                self.draw_char_with_font(cursor_x, y, ch, style.color, 255, style.font)?;
1944                cursor_x += style.font.advance() as i32;
1945            }
1946        }
1947        Ok(())
1948    }
1949
1950    fn line_segment_width(&self, line: text::Line<'_>, start: usize, len: usize) -> u32 {
1951        line.spans
1952            .iter()
1953            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style.font)))
1954            .enumerate()
1955            .filter_map(|(idx, (ch, font))| {
1956                if idx < start || idx >= start + len || ch == '\n' {
1957                    None
1958                } else {
1959                    Some(font.advance())
1960                }
1961            })
1962            .sum()
1963    }
1964
1965    fn draw_char_with_font(
1966        &mut self,
1967        x: i32,
1968        y: i32,
1969        ch: char,
1970        color: Rgb565,
1971        opacity: u8,
1972        font: FontId,
1973    ) -> Result<(), D::Error> {
1974        let glyph = glyph_rows(font, ch);
1975        let layer = self.current_layer();
1976        let fast_spans = opacity == 255
1977            && layer.opacity == 255
1978            && self.current_transform().is_identity()
1979            && layer.blend == BlendMode::Normal;
1980
1981        match font {
1982            FontId::Tiny3x5 | FontId::Medium4x7 | FontId::Custom(_) => {
1983                for (row, bits) in glyph.iter().enumerate() {
1984                    let ry = y + row as i32;
1985                    if fast_spans && *bits == 0b111 {
1986                        self.fill_rect(Rect::new(x, ry, 3, 1), color)?;
1987                    } else if fast_spans && *bits == 0b110 {
1988                        self.fill_rect(Rect::new(x, ry, 2, 1), color)?;
1989                    } else if fast_spans && *bits == 0b011 {
1990                        self.fill_rect(Rect::new(x + 1, ry, 2, 1), color)?;
1991                    } else {
1992                        for col in 0..3 {
1993                            if bits & (1 << (2 - col)) != 0 {
1994                                self.pixel(x + col, ry, color, opacity)?;
1995                            }
1996                        }
1997                    }
1998                }
1999            }
2000            FontId::Scaled6x10 => {
2001                for (row, bits) in glyph.iter().enumerate() {
2002                    for col in 0..3 {
2003                        if bits & (1 << (2 - col)) != 0 {
2004                            let px = x + (col * 2);
2005                            let py = y + (row as i32 * 2);
2006                            self.pixel(px, py, color, opacity)?;
2007                            self.pixel(px + 1, py, color, opacity)?;
2008                            self.pixel(px, py + 1, color, opacity)?;
2009                            self.pixel(px + 1, py + 1, color, opacity)?;
2010                        }
2011                    }
2012                }
2013            }
2014            FontId::Vector(scale) => {
2015                let glyph = crate::font::get_vector_glyph(ch);
2016                let mut last_point: Option<(i32, i32)> = None;
2017                let scale_f = scale as f32;
2018                for &(px, py) in glyph {
2019                    if px == 0xFF && py == 0xFF {
2020                        last_point = None;
2021                        continue;
2022                    }
2023                    let draw_x = x + (px as f32 * scale_f) as i32;
2024                    let draw_y = y + (py as f32 * scale_f) as i32;
2025                    if let Some((lx, ly)) = last_point {
2026                        self.draw_line_styled(
2027                            lx,
2028                            ly,
2029                            draw_x,
2030                            draw_y,
2031                            StrokeStyle::new(color).with_width(1).with_antialias(true),
2032                        )?;
2033                    }
2034                    last_point = Some((draw_x, draw_y));
2035                }
2036            }
2037            #[cfg(feature = "embedded-graphics")]
2038            FontId::MonoFont(font) => {
2039                use embedded_graphics::Drawable;
2040                use embedded_graphics::draw_target::DrawTarget;
2041                use embedded_graphics::geometry::{OriginDimensions, Point, Size};
2042                use embedded_graphics::mono_font::MonoTextStyle;
2043                use embedded_graphics::pixelcolor::BinaryColor;
2044                use embedded_graphics::text::Text;
2045
2046                struct GlyphPixelCollector<'a, F> {
2047                    x: i32,
2048                    y: i32,
2049                    f: &'a mut F,
2050                }
2051
2052                impl<F> OriginDimensions for GlyphPixelCollector<'_, F> {
2053                    fn size(&self) -> Size {
2054                        Size::new(u32::MAX, u32::MAX)
2055                    }
2056                }
2057
2058                impl<F: FnMut(i32, i32)> DrawTarget for GlyphPixelCollector<'_, F> {
2059                    type Color = BinaryColor;
2060                    type Error = core::convert::Infallible;
2061
2062                    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
2063                    where
2064                        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
2065                    {
2066                        for embedded_graphics::Pixel(pos, color) in pixels {
2067                            if color.is_on() {
2068                                (self.f)(self.x + pos.x, self.y + pos.y);
2069                            }
2070                        }
2071                        Ok(())
2072                    }
2073                }
2074
2075                let mut collector_err = Ok(());
2076                let mut pixel_cb = |px: i32, py: i32| {
2077                    if collector_err.is_ok() {
2078                        if let Err(e) = self.pixel(px, py, color, opacity) {
2079                            collector_err = Err(e);
2080                        }
2081                    }
2082                };
2083
2084                let mut collector = GlyphPixelCollector {
2085                    x,
2086                    y,
2087                    f: &mut pixel_cb,
2088                };
2089
2090                let text_style = MonoTextStyle::new(font, BinaryColor::On);
2091                let mut buf = [0u8; 4];
2092                let ch_str = ch.encode_utf8(&mut buf);
2093                let _ = Text::new(ch_str, Point::zero(), text_style).draw(&mut collector);
2094                collector_err?;
2095            }
2096        }
2097        Ok(())
2098    }
2099
2100    fn pixel(&mut self, x: i32, y: i32, color: Rgb565, opacity: u8) -> Result<(), D::Error> {
2101        let (x, y) = self.current_transform().apply(x, y);
2102        if !self.clip.contains(x, y) {
2103            return Ok(());
2104        }
2105        if let Some(dirty) = self.dirty {
2106            if !dirty.contains(x, y) {
2107                return Ok(());
2108            }
2109        }
2110        let layer = self.current_layer();
2111        let combined_opacity = ((opacity as u16 * layer.opacity as u16) / 255) as u8;
2112        // The compositor policy (`Dither` vs `Blend`) decides how the pixel
2113        // lands: ordered dither for write-only targets, true alpha blend for
2114        // readback-capable ones. Zero-cost — resolved by `C` at monomorphization.
2115        C::plot(
2116            self.target,
2117            x,
2118            y,
2119            color,
2120            combined_opacity,
2121            layer.blend,
2122            layer.backdrop,
2123        )
2124    }
2125
2126    fn visible_rect(&self, rect: Rect) -> Rect {
2127        let mut draw = rect.intersection(self.clip);
2128        if let Some(dirty) = self.dirty {
2129            draw = draw.intersection(dirty);
2130        }
2131        draw
2132    }
2133
2134    fn current_transform(&self) -> Transform2D {
2135        self.transform_stack[self.transform_len - 1]
2136    }
2137
2138    fn current_layer(&self) -> LayerState {
2139        self.layer_stack[self.layer_len - 1]
2140    }
2141
2142    fn stroke_opacity(&self, style: StrokeStyle) -> u8 {
2143        if !style.antialias || matches!(style.antialias_mode, AntiAliasMode::None) {
2144            return 255;
2145        }
2146        match style.antialias_mode {
2147            AntiAliasMode::None => 255,
2148            AntiAliasMode::Coverage => match self.quality {
2149                RenderQuality::Low => 96,
2150                RenderQuality::Medium => 160,
2151                RenderQuality::High => 220,
2152            },
2153            AntiAliasMode::Subpixel => {
2154                if self.backend_caps.supports_subpixel {
2155                    match self.quality {
2156                        RenderQuality::Low => 128,
2157                        RenderQuality::Medium => 192,
2158                        RenderQuality::High => 240,
2159                    }
2160                } else {
2161                    match self.quality {
2162                        RenderQuality::Low => 96,
2163                        RenderQuality::Medium => 160,
2164                        RenderQuality::High => 220,
2165                    }
2166                }
2167            }
2168        }
2169    }
2170}
2171
2172impl<'a, D, C> RenderCtx<'a, D, C>
2173where
2174    D: DrawTarget<Color = Rgb565> + PixelRead,
2175    C: Compositor<D>,
2176{
2177    /// Alpha-composite `color` over whatever is already at `(x, y)` in the
2178    /// destination, using true per-pixel blending (`lerp_rgb565`) rather
2179    /// than the dithered approximation `pixel()` uses.
2180    fn pixel_blended(&mut self, x: i32, y: i32, color: Rgb565, alpha: u8) -> Result<(), D::Error> {
2181        let (x, y) = self.current_transform().apply(x, y);
2182        if !self.clip.contains(x, y) {
2183            return Ok(());
2184        }
2185        if let Some(dirty) = self.dirty {
2186            if !dirty.contains(x, y) {
2187                return Ok(());
2188            }
2189        }
2190        let layer = self.current_layer();
2191        let combined_alpha = ((alpha as u16 * layer.opacity as u16) / 255) as u8;
2192        if combined_alpha == 0 {
2193            return Ok(());
2194        }
2195        let backdrop = self.target.get_pixel(Point::new(x, y));
2196        let blended = lerp_rgb565(backdrop, color, combined_alpha);
2197        let blended = apply_blend_mode(blended, layer.blend, layer.backdrop);
2198        self.target.draw_iter([Pixel(Point::new(x, y), blended)])
2199    }
2200
2201    /// Like [`RenderCtx::fill_rect_alpha`], but alpha-composites against the
2202    /// destination's real current pixels instead of dithering.
2203    pub fn fill_rect_true_alpha(
2204        &mut self,
2205        rect: Rect,
2206        color: Rgb565,
2207        alpha: u8,
2208    ) -> Result<(), D::Error> {
2209        self.fill_rounded_rect_true_alpha(rect, 0, color, alpha)
2210    }
2211
2212    /// Like [`RenderCtx::fill_rounded_rect_alpha`], but alpha-composites
2213    /// against the destination's real current pixels instead of dithering.
2214    pub fn fill_rounded_rect_true_alpha(
2215        &mut self,
2216        rect: Rect,
2217        radius: u8,
2218        color: Rgb565,
2219        alpha: u8,
2220    ) -> Result<(), D::Error> {
2221        let draw = self.visible_rect(rect);
2222        if draw.is_empty() || alpha == 0 {
2223            return Ok(());
2224        }
2225        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
2226
2227        for y in draw.y..draw.bottom() {
2228            for x in draw.x..draw.right() {
2229                if !in_rounded_rect(x, y, rect, radius) {
2230                    continue;
2231                }
2232                self.pixel_blended(x, y, color, alpha)?;
2233            }
2234        }
2235        Ok(())
2236    }
2237
2238    /// Fill a rectangle with an 8-bit alpha mask and solid color.
2239    pub fn fill_rect_alpha_mask(
2240        &mut self,
2241        rect: Rect,
2242        mask: &[u8],
2243        mask_stride: usize,
2244        color: Rgb565,
2245        opacity: u8,
2246    ) -> Result<(), D::Error> {
2247        let draw = self.visible_rect(rect);
2248        if draw.is_empty() || opacity == 0 || mask_stride == 0 {
2249            return Ok(());
2250        }
2251        for y in draw.y..draw.bottom() {
2252            let my = (y - rect.y) as usize;
2253            for x in draw.x..draw.right() {
2254                let mx = (x - rect.x) as usize;
2255                let idx = my * mask_stride + mx;
2256                if let Some(&m_val) = mask.get(idx) {
2257                    if m_val > 0 {
2258                        let pix_opacity = ((m_val as u16 * opacity as u16) / 255) as u8;
2259                        self.pixel(x, y, color, pix_opacity)?;
2260                    }
2261                }
2262            }
2263        }
2264        Ok(())
2265    }
2266
2267    /// Fill a rounded rectangle with an [`AlphaLinearGradient`].
2268    pub fn fill_rounded_rect_alpha_gradient(
2269        &mut self,
2270        rect: Rect,
2271        radius: u8,
2272        gradient: &AlphaLinearGradient,
2273        opacity: u8,
2274    ) -> Result<(), D::Error> {
2275        let draw = self.visible_rect(rect);
2276        if draw.is_empty() || opacity == 0 {
2277            return Ok(());
2278        }
2279        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
2280        let denom = match gradient.direction {
2281            GradientDirection::Horizontal => rect.w.saturating_sub(1).max(1),
2282            GradientDirection::Vertical => rect.h.saturating_sub(1).max(1),
2283        };
2284
2285        for y in draw.y..draw.bottom() {
2286            for x in draw.x..draw.right() {
2287                if !in_rounded_rect(x, y, rect, radius) {
2288                    continue;
2289                }
2290                let numer = match gradient.direction {
2291                    GradientDirection::Horizontal => (x - rect.x).max(0) as u32,
2292                    GradientDirection::Vertical => (y - rect.y).max(0) as u32,
2293                }
2294                .min(denom);
2295                let t = ((numer * 255) / denom) as u8;
2296                let (color, grad_alpha) = gradient.sample(t);
2297                let combined_alpha = ((grad_alpha as u16 * opacity as u16) / 255) as u8;
2298                self.pixel(x, y, color, combined_alpha)?;
2299            }
2300        }
2301        Ok(())
2302    }
2303
2304    /// Fill a rounded rectangle with an [`AlphaRadialGradient`].
2305    pub fn fill_rounded_rect_radial_gradient(
2306        &mut self,
2307        rect: Rect,
2308        radius: u8,
2309        gradient: &AlphaRadialGradient,
2310        opacity: u8,
2311    ) -> Result<(), D::Error> {
2312        let draw = self.visible_rect(rect);
2313        if draw.is_empty() || opacity == 0 {
2314            return Ok(());
2315        }
2316        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
2317        let cx = rect.x as f32 + rect.w as f32 * gradient.center_x;
2318        let cy = rect.y as f32 + rect.h as f32 * gradient.center_y;
2319
2320        for y in draw.y..draw.bottom() {
2321            let dy = y as f32 - cy;
2322            for x in draw.x..draw.right() {
2323                if !in_rounded_rect(x, y, rect, radius) {
2324                    continue;
2325                }
2326                let dx = x as f32 - cx;
2327                let dist = (dx * dx + dy * dy).sqrt();
2328                let (color, grad_alpha) = gradient.sample_at_dist(dist);
2329                let combined_alpha = ((grad_alpha as u16 * opacity as u16) / 255) as u8;
2330                self.pixel(x, y, color, combined_alpha)?;
2331            }
2332        }
2333        Ok(())
2334    }
2335
2336    /// Render a soft drop shadow around a rounded rectangle.
2337    pub fn draw_drop_shadow(
2338        &mut self,
2339        rect: Rect,
2340        _radius: u8,
2341        shadow_color: Rgb565,
2342        shadow_opacity: u8,
2343        shadow_spread: u8,
2344        blur_radius: u8,
2345    ) -> Result<(), D::Error> {
2346        if shadow_opacity == 0 {
2347            return Ok(());
2348        }
2349        let margin = (shadow_spread as i32) + (blur_radius as i32);
2350        let shadow_rect = Rect::new(
2351            rect.x - margin,
2352            rect.y - margin,
2353            rect.w + (margin as u32 * 2),
2354            rect.h + (margin as u32 * 2),
2355        );
2356        let draw = self.visible_rect(shadow_rect);
2357        if draw.is_empty() {
2358            return Ok(());
2359        }
2360
2361        for y in draw.y..draw.bottom() {
2362            for x in draw.x..draw.right() {
2363                let dx = if x < rect.x {
2364                    rect.x - x
2365                } else if x >= rect.right() {
2366                    x - rect.right() + 1
2367                } else {
2368                    0
2369                };
2370                let dy = if y < rect.y {
2371                    rect.y - y
2372                } else if y >= rect.bottom() {
2373                    y - rect.bottom() + 1
2374                } else {
2375                    0
2376                };
2377
2378                let dist = ((dx * dx + dy * dy) as f32).sqrt();
2379                if dist > margin as f32 {
2380                    continue;
2381                }
2382
2383                let factor = (1.0 - (dist / (margin as f32 + 1.0))).clamp(0.0, 1.0);
2384                let alpha = (shadow_opacity as f32 * factor) as u8;
2385                if alpha > 0 {
2386                    self.pixel(x, y, shadow_color, alpha)?;
2387                }
2388            }
2389        }
2390        Ok(())
2391    }
2392
2393    /// Draw a UI card fill with an alpha gradient background and soft drop shadow.
2394    pub fn draw_card_fill(
2395        &mut self,
2396        rect: Rect,
2397        radius: u8,
2398        bg_gradient: &AlphaLinearGradient,
2399        shadow_color: Rgb565,
2400        shadow_opacity: u8,
2401        blur_radius: u8,
2402    ) -> Result<(), D::Error> {
2403        if shadow_opacity > 0 && blur_radius > 0 {
2404            self.draw_drop_shadow(rect, radius, shadow_color, shadow_opacity, 2, blur_radius)?;
2405        }
2406        self.fill_rounded_rect_alpha_gradient(rect, radius, bg_gradient, 255)
2407    }
2408
2409    /// Render a tile with optional wrapping mode.
2410    pub fn draw_tile(
2411        &mut self,
2412        rect: Rect,
2413        tile: TileRef<'_>,
2414        opacity: u8,
2415    ) -> Result<(), D::Error> {
2416        self.draw_tile_transformed_ssaa(rect, tile, Transform2D::IDENTITY, opacity, false)
2417    }
2418
2419    /// Render a transformed tile with optional wrapping mode.
2420    pub fn draw_tile_transformed(
2421        &mut self,
2422        rect: Rect,
2423        tile: TileRef<'_>,
2424        transform: Transform2D,
2425        opacity: u8,
2426    ) -> Result<(), D::Error> {
2427        self.draw_tile_transformed_ssaa(rect, tile, transform, opacity, false)
2428    }
2429
2430    /// Render a transformed tile with 2xSSAA (2x Super-Sampling Anti-Aliasing).
2431    pub fn draw_tile_transformed_ssaa(
2432        &mut self,
2433        rect: Rect,
2434        tile: TileRef<'_>,
2435        transform: Transform2D,
2436        opacity: u8,
2437        enable_ssaa: bool,
2438    ) -> Result<(), D::Error> {
2439        let draw = self.visible_rect(rect);
2440        if draw.is_empty() || opacity == 0 || tile.width == 0 || tile.height == 0 {
2441            return Ok(());
2442        }
2443
2444        let inv_transform = match transform.inverse() {
2445            Some(inv) => inv,
2446            None => return Ok(()),
2447        };
2448
2449        let cx = rect.x as f32 + rect.w as f32 * 0.5;
2450        let cy = rect.y as f32 + rect.h as f32 * 0.5;
2451
2452        let offsets = [
2453            (0.25f32, 0.25f32),
2454            (0.75f32, 0.25f32),
2455            (0.25f32, 0.75f32),
2456            (0.75f32, 0.75f32),
2457        ];
2458
2459        for y in draw.y..draw.bottom() {
2460            for x in draw.x..draw.right() {
2461                if !enable_ssaa {
2462                    let px = (x as f32 + 0.5) - cx;
2463                    let py = (y as f32 + 0.5) - cy;
2464                    let (tx, ty) = inv_transform.apply_f32(px, py);
2465                    let u = (tx + tile.width as f32 * 0.5).floor() as i32;
2466                    let v = (ty + tile.height as f32 * 0.5).floor() as i32;
2467                    if let Some(col) = tile.get_pixel(u, v) {
2468                        self.pixel(x, y, col, opacity)?;
2469                    }
2470                } else {
2471                    let mut r_sum = 0u32;
2472                    let mut g_sum = 0u32;
2473                    let mut b_sum = 0u32;
2474                    let mut weight = 0u32;
2475
2476                    for &(ox, oy) in &offsets {
2477                        let px = (x as f32 + ox) - cx;
2478                        let py = (y as f32 + oy) - cy;
2479                        let (tx, ty) = inv_transform.apply_f32(px, py);
2480                        let u = (tx + tile.width as f32 * 0.5).floor() as i32;
2481                        let v = (ty + tile.height as f32 * 0.5).floor() as i32;
2482                        if let Some(col) = tile.get_pixel(u, v) {
2483                            r_sum += col.r() as u32;
2484                            g_sum += col.g() as u32;
2485                            b_sum += col.b() as u32;
2486                            weight += 1;
2487                        }
2488                    }
2489
2490                    if let Some(w) = core::num::NonZeroU32::new(weight) {
2491                        let weight_val = w.get();
2492                        let r_avg = (r_sum / weight_val) as u8;
2493                        let g_avg = (g_sum / weight_val) as u8;
2494                        let b_avg = (b_sum / weight_val) as u8;
2495                        let color = Rgb565::new(r_avg, g_avg, b_avg);
2496                        let pix_opacity = ((weight * opacity as u32 + 2) / 4) as u8;
2497                        self.pixel(x, y, color, pix_opacity)?;
2498                    }
2499                }
2500            }
2501        }
2502        Ok(())
2503    }
2504
2505    /// Render a transformed image/tile with optional 2xSSAA.
2506    pub fn draw_image_transformed_ssaa(
2507        &mut self,
2508        rect: Rect,
2509        image: ImageRef<'_>,
2510        scale: f32,
2511        rotation_deg: f32,
2512        opacity: u8,
2513        enable_ssaa: bool,
2514    ) -> Result<(), D::Error> {
2515        let transform = Transform2D::rotation(rotation_deg).then(Transform2D::scale(scale, scale));
2516        let tile = TileRef::from_image(image, TileMode::None);
2517        self.draw_tile_transformed_ssaa(rect, tile, transform, opacity, enable_ssaa)
2518    }
2519}
2520
2521fn should_draw_at_opacity(x: i32, y: i32, opacity: u8) -> bool {
2522    if opacity == 255 {
2523        return true;
2524    }
2525    if opacity == 0 {
2526        return false;
2527    }
2528    let bayer4 = [
2529        [0u8, 8, 2, 10],
2530        [12, 4, 14, 6],
2531        [3, 11, 1, 9],
2532        [15, 7, 13, 5],
2533    ];
2534    let threshold = ((opacity as u16 * 16) / 255) as u8;
2535    let sample = bayer4[(y as usize) & 3][(x as usize) & 3];
2536    sample < threshold.max(1)
2537}
2538
2539fn lerp_rgb565(a: Rgb565, b: Rgb565, t: u8) -> Rgb565 {
2540    let t = t as u16;
2541    let inv = 255u16.saturating_sub(t);
2542    let r = ((a.r() as u16 * inv) + (b.r() as u16 * t)) / 255;
2543    let g = ((a.g() as u16 * inv) + (b.g() as u16 * t)) / 255;
2544    let bb = ((a.b() as u16 * inv) + (b.b() as u16 * t)) / 255;
2545    Rgb565::new(r as u8, g as u8, bb as u8)
2546}
2547
2548#[inline]
2549fn normalize_angle_deg(mut deg: f32) -> f32 {
2550    while deg < 0.0 {
2551        deg += 360.0;
2552    }
2553    while deg >= 360.0 {
2554        deg -= 360.0;
2555    }
2556    deg
2557}
2558
2559/// Exact (cos, sin) for a boundary angle that lands on a cardinal direction,
2560/// or `None` to fall back to a real trig call. Widgets built around a fixed
2561/// "12 o'clock" (or 3/6/9 o'clock) start angle -- the common case, e.g. a
2562/// sweeping-arc or gauge starting at -90 degrees -- hit this on every call
2563/// for that boundary, since only the other (animated) boundary ever lands on
2564/// a non-cardinal angle. Skips the `sin`/`cos` pair entirely for that
2565/// boundary instead of computing (and rounding) values that are always
2566/// exactly 0, 1, or -1.
2567#[inline]
2568fn cardinal_unit(deg: f32) -> Option<(f32, f32)> {
2569    const EPS: f32 = 1e-4;
2570    let normalized = normalize_angle_deg(deg);
2571    if (normalized - 0.0).abs() < EPS {
2572        Some((1.0, 0.0))
2573    } else if (normalized - 90.0).abs() < EPS {
2574        Some((0.0, 1.0))
2575    } else if (normalized - 180.0).abs() < EPS {
2576        Some((-1.0, 0.0))
2577    } else if (normalized - 270.0).abs() < EPS {
2578        Some((0.0, -1.0))
2579    } else {
2580        None
2581    }
2582}
2583
2584#[inline]
2585fn cross(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 {
2586    ux * vy - uy * vx
2587}
2588
2589fn apply_blend_mode(src: Rgb565, mode: BlendMode, backdrop: Rgb565) -> Rgb565 {
2590    match mode {
2591        BlendMode::Normal => src,
2592        BlendMode::Add => Rgb565::new(
2593            src.r().saturating_add(backdrop.r()),
2594            src.g().saturating_add(backdrop.g()),
2595            src.b().saturating_add(backdrop.b()),
2596        ),
2597        BlendMode::Multiply => Rgb565::new(
2598            ((src.r() as u16 * backdrop.r() as u16) / 31) as u8,
2599            ((src.g() as u16 * backdrop.g() as u16) / 63) as u8,
2600            ((src.b() as u16 * backdrop.b() as u16) / 31) as u8,
2601        ),
2602        BlendMode::Screen => Rgb565::new(
2603            (31 - ((31 - src.r() as u16) * (31 - backdrop.r() as u16) / 31)) as u8,
2604            (63 - ((63 - src.g() as u16) * (63 - backdrop.g() as u16) / 63)) as u8,
2605            (31 - ((31 - src.b() as u16) * (31 - backdrop.b() as u16) / 31)) as u8,
2606        ),
2607    }
2608}
2609
2610fn in_rounded_rect(x: i32, y: i32, rect: Rect, radius: u8) -> bool {
2611    if rect.is_empty() {
2612        return false;
2613    }
2614    let radius = radius as i32;
2615    if radius <= 0 {
2616        return rect.contains(x, y);
2617    }
2618
2619    let left = rect.x;
2620    let top = rect.y;
2621    let right = rect.right() - 1;
2622    let bottom = rect.bottom() - 1;
2623    let inner_left = left + radius;
2624    let inner_right = right - radius;
2625    let inner_top = top + radius;
2626    let inner_bottom = bottom - radius;
2627
2628    if (x >= inner_left && x <= inner_right) || (y >= inner_top && y <= inner_bottom) {
2629        return rect.contains(x, y);
2630    }
2631
2632    let (cx, cy) = if x < inner_left && y < inner_top {
2633        (inner_left, inner_top)
2634    } else if x > inner_right && y < inner_top {
2635        (inner_right, inner_top)
2636    } else if x < inner_left && y > inner_bottom {
2637        (inner_left, inner_bottom)
2638    } else if x > inner_right && y > inner_bottom {
2639        (inner_right, inner_bottom)
2640    } else {
2641        return rect.contains(x, y);
2642    };
2643
2644    let dx = x - cx;
2645    let dy = y - cy;
2646    dx * dx + dy * dy <= radius * radius
2647}
2648
2649fn line_len_at(text: &str, start: usize, max_chars: usize, wrap: TextWrap) -> (usize, bool) {
2650    let mut len = 0;
2651    let limit = match wrap {
2652        TextWrap::None => usize::MAX,
2653        TextWrap::Character => max_chars.max(1),
2654        TextWrap::Word => max_chars.max(1),
2655    };
2656    let mut last_ws_break = None;
2657
2658    for ch in text.chars().skip(start) {
2659        if ch == '\n' {
2660            return (len, true);
2661        }
2662        if matches!(wrap, TextWrap::Word) && ch.is_whitespace() {
2663            last_ws_break = Some(len + 1);
2664        }
2665        if len >= limit {
2666            if matches!(wrap, TextWrap::Word) {
2667                if let Some(idx) = last_ws_break {
2668                    return (idx, false);
2669                }
2670            }
2671            return (len, false);
2672        }
2673        len += 1;
2674    }
2675
2676    (len, false)
2677}
2678
2679fn count_lines(text: &str, max_chars: usize, wrap: TextWrap) -> usize {
2680    if text.is_empty() {
2681        return 1;
2682    }
2683    let char_count = text.chars().count();
2684    let mut lines = 0;
2685    let mut start = 0;
2686    while start < char_count {
2687        let (len, consumed_newline) = line_len_at(text, start, max_chars, wrap);
2688        lines += 1;
2689        start += len + usize::from(consumed_newline);
2690        if len == 0 && !consumed_newline {
2691            break;
2692        }
2693    }
2694    lines
2695}
2696
2697fn widest_line(text: &str, max_chars: usize, wrap: TextWrap) -> usize {
2698    let char_count = text.chars().count();
2699    let mut widest = 0;
2700    let mut start = 0;
2701    while start < char_count {
2702        let (len, consumed_newline) = line_len_at(text, start, max_chars, wrap);
2703        widest = widest.max(len);
2704        start += len + usize::from(consumed_newline);
2705        if len == 0 && !consumed_newline {
2706            break;
2707        }
2708    }
2709    widest
2710}
2711
2712fn kerning_adjust(prev: Option<char>, next: char, enabled: bool) -> i32 {
2713    if !enabled {
2714        return 0;
2715    }
2716    match (prev, next) {
2717        (Some('A'), 'V') | (Some('A'), 'W') | (Some('T'), 'o') | (Some('L'), 'T') => -1,
2718        _ => 0,
2719    }
2720}
2721
2722#[cfg(test)]
2723mod tests {
2724    use super::*;
2725
2726    #[test]
2727    fn test_transform2d_is_identity() {
2728        let id = Transform2D::IDENTITY;
2729        assert!(id.is_identity());
2730        assert_eq!(id.apply(10, 20), (10, 20));
2731
2732        let tr = Transform2D::translation(5.0, 10.0);
2733        assert!(!tr.is_identity());
2734        assert_eq!(tr.apply(10, 20), (15, 30));
2735    }
2736
2737    #[test]
2738    fn test_fill_circle_scanline_spans_correctness() {
2739        let mut buf = crate::test_buffer::TestBuffer::new(50, 50);
2740        let mut ctx = RenderCtx::new(&mut buf, Rect::new(0, 0, 50, 50));
2741
2742        // Draw a circle of radius 10 at center (25, 25)
2743        ctx.fill_circle(25, 25, 10, Rgb565::RED).unwrap();
2744
2745        // Center pixel must be red
2746        assert_eq!(buf.pixel_at(25, 25), Some(Rgb565::RED));
2747
2748        // Points inside radius 10 must be red (e.g. 25 + 7, 25 + 7 => dist^2 = 98 <= 100)
2749        assert_eq!(buf.pixel_at(32, 32), Some(Rgb565::RED));
2750
2751        // Points outside radius 10 must remain black (e.g. 25 + 11, 25)
2752        assert_eq!(buf.pixel_at(37, 25), Some(Rgb565::BLACK));
2753        assert_eq!(buf.pixel_at(25, 37), Some(Rgb565::BLACK));
2754    }
2755
2756    #[test]
2757    fn test_cardinal_unit_exact_values_and_fallback() {
2758        assert_eq!(cardinal_unit(0.0), Some((1.0, 0.0)));
2759        assert_eq!(cardinal_unit(90.0), Some((0.0, 1.0)));
2760        assert_eq!(cardinal_unit(180.0), Some((-1.0, 0.0)));
2761        assert_eq!(cardinal_unit(270.0), Some((0.0, -1.0)));
2762        // -90 degrees normalizes to 270 -- the common "12 o'clock start"
2763        // sweeping-arc/gauge convention.
2764        assert_eq!(cardinal_unit(-90.0), Some((0.0, -1.0)));
2765        // A non-cardinal angle (or one more than EPS off a cardinal one)
2766        // must fall through to a real trig call.
2767        assert_eq!(cardinal_unit(45.0), None);
2768        assert_eq!(cardinal_unit(89.99), None);
2769    }
2770
2771    #[test]
2772    fn test_cardinal_unit_agrees_with_real_trig_at_cardinal_angles() {
2773        // The fast path's exact 0/1/-1 constants must be numerically
2774        // consistent with what a real sin/cos call would produce for the
2775        // same angle (up to float rounding) -- this is the actual property
2776        // that makes skipping the trig call safe, independent of any
2777        // downstream rasterization sensitivity near sector boundaries.
2778        for deg in [0.0_f32, 90.0, 180.0, 270.0, -90.0, 450.0] {
2779            let (fast_c, fast_s) = cardinal_unit(deg).expect("cardinal angle");
2780            let (real_c, real_s) = (deg.to_radians().cos(), deg.to_radians().sin());
2781            assert!(
2782                (fast_c - real_c).abs() < 1e-6,
2783                "cos mismatch at {deg}: fast={fast_c} real={real_c}"
2784            );
2785            assert!(
2786                (fast_s - real_s).abs() < 1e-6,
2787                "sin mismatch at {deg}: fast={fast_s} real={real_s}"
2788            );
2789        }
2790    }
2791
2792    #[test]
2793    fn test_fill_sector_sweep_cardinal_fast_path_renders() {
2794        // Smoke-test the fast path end-to-end: a start angle that hits
2795        // cardinal_unit must still paint a plausible, growing sector (the
2796        // per-pixel geometry test is unchanged either way -- only how the
2797        // boundary direction vectors are obtained differs).
2798        let mut buf = crate::test_buffer::TestBuffer::new(50, 50);
2799        let mut ctx = RenderCtx::new(&mut buf, Rect::new(0, 0, 50, 50));
2800        ctx.fill_sector_sweep(25, 25, 20, -90.0, 90.0, Rgb565::RED)
2801            .unwrap();
2802        assert!(buf.count_color(Rgb565::RED) > 0);
2803        // A quarter sweep from 12 o'clock (clockwise, since sweep is
2804        // positive/ccw in this atan2-angle convention going toward 3
2805        // o'clock) should light up the pixel directly right of center but
2806        // not the one directly below it.
2807        assert_eq!(buf.pixel_at(40, 25), Some(Rgb565::RED));
2808        assert_eq!(buf.pixel_at(25, 40), Some(Rgb565::BLACK));
2809    }
2810}