Skip to main content

embedded_gui/render/
mod.rs

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