Skip to main content

embedded_gui/render/
mod.rs

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