Skip to main content

embedded_gui/
render.rs

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