Skip to main content

embedded_gui/
render.rs

1use core::marker::PhantomData;
2
3use embedded_graphics_core::{
4    Pixel,
5    draw_target::DrawTarget,
6    geometry::Point,
7    pixelcolor::{Rgb565, RgbColor},
8};
9
10#[cfg(not(feature = "std"))]
11use crate::math::F32Ext as _;
12use crate::{
13    font::{FontId, glyph_rows},
14    geometry::Rect,
15    image::{ImageFit, ImageRef},
16    style::{Border, GradientDirection, LinearGradient},
17    text,
18};
19
20pub const CHAR_WIDTH: u32 = 4;
21pub const CHAR_HEIGHT: u32 = 6;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum TextAlign {
25    Left,
26    Center,
27    Right,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum VerticalAlign {
32    Top,
33    Middle,
34    Bottom,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum TextWrap {
39    None,
40    Character,
41    Word,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum TextOverflow {
46    Clip,
47    Ellipsis,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum EllipsisMode {
52    ThreeDots,
53    SingleGlyph,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum TextOverflowPolicy {
58    Global(TextOverflow),
59    WrapThenEllipsis { max_lines: u8 },
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub struct TextStyle {
64    pub color: Rgb565,
65    pub font: FontId,
66    pub opacity: u8,
67    pub align: TextAlign,
68    pub vertical_align: VerticalAlign,
69    pub wrap: TextWrap,
70    pub overflow: TextOverflow,
71    pub overflow_policy: TextOverflowPolicy,
72    pub kerning: bool,
73    pub max_lines: Option<u8>,
74    pub ellipsis: EllipsisMode,
75    pub line_spacing: u8,
76}
77
78impl TextStyle {
79    pub const fn new(color: Rgb565) -> Self {
80        Self {
81            color,
82            font: FontId::Tiny3x5,
83            opacity: 255,
84            align: TextAlign::Left,
85            vertical_align: VerticalAlign::Top,
86            wrap: TextWrap::None,
87            overflow: TextOverflow::Clip,
88            overflow_policy: TextOverflowPolicy::Global(TextOverflow::Clip),
89            kerning: false,
90            max_lines: None,
91            ellipsis: EllipsisMode::ThreeDots,
92            line_spacing: 1,
93        }
94    }
95
96    pub const fn centered(mut self) -> Self {
97        self.align = TextAlign::Center;
98        self.vertical_align = VerticalAlign::Middle;
99        self
100    }
101
102    pub const fn with_align(mut self, align: TextAlign) -> Self {
103        self.align = align;
104        self
105    }
106
107    pub const fn with_vertical_align(mut self, align: VerticalAlign) -> Self {
108        self.vertical_align = align;
109        self
110    }
111
112    pub const fn with_wrap(mut self, wrap: TextWrap) -> Self {
113        self.wrap = wrap;
114        self
115    }
116
117    pub const fn with_line_spacing(mut self, spacing: u8) -> Self {
118        self.line_spacing = spacing;
119        self
120    }
121
122    pub const fn with_overflow(mut self, overflow: TextOverflow) -> Self {
123        self.overflow = overflow;
124        self.overflow_policy = TextOverflowPolicy::Global(overflow);
125        self
126    }
127
128    pub const fn with_kerning(mut self, kerning: bool) -> Self {
129        self.kerning = kerning;
130        self
131    }
132
133    pub const fn with_max_lines(mut self, max_lines: Option<u8>) -> Self {
134        self.max_lines = max_lines;
135        self
136    }
137
138    pub const fn with_ellipsis_mode(mut self, ellipsis: EllipsisMode) -> Self {
139        self.ellipsis = ellipsis;
140        self
141    }
142
143    pub const fn with_overflow_policy(mut self, policy: TextOverflowPolicy) -> Self {
144        self.overflow_policy = policy;
145        self
146    }
147
148    pub const fn with_opacity(mut self, opacity: u8) -> Self {
149        self.opacity = opacity;
150        self
151    }
152
153    pub const fn with_font(mut self, font: FontId) -> Self {
154        self.font = font;
155        self
156    }
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct TextMetrics {
161    pub width: u32,
162    pub height: u32,
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub enum RenderQuality {
167    Low,
168    Medium,
169    High,
170}
171
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub enum AntiAliasMode {
174    None,
175    Coverage,
176    Subpixel,
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub struct StrokeStyle {
181    pub color: Rgb565,
182    pub width: u8,
183    pub antialias: bool,
184    pub antialias_mode: AntiAliasMode,
185    pub cap: StrokeCap,
186    pub join: StrokeJoin,
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub enum StrokeCap {
191    Butt,
192    Round,
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub enum StrokeJoin {
197    Miter,
198    Round,
199}
200
201#[derive(Clone, Copy, Debug, PartialEq)]
202pub struct Transform2D {
203    pub m11: f32,
204    pub m12: f32,
205    pub m21: f32,
206    pub m22: f32,
207    pub tx: f32,
208    pub ty: f32,
209}
210
211impl Transform2D {
212    pub const IDENTITY: Self = Self {
213        m11: 1.0,
214        m12: 0.0,
215        m21: 0.0,
216        m22: 1.0,
217        tx: 0.0,
218        ty: 0.0,
219    };
220
221    pub const fn translation(x: f32, y: f32) -> Self {
222        Self {
223            tx: x,
224            ty: y,
225            ..Self::IDENTITY
226        }
227    }
228
229    pub const fn scale(x: f32, y: f32) -> Self {
230        Self {
231            m11: x,
232            m22: y,
233            ..Self::IDENTITY
234        }
235    }
236
237    pub fn rotation(deg: f32) -> Self {
238        let r = deg.to_radians();
239        Self {
240            m11: r.cos(),
241            m12: -r.sin(),
242            m21: r.sin(),
243            m22: r.cos(),
244            ..Self::IDENTITY
245        }
246    }
247
248    pub fn skew(x_deg: f32, y_deg: f32) -> Self {
249        Self {
250            m12: x_deg.to_radians().tan(),
251            m21: y_deg.to_radians().tan(),
252            ..Self::IDENTITY
253        }
254    }
255
256    pub fn then(self, rhs: Self) -> Self {
257        Self {
258            m11: self.m11 * rhs.m11 + self.m12 * rhs.m21,
259            m12: self.m11 * rhs.m12 + self.m12 * rhs.m22,
260            m21: self.m21 * rhs.m11 + self.m22 * rhs.m21,
261            m22: self.m21 * rhs.m12 + self.m22 * rhs.m22,
262            tx: self.m11 * rhs.tx + self.m12 * rhs.ty + self.tx,
263            ty: self.m21 * rhs.tx + self.m22 * rhs.ty + self.ty,
264        }
265    }
266
267    pub fn apply(self, x: i32, y: i32) -> (i32, i32) {
268        let xf = x as f32;
269        let yf = y as f32;
270        (
271            (self.m11 * xf + self.m12 * yf + self.tx).round() as i32,
272            (self.m21 * xf + self.m22 * yf + self.ty).round() as i32,
273        )
274    }
275}
276
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub enum BlendMode {
279    Normal,
280    Add,
281    Multiply,
282    Screen,
283}
284
285/// Capability trait for draw targets that can read back a pixel they've
286/// already written. Optional: the default opacity/blend APIs on
287/// [`RenderCtx`] only require [`DrawTarget`] and approximate translucency
288/// with ordered dithering, so they keep working on write-only displays.
289/// Implementing `PixelRead` additionally unlocks the `*_true_alpha` methods,
290/// which composite against the destination's actual current contents
291/// instead of dithering.
292pub trait PixelRead: DrawTarget {
293    fn get_pixel(&self, point: Point) -> Self::Color;
294}
295
296/// Pixel-plotting policy for a [`RenderCtx`]. Selected by the ctx's `C` type
297/// parameter so the *same* drawing calls composite differently depending on
298/// the target's capabilities — with no runtime branch and no specialization.
299///
300/// - [`Dither`] (the default) approximates translucency with ordered dithering
301///   and works on any write-only [`DrawTarget`].
302/// - [`Blend`] performs true per-pixel alpha compositing and requires a
303///   readback-capable target ([`PixelRead`]), e.g. a
304///   [`Framebuffer`](crate::Framebuffer).
305///
306/// `plot` receives screen-space coords already transformed and clipped, the
307/// layer-combined `opacity`, and the active layer blend mode + backdrop.
308pub trait Compositor<D: DrawTarget<Color = Rgb565>> {
309    fn plot(
310        target: &mut D,
311        x: i32,
312        y: i32,
313        color: Rgb565,
314        opacity: u8,
315        blend: BlendMode,
316        backdrop: Rgb565,
317    ) -> Result<(), D::Error>;
318}
319
320/// Ordered-dither compositor (default). No readback required.
321#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
322pub struct Dither;
323
324/// True alpha-blending compositor. Requires a [`PixelRead`] target.
325#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
326pub struct Blend;
327
328impl<D: DrawTarget<Color = Rgb565>> Compositor<D> for Dither {
329    fn plot(
330        target: &mut D,
331        x: i32,
332        y: i32,
333        color: Rgb565,
334        opacity: u8,
335        blend: BlendMode,
336        backdrop: Rgb565,
337    ) -> Result<(), D::Error> {
338        if !should_draw_at_opacity(x, y, opacity) {
339            return Ok(());
340        }
341        let color = apply_blend_mode(color, blend, backdrop);
342        target.draw_iter([Pixel(Point::new(x, y), color)])
343    }
344}
345
346impl<D: DrawTarget<Color = Rgb565> + PixelRead> Compositor<D> for Blend {
347    fn plot(
348        target: &mut D,
349        x: i32,
350        y: i32,
351        color: Rgb565,
352        opacity: u8,
353        blend: BlendMode,
354        backdrop: Rgb565,
355    ) -> Result<(), D::Error> {
356        if opacity == 0 {
357            return Ok(());
358        }
359        let bg = target.get_pixel(Point::new(x, y));
360        let blended = lerp_rgb565(bg, color, opacity);
361        let blended = apply_blend_mode(blended, blend, backdrop);
362        target.draw_iter([Pixel(Point::new(x, y), blended)])
363    }
364}
365
366#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367pub enum ColorFormat {
368    Rgb565,
369    Rgb888,
370    Argb8888,
371}
372
373#[derive(Clone, Copy, Debug, PartialEq, Eq)]
374pub struct RenderBackendCaps {
375    pub color_format: ColorFormat,
376    pub supports_layers: bool,
377    pub supports_subpixel: bool,
378}
379
380impl RenderBackendCaps {
381    pub const fn software_rgb565() -> Self {
382        Self {
383            color_format: ColorFormat::Rgb565,
384            supports_layers: true,
385            supports_subpixel: false,
386        }
387    }
388}
389
390#[derive(Clone, Copy, Debug, PartialEq, Eq)]
391pub struct LayerState {
392    pub opacity: u8,
393    pub blend: BlendMode,
394    pub backdrop: Rgb565,
395}
396
397impl LayerState {
398    pub const fn normal() -> Self {
399        Self {
400            opacity: 255,
401            blend: BlendMode::Normal,
402            backdrop: Rgb565::BLACK,
403        }
404    }
405}
406
407impl StrokeStyle {
408    pub const fn new(color: Rgb565) -> Self {
409        Self {
410            color,
411            width: 1,
412            antialias: false,
413            antialias_mode: AntiAliasMode::None,
414            cap: StrokeCap::Butt,
415            join: StrokeJoin::Miter,
416        }
417    }
418
419    pub const fn with_width(mut self, width: u8) -> Self {
420        self.width = if width == 0 { 1 } else { width };
421        self
422    }
423
424    pub const fn with_antialias(mut self, antialias: bool) -> Self {
425        self.antialias = antialias;
426        if antialias {
427            if let AntiAliasMode::None = self.antialias_mode {
428                self.antialias_mode = AntiAliasMode::Coverage;
429            }
430        }
431        if !antialias {
432            self.antialias_mode = AntiAliasMode::None;
433        }
434        self
435    }
436
437    pub const fn with_antialias_mode(mut self, mode: AntiAliasMode) -> Self {
438        self.antialias_mode = mode;
439        self.antialias = !matches!(mode, AntiAliasMode::None);
440        self
441    }
442
443    pub const fn with_cap(mut self, cap: StrokeCap) -> Self {
444        self.cap = cap;
445        self
446    }
447
448    pub const fn with_join(mut self, join: StrokeJoin) -> Self {
449        self.join = join;
450        self
451    }
452}
453
454pub struct RenderCtx<'a, D, C = Dither>
455where
456    D: DrawTarget<Color = Rgb565>,
457{
458    target: &'a mut D,
459    clip: Rect,
460    dirty: Option<Rect>,
461    quality: RenderQuality,
462    backend_caps: RenderBackendCaps,
463    transform_stack: [Transform2D; 8],
464    transform_len: usize,
465    layer_stack: [LayerState; 8],
466    layer_len: usize,
467    _compositor: PhantomData<C>,
468}
469
470impl<'a, D> RenderCtx<'a, D, Dither>
471where
472    D: DrawTarget<Color = Rgb565>,
473{
474    pub fn new(target: &'a mut D, viewport: Rect) -> Self {
475        Self {
476            target,
477            clip: viewport,
478            dirty: None,
479            quality: RenderQuality::High,
480            backend_caps: RenderBackendCaps::software_rgb565(),
481            transform_stack: [Transform2D::IDENTITY; 8],
482            transform_len: 1,
483            layer_stack: [LayerState::normal(); 8],
484            layer_len: 1,
485            _compositor: PhantomData,
486        }
487    }
488
489    pub fn with_dirty(target: &'a mut D, viewport: Rect, dirty: Rect) -> Self {
490        Self {
491            target,
492            clip: viewport,
493            dirty: Some(dirty),
494            quality: RenderQuality::High,
495            backend_caps: RenderBackendCaps::software_rgb565(),
496            transform_stack: [Transform2D::IDENTITY; 8],
497            transform_len: 1,
498            layer_stack: [LayerState::normal(); 8],
499            layer_len: 1,
500            _compositor: PhantomData,
501        }
502    }
503}
504
505impl<'a, D> RenderCtx<'a, D, Blend>
506where
507    D: DrawTarget<Color = Rgb565> + PixelRead,
508{
509    /// Like [`RenderCtx::new`], but every drawing call alpha-composites against
510    /// the target's current contents (true blending) instead of dithering.
511    /// Requires a readback-capable target ([`PixelRead`]), e.g. a
512    /// [`Framebuffer`](crate::Framebuffer).
513    pub fn compositing(target: &'a mut D, viewport: Rect) -> Self {
514        Self {
515            target,
516            clip: viewport,
517            dirty: None,
518            quality: RenderQuality::High,
519            backend_caps: RenderBackendCaps::software_rgb565(),
520            transform_stack: [Transform2D::IDENTITY; 8],
521            transform_len: 1,
522            layer_stack: [LayerState::normal(); 8],
523            layer_len: 1,
524            _compositor: PhantomData,
525        }
526    }
527}
528
529impl<'a, D, C> RenderCtx<'a, D, C>
530where
531    D: DrawTarget<Color = Rgb565>,
532    C: Compositor<D>,
533{
534    pub const fn clip(&self) -> Rect {
535        self.clip
536    }
537
538    pub fn set_clip(&mut self, clip: Rect) {
539        self.clip = clip;
540    }
541
542    /// Draws any `embedded_graphics::Drawable` (e.g. an `embedded_text::TextBox`
543    /// built via [`crate::interop::text::text_box`], or an arranged
544    /// `embedded_layout` view group) onto this context's target, clipped to
545    /// the current [`clip`](Self::clip) rect.
546    #[cfg(any(feature = "embedded-text", feature = "embedded-layout"))]
547    pub fn draw_embedded_graphics<T>(&mut self, drawable: &T) -> Result<T::Output, D::Error>
548    where
549        T: embedded_graphics::Drawable<Color = Rgb565>,
550    {
551        use embedded_graphics::draw_target::DrawTargetExt;
552
553        let clip_rect: embedded_graphics::primitives::Rectangle = self.clip.into();
554        let mut clipped = self.target.clipped(&clip_rect);
555        drawable.draw(&mut clipped)
556    }
557
558    pub const fn quality(&self) -> RenderQuality {
559        self.quality
560    }
561
562    pub fn set_quality(&mut self, quality: RenderQuality) {
563        self.quality = quality;
564    }
565
566    pub const fn backend_caps(&self) -> RenderBackendCaps {
567        self.backend_caps
568    }
569
570    pub fn set_backend_caps(&mut self, caps: RenderBackendCaps) {
571        self.backend_caps = caps;
572    }
573
574    pub fn push_transform(&mut self, transform: Transform2D) {
575        if self.transform_len >= self.transform_stack.len() {
576            return;
577        }
578        let current = self.current_transform();
579        self.transform_stack[self.transform_len] = current.then(transform);
580        self.transform_len += 1;
581    }
582
583    pub fn pop_transform(&mut self) {
584        if self.transform_len > 1 {
585            self.transform_len -= 1;
586        }
587    }
588
589    pub fn translate(&mut self, x: f32, y: f32) {
590        self.push_transform(Transform2D::translation(x, y));
591    }
592
593    pub fn scale(&mut self, x: f32, y: f32) {
594        self.push_transform(Transform2D::scale(x, y));
595    }
596
597    pub fn rotate(&mut self, deg: f32) {
598        self.push_transform(Transform2D::rotation(deg));
599    }
600
601    pub fn skew(&mut self, x_deg: f32, y_deg: f32) {
602        self.push_transform(Transform2D::skew(x_deg, y_deg));
603    }
604
605    pub fn push_layer(&mut self, layer: LayerState) {
606        if self.layer_len >= self.layer_stack.len() {
607            return;
608        }
609        let current = self.current_layer();
610        self.layer_stack[self.layer_len] = LayerState {
611            opacity: ((current.opacity as u16 * layer.opacity as u16) / 255) as u8,
612            blend: layer.blend,
613            backdrop: layer.backdrop,
614        };
615        self.layer_len += 1;
616    }
617
618    pub fn pop_layer(&mut self) {
619        if self.layer_len > 1 {
620            self.layer_len -= 1;
621        }
622    }
623
624    pub const fn shadow_spread_for(&self, spread: u8) -> u8 {
625        match self.quality {
626            RenderQuality::Low => 0,
627            RenderQuality::Medium => {
628                if spread > 1 {
629                    1
630                } else {
631                    spread
632                }
633            }
634            RenderQuality::High => spread,
635        }
636    }
637
638    pub fn fill_rect(&mut self, rect: Rect, color: Rgb565) -> Result<(), D::Error> {
639        self.fill_rect_alpha(rect, color, 255)
640    }
641
642    pub fn fill_rect_alpha(
643        &mut self,
644        rect: Rect,
645        color: Rgb565,
646        opacity: u8,
647    ) -> Result<(), D::Error> {
648        self.fill_rounded_rect_alpha(rect, 0, color, opacity)
649    }
650
651    pub fn fill_rounded_rect(
652        &mut self,
653        rect: Rect,
654        radius: u8,
655        color: Rgb565,
656    ) -> Result<(), D::Error> {
657        self.fill_rounded_rect_alpha(rect, radius, color, 255)
658    }
659
660    pub fn fill_rounded_rect_alpha(
661        &mut self,
662        rect: Rect,
663        radius: u8,
664        color: Rgb565,
665        opacity: u8,
666    ) -> Result<(), D::Error> {
667        let draw = self.visible_rect(rect);
668        if draw.is_empty() || opacity == 0 {
669            return Ok(());
670        }
671        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
672
673        for y in draw.y..draw.bottom() {
674            for x in draw.x..draw.right() {
675                if !in_rounded_rect(x, y, rect, radius) {
676                    continue;
677                }
678                self.pixel(x, y, color, opacity)?;
679            }
680        }
681        Ok(())
682    }
683
684    pub fn fill_rounded_rect_gradient_alpha(
685        &mut self,
686        rect: Rect,
687        radius: u8,
688        gradient: LinearGradient,
689        opacity: u8,
690    ) -> Result<(), D::Error> {
691        let draw = self.visible_rect(rect);
692        if draw.is_empty() || opacity == 0 {
693            return Ok(());
694        }
695        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
696        let denom = match gradient.direction {
697            GradientDirection::Horizontal => rect.w.saturating_sub(1).max(1),
698            GradientDirection::Vertical => rect.h.saturating_sub(1).max(1),
699        };
700
701        for y in draw.y..draw.bottom() {
702            for x in draw.x..draw.right() {
703                if !in_rounded_rect(x, y, rect, radius) {
704                    continue;
705                }
706                let numer = match gradient.direction {
707                    GradientDirection::Horizontal => (x - rect.x).max(0) as u32,
708                    GradientDirection::Vertical => (y - rect.y).max(0) as u32,
709                }
710                .min(denom);
711                let mut t = ((numer * 255) / denom) as u8;
712                t = match self.quality {
713                    RenderQuality::Low => 128,
714                    RenderQuality::Medium => (t / 64) * 64,
715                    RenderQuality::High => t,
716                };
717                let color = lerp_rgb565(gradient.start, gradient.end, t);
718                self.pixel(x, y, color, opacity)?;
719            }
720        }
721        Ok(())
722    }
723
724    pub fn stroke_rect(&mut self, rect: Rect, border: Border) -> Result<(), D::Error> {
725        self.stroke_rect_alpha(rect, border, 255)
726    }
727
728    pub fn stroke_rect_alpha(
729        &mut self,
730        rect: Rect,
731        border: Border,
732        opacity: u8,
733    ) -> Result<(), D::Error> {
734        if border.width == 0 || rect.is_empty() {
735            return Ok(());
736        }
737
738        for i in 0..border.width as i32 {
739            let w = rect.w.saturating_sub((i as u32).saturating_mul(2));
740            let h = rect.h.saturating_sub((i as u32).saturating_mul(2));
741            if w == 0 || h == 0 {
742                break;
743            }
744            let r = Rect::new(rect.x + i, rect.y + i, w, h);
745            self.fill_rect_alpha(Rect::new(r.x, r.y, r.w, 1), border.color, opacity)?;
746            if r.h > 1 {
747                self.fill_rect_alpha(
748                    Rect::new(r.x, r.bottom() - 1, r.w, 1),
749                    border.color,
750                    opacity,
751                )?;
752            }
753            if r.h > 2 {
754                self.fill_rect_alpha(Rect::new(r.x, r.y + 1, 1, r.h - 2), border.color, opacity)?;
755                if r.w > 1 {
756                    self.fill_rect_alpha(
757                        Rect::new(r.right() - 1, r.y + 1, 1, r.h - 2),
758                        border.color,
759                        opacity,
760                    )?;
761                }
762            }
763        }
764        Ok(())
765    }
766
767    pub fn stroke_rounded_rect(
768        &mut self,
769        rect: Rect,
770        radius: u8,
771        border: Border,
772    ) -> Result<(), D::Error> {
773        self.stroke_rounded_rect_alpha(rect, radius, border, 255)
774    }
775
776    pub fn stroke_rounded_rect_alpha(
777        &mut self,
778        rect: Rect,
779        radius: u8,
780        border: Border,
781        opacity: u8,
782    ) -> Result<(), D::Error> {
783        if border.width == 0 || rect.is_empty() || opacity == 0 {
784            return Ok(());
785        }
786        let draw = self.visible_rect(rect);
787        if draw.is_empty() {
788            return Ok(());
789        }
790
791        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
792        for y in draw.y..draw.bottom() {
793            for x in draw.x..draw.right() {
794                if !in_rounded_rect(x, y, rect, radius) {
795                    continue;
796                }
797
798                let mut inner_hit = false;
799                let mut i = 1u8;
800                while i < border.width {
801                    let inset = i as i32;
802                    let inner = Rect::new(
803                        rect.x + inset,
804                        rect.y + inset,
805                        rect.w.saturating_sub((i as u32) * 2),
806                        rect.h.saturating_sub((i as u32) * 2),
807                    );
808                    let inner_radius = radius.saturating_sub(i);
809                    if !inner.is_empty() && in_rounded_rect(x, y, inner, inner_radius) {
810                        inner_hit = true;
811                        break;
812                    }
813                    i += 1;
814                }
815
816                if !inner_hit {
817                    self.pixel(x, y, border.color, opacity)?;
818                }
819            }
820        }
821        Ok(())
822    }
823
824    pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Rgb565) -> Result<(), D::Error> {
825        self.draw_text_with_font(x, y, text, color, FontId::Tiny3x5)
826    }
827
828    pub fn draw_text_with_font(
829        &mut self,
830        x: i32,
831        y: i32,
832        text: &str,
833        color: Rgb565,
834        font: FontId,
835    ) -> Result<(), D::Error> {
836        let advance = font.advance() as i32;
837        let line_h = font.line_height() as i32;
838        let mut cursor_x = x;
839        let mut cursor_y = y;
840        for ch in text.chars() {
841            if ch == '\n' {
842                cursor_x = x;
843                cursor_y += line_h;
844                continue;
845            }
846            self.draw_char_with_font(cursor_x, cursor_y, ch, color, 255, font)?;
847            cursor_x += advance;
848        }
849        Ok(())
850    }
851
852    pub fn draw_text_in(
853        &mut self,
854        rect: Rect,
855        text: &str,
856        style: TextStyle,
857    ) -> Result<(), D::Error> {
858        self.draw_text_in_with_font(rect, text, style, style.font)
859    }
860
861    pub fn draw_text_shaped_in<S, const N: usize>(
862        &mut self,
863        rect: Rect,
864        text: &str,
865        style: TextStyle,
866        shaper: &S,
867        config: crate::text::ShapingConfig,
868    ) -> Result<(), D::Error>
869    where
870        S: crate::text::TextShaper,
871    {
872        if rect.is_empty() {
873            return Ok(());
874        }
875        let mut shaped = heapless::Vec::<crate::text::ShapedGlyph, N>::new();
876        shaper.shape(text, config, &mut shaped);
877        if shaped.is_empty() {
878            return Ok(());
879        }
880        let mut x = rect.x;
881        let y = rect.y + rect.h.saturating_sub(style.font.line_height()) as i32 / 2;
882        for glyph in shaped {
883            self.draw_char_with_font(x, y, glyph.ch, style.color, style.opacity, style.font)?;
884            x += (glyph.x_advance as i32).max(1) * style.font.advance() as i32;
885            if x >= rect.right() {
886                break;
887            }
888        }
889        Ok(())
890    }
891
892    pub fn draw_text_in_with_font(
893        &mut self,
894        rect: Rect,
895        text: &str,
896        style: TextStyle,
897        font: FontId,
898    ) -> Result<(), D::Error> {
899        if rect.is_empty() {
900            return Ok(());
901        }
902
903        let advance = font.advance();
904        let line_h = font.line_height();
905        let max_chars = (rect.w / advance).max(1) as usize;
906        let char_count = text.chars().count();
907        let line_count = count_lines(text, max_chars, style.wrap).max(1);
908        let line_step = line_h + style.line_spacing as u32;
909        let total_h = line_count as u32 * line_h
910            + line_count.saturating_sub(1) as u32 * style.line_spacing as u32;
911        let mut y = match style.vertical_align {
912            VerticalAlign::Top => rect.y,
913            VerticalAlign::Middle => rect.y + rect.h.saturating_sub(total_h) as i32 / 2,
914            VerticalAlign::Bottom => rect.y + rect.h.saturating_sub(total_h) as i32,
915        };
916
917        let mut start = 0;
918        let mut rendered_lines = 0u8;
919        let max_lines = match style.overflow_policy {
920            TextOverflowPolicy::WrapThenEllipsis { max_lines } => max_lines.max(1),
921            TextOverflowPolicy::Global(_) => style.max_lines.unwrap_or(u8::MAX),
922        };
923        while start < char_count {
924            if rendered_lines >= max_lines {
925                break;
926            }
927            let (len, consumed_newline) = line_len_at(text, start, max_chars, style.wrap);
928            let mut draw_len = len;
929            let is_last_allowed_line = rendered_lines.saturating_add(1) >= max_lines;
930            let use_ellipsis = match style.overflow_policy {
931                TextOverflowPolicy::WrapThenEllipsis { .. } => is_last_allowed_line,
932                TextOverflowPolicy::Global(mode) => mode == TextOverflow::Ellipsis,
933            };
934            if use_ellipsis
935                && ((!consumed_newline && start + len < char_count) || is_last_allowed_line)
936            {
937                let ellipsis_width = match style.ellipsis {
938                    EllipsisMode::ThreeDots => 3usize,
939                    EllipsisMode::SingleGlyph => 1usize,
940                };
941                if len > ellipsis_width {
942                    draw_len = len - ellipsis_width;
943                }
944            }
945            let line_w = self.substring_width(text, start, draw_len, font, style.kerning);
946            let x = match style.align {
947                TextAlign::Left => rect.x,
948                TextAlign::Center => rect.x + rect.w.saturating_sub(line_w) as i32 / 2,
949                TextAlign::Right => rect.x + rect.w.saturating_sub(line_w) as i32,
950            };
951            self.draw_chars_with_font(
952                x,
953                y,
954                text,
955                start,
956                draw_len,
957                style.color,
958                style.opacity,
959                font,
960                style.kerning,
961            )?;
962            if draw_len < len && use_ellipsis {
963                let token = match style.ellipsis {
964                    EllipsisMode::ThreeDots => "...",
965                    EllipsisMode::SingleGlyph => ".",
966                };
967                self.draw_text_with_font(x + line_w as i32, y, token, style.color, font)?;
968            }
969            y += line_step as i32;
970            rendered_lines = rendered_lines.saturating_add(1);
971            start += len + usize::from(consumed_newline);
972            if style.wrap == TextWrap::Word && start < char_count {
973                while text.chars().nth(start).is_some_and(|ch| ch == ' ') {
974                    start += 1;
975                }
976            }
977            if len == 0 && !consumed_newline {
978                break;
979            }
980        }
981
982        Ok(())
983    }
984
985    pub fn draw_line_in(&mut self, rect: Rect, line: text::Line<'_>) -> Result<(), D::Error> {
986        if rect.is_empty() {
987            return Ok(());
988        }
989
990        self.draw_line_segment_in(rect, line, 0, line.width_chars())
991    }
992
993    pub fn draw_line(
994        &mut self,
995        x0: i32,
996        y0: i32,
997        x1: i32,
998        y1: i32,
999        color: Rgb565,
1000    ) -> Result<(), D::Error> {
1001        self.draw_line_styled(x0, y0, x1, y1, StrokeStyle::new(color))
1002    }
1003
1004    pub fn draw_line_styled(
1005        &mut self,
1006        x0: i32,
1007        y0: i32,
1008        x1: i32,
1009        y1: i32,
1010        style: StrokeStyle,
1011    ) -> Result<(), D::Error> {
1012        let mut x = x0;
1013        let mut y = y0;
1014        let dx = (x1 - x0).abs();
1015        let sx = if x0 < x1 { 1 } else { -1 };
1016        let dy = -(y1 - y0).abs();
1017        let sy = if y0 < y1 { 1 } else { -1 };
1018        let mut err = dx + dy;
1019        let half = (style.width as i32 / 2).max(0);
1020        let opacity = self.stroke_opacity(style);
1021
1022        loop {
1023            for oy in -half..=half {
1024                for ox in -half..=half {
1025                    self.pixel(x + ox, y + oy, style.color, opacity)?;
1026                }
1027            }
1028            if style.cap == StrokeCap::Round {
1029                self.fill_circle(x0, y0, half.max(1) as u32, style.color)?;
1030                self.fill_circle(x1, y1, half.max(1) as u32, style.color)?;
1031            }
1032            if x == x1 && y == y1 {
1033                break;
1034            }
1035            let e2 = 2 * err;
1036            if e2 >= dy {
1037                err += dy;
1038                x += sx;
1039            }
1040            if e2 <= dx {
1041                err += dx;
1042                y += sy;
1043            }
1044        }
1045        Ok(())
1046    }
1047
1048    pub fn fill_circle(
1049        &mut self,
1050        center_x: i32,
1051        center_y: i32,
1052        radius: u32,
1053        color: Rgb565,
1054    ) -> Result<(), D::Error> {
1055        let radius = radius as i32;
1056        if radius <= 0 {
1057            return Ok(());
1058        }
1059        for y in -radius..=radius {
1060            for x in -radius..=radius {
1061                if x * x + y * y <= radius * radius {
1062                    self.pixel(center_x + x, center_y + y, color, 255)?;
1063                }
1064            }
1065        }
1066        Ok(())
1067    }
1068
1069    pub fn stroke_circle(
1070        &mut self,
1071        center_x: i32,
1072        center_y: i32,
1073        radius: u32,
1074        color: Rgb565,
1075    ) -> Result<(), D::Error> {
1076        let radius = radius as i32;
1077        if radius <= 0 {
1078            return Ok(());
1079        }
1080        let mut x = radius;
1081        let mut y = 0;
1082        let mut err = 1 - x;
1083        while x >= y {
1084            self.pixel(center_x + x, center_y + y, color, 255)?;
1085            self.pixel(center_x + y, center_y + x, color, 255)?;
1086            self.pixel(center_x - y, center_y + x, color, 255)?;
1087            self.pixel(center_x - x, center_y + y, color, 255)?;
1088            self.pixel(center_x - x, center_y - y, color, 255)?;
1089            self.pixel(center_x - y, center_y - x, color, 255)?;
1090            self.pixel(center_x + y, center_y - x, color, 255)?;
1091            self.pixel(center_x + x, center_y - y, color, 255)?;
1092            y += 1;
1093            if err < 0 {
1094                err += 2 * y + 1;
1095            } else {
1096                x -= 1;
1097                err += 2 * (y - x) + 1;
1098            }
1099        }
1100        Ok(())
1101    }
1102
1103    pub fn stroke_arc(
1104        &mut self,
1105        center_x: i32,
1106        center_y: i32,
1107        radius: u32,
1108        start_deg: i32,
1109        end_deg: i32,
1110        color: Rgb565,
1111    ) -> Result<(), D::Error> {
1112        self.stroke_arc_styled(
1113            center_x,
1114            center_y,
1115            radius,
1116            start_deg,
1117            end_deg,
1118            StrokeStyle::new(color),
1119        )
1120    }
1121
1122    pub fn stroke_arc_styled(
1123        &mut self,
1124        center_x: i32,
1125        center_y: i32,
1126        radius: u32,
1127        start_deg: i32,
1128        end_deg: i32,
1129        style: StrokeStyle,
1130    ) -> Result<(), D::Error> {
1131        let mut start = start_deg;
1132        let mut end = end_deg;
1133        if end < start {
1134            core::mem::swap(&mut start, &mut end);
1135        }
1136        let mut deg = start;
1137        let step = match self.quality {
1138            RenderQuality::Low => 8,
1139            RenderQuality::Medium => 4,
1140            RenderQuality::High => 2,
1141        };
1142        while deg <= end {
1143            let rad = (deg as f32).to_radians();
1144            let x = center_x + (radius as f32 * rad.cos()) as i32;
1145            let y = center_y + (radius as f32 * rad.sin()) as i32;
1146            let half = (style.width as i32 / 2).max(0);
1147            let opacity = self.stroke_opacity(style);
1148            for oy in -half..=half {
1149                for ox in -half..=half {
1150                    self.pixel(x + ox, y + oy, style.color, opacity)?;
1151                }
1152            }
1153            if style.join == StrokeJoin::Round {
1154                self.fill_circle(x, y, half.max(1) as u32, style.color)?;
1155            }
1156            deg += step;
1157        }
1158        Ok(())
1159    }
1160
1161    /// Fill a sector ("pie slice") using a start angle and sweep angle in degrees.
1162    ///
1163    /// Positive sweep draws counterclockwise, negative sweep clockwise.
1164    pub fn fill_sector_sweep(
1165        &mut self,
1166        center_x: i32,
1167        center_y: i32,
1168        radius: u32,
1169        start_deg: f32,
1170        sweep_deg: f32,
1171        color: Rgb565,
1172    ) -> Result<(), D::Error> {
1173        if radius == 0 {
1174            return Ok(());
1175        }
1176
1177        let draw = self.visible_rect(Rect::new(
1178            center_x - radius as i32,
1179            center_y - radius as i32,
1180            radius.saturating_mul(2).saturating_add(1),
1181            radius.saturating_mul(2).saturating_add(1),
1182        ));
1183        if draw.is_empty() {
1184            return Ok(());
1185        }
1186
1187        let max_sweep = sweep_deg.abs().min(360.0);
1188        if max_sweep <= 0.0 {
1189            return Ok(());
1190        }
1191
1192        let rr = (radius as i32) * (radius as i32);
1193        let start = normalize_angle_deg(start_deg);
1194        let ccw = sweep_deg >= 0.0;
1195
1196        for y in draw.y..draw.bottom() {
1197            for x in draw.x..draw.right() {
1198                let dx = x - center_x;
1199                let dy = y - center_y;
1200                let d2 = dx * dx + dy * dy;
1201                if d2 > rr {
1202                    continue;
1203                }
1204
1205                let mut angle = (dy as f32).atan2(dx as f32).to_degrees();
1206                if angle < 0.0 {
1207                    angle += 360.0;
1208                }
1209                let in_sweep = if ccw {
1210                    ccw_distance_deg(start, angle) <= max_sweep
1211                } else {
1212                    ccw_distance_deg(angle, start) <= max_sweep
1213                };
1214                if in_sweep {
1215                    self.pixel(x, y, color, 255)?;
1216                }
1217            }
1218        }
1219        Ok(())
1220    }
1221
1222    pub fn fill_polygon(&mut self, points: &[Point], color: Rgb565) -> Result<(), D::Error> {
1223        if points.len() < 3 {
1224            return Ok(());
1225        }
1226        let min_y = points.iter().map(|p| p.y).min().unwrap_or(0);
1227        let max_y = points.iter().map(|p| p.y).max().unwrap_or(-1);
1228        for y in min_y..=max_y {
1229            let mut intersections = [i32::MIN; 16];
1230            let mut count = 0usize;
1231            for i in 0..points.len() {
1232                let p1 = points[i];
1233                let p2 = points[(i + 1) % points.len()];
1234                let (y1, y2) = if p1.y <= p2.y {
1235                    (p1.y, p2.y)
1236                } else {
1237                    (p2.y, p1.y)
1238                };
1239                if y < y1 || y >= y2 || y1 == y2 {
1240                    continue;
1241                }
1242                if count >= intersections.len() {
1243                    break;
1244                }
1245                let x = p1.x + ((y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y);
1246                intersections[count] = x;
1247                count += 1;
1248            }
1249            intersections[..count].sort_unstable();
1250            let mut i = 0;
1251            while i + 1 < count {
1252                let x0 = intersections[i];
1253                let x1 = intersections[i + 1];
1254                for x in x0..=x1 {
1255                    self.pixel(x, y, color, 255)?;
1256                }
1257                i += 2;
1258            }
1259        }
1260        Ok(())
1261    }
1262
1263    pub fn draw_image(
1264        &mut self,
1265        rect: Rect,
1266        image: ImageRef<'_>,
1267        fit: ImageFit,
1268    ) -> Result<(), D::Error> {
1269        self.draw_image_region(rect, image, fit, Rect::new(0, 0, image.width, image.height))
1270    }
1271
1272    pub fn draw_image_region(
1273        &mut self,
1274        rect: Rect,
1275        image: ImageRef<'_>,
1276        fit: ImageFit,
1277        src_rect: Rect,
1278    ) -> Result<(), D::Error> {
1279        let bounds = image.bounds_at(rect, fit);
1280        if bounds.is_empty() || image.width == 0 || image.height == 0 {
1281            return Ok(());
1282        }
1283        let src_w = image.width as usize;
1284        for y in 0..bounds.h {
1285            let src_y = match fit {
1286                ImageFit::Stretch => {
1287                    src_rect.y.max(0) as usize
1288                        + ((y as u64 * src_rect.h as u64) / bounds.h as u64) as usize
1289                }
1290                ImageFit::Center => src_rect.y.max(0) as usize + y as usize,
1291            };
1292            for x in 0..bounds.w {
1293                let src_x = match fit {
1294                    ImageFit::Stretch => {
1295                        src_rect.x.max(0) as usize
1296                            + ((x as u64 * src_rect.w as u64) / bounds.w as u64) as usize
1297                    }
1298                    ImageFit::Center => src_rect.x.max(0) as usize + x as usize,
1299                };
1300                let idx = src_y.saturating_mul(src_w).saturating_add(src_x);
1301                if let Some(raw) = image.pixels.get(idx) {
1302                    let color = Rgb565::new(
1303                        ((raw >> 11) & 0x1F) as u8,
1304                        ((raw >> 5) & 0x3F) as u8,
1305                        (raw & 0x1F) as u8,
1306                    );
1307                    self.pixel(bounds.x + x as i32, bounds.y + y as i32, color, 255)?;
1308                }
1309            }
1310        }
1311        Ok(())
1312    }
1313
1314    pub fn draw_image_transformed(
1315        &mut self,
1316        rect: Rect,
1317        image: ImageRef<'_>,
1318        scale: f32,
1319        rotation_deg: f32,
1320    ) -> Result<(), D::Error> {
1321        if rect.is_empty() || image.width == 0 || image.height == 0 || scale <= 0.0 {
1322            return Ok(());
1323        }
1324        let cx = rect.x + rect.w as i32 / 2;
1325        let cy = rect.y + rect.h as i32 / 2;
1326        let rad = rotation_deg.to_radians();
1327        let cos_r = rad.cos();
1328        let sin_r = rad.sin();
1329        let src_w = image.width as usize;
1330        let src_cx = image.width as f32 / 2.0;
1331        let src_cy = image.height as f32 / 2.0;
1332        for y in rect.y..rect.bottom() {
1333            for x in rect.x..rect.right() {
1334                let dx = (x - cx) as f32 / scale;
1335                let dy = (y - cy) as f32 / scale;
1336                let sx = cos_r * dx + sin_r * dy + src_cx;
1337                let sy = -sin_r * dx + cos_r * dy + src_cy;
1338                if sx < 0.0 || sy < 0.0 || sx >= image.width as f32 || sy >= image.height as f32 {
1339                    continue;
1340                }
1341                let idx = (sy as usize)
1342                    .saturating_mul(src_w)
1343                    .saturating_add(sx as usize);
1344                if let Some(raw) = image.pixels.get(idx) {
1345                    let color = Rgb565::new(
1346                        ((raw >> 11) & 0x1F) as u8,
1347                        ((raw >> 5) & 0x3F) as u8,
1348                        (raw & 0x1F) as u8,
1349                    );
1350                    self.pixel(x, y, color, 255)?;
1351                }
1352            }
1353        }
1354        Ok(())
1355    }
1356
1357    pub fn fill_rect_masked(
1358        &mut self,
1359        rect: Rect,
1360        color: Rgb565,
1361        mask: fn(i32, i32) -> bool,
1362    ) -> Result<(), D::Error> {
1363        let draw = self.visible_rect(rect);
1364        if draw.is_empty() {
1365            return Ok(());
1366        }
1367        for y in draw.y..draw.bottom() {
1368            for x in draw.x..draw.right() {
1369                if mask(x, y) {
1370                    self.pixel(x, y, color, 255)?;
1371                }
1372            }
1373        }
1374        Ok(())
1375    }
1376
1377    pub fn draw_text_model_in(&mut self, rect: Rect, text: text::Text<'_>) -> Result<(), D::Error> {
1378        if rect.is_empty() || text.lines.is_empty() {
1379            return Ok(());
1380        }
1381
1382        let metrics = text.metrics(rect.w);
1383        let max_line_height = text
1384            .lines
1385            .iter()
1386            .map(|line| line.max_line_height())
1387            .max()
1388            .unwrap_or(CHAR_HEIGHT);
1389        let line_step = max_line_height + text.line_spacing as u32;
1390        let mut y = match text.vertical_align {
1391            VerticalAlign::Top => rect.y,
1392            VerticalAlign::Middle => rect.y + rect.h.saturating_sub(metrics.height) as i32 / 2,
1393            VerticalAlign::Bottom => rect.y + rect.h.saturating_sub(metrics.height) as i32,
1394        };
1395        for line in text.lines {
1396            let align = if line.align == TextAlign::Left {
1397                text.align
1398            } else {
1399                line.align
1400            };
1401            let line = text::Line { align, ..*line };
1402
1403            let mut start = 0;
1404            let char_count = line.char_count();
1405            if char_count == 0 {
1406                y += line_step as i32;
1407                continue;
1408            }
1409            while start < char_count {
1410                if y >= rect.bottom() {
1411                    return Ok(());
1412                }
1413                let (len, consumed_newline) = line.segment_len_at(start, rect.w, text.wrap);
1414                self.draw_line_segment_in(
1415                    Rect::new(rect.x, y, rect.w, max_line_height),
1416                    line,
1417                    start,
1418                    len,
1419                )?;
1420                y += line_step as i32;
1421                start += len + usize::from(consumed_newline);
1422                if len == 0 && !consumed_newline {
1423                    break;
1424                }
1425            }
1426        }
1427
1428        Ok(())
1429    }
1430
1431    pub fn text_metrics(text: &str) -> TextMetrics {
1432        Self::text_metrics_with_font(text, FontId::Tiny3x5)
1433    }
1434
1435    pub fn text_metrics_with_font(text: &str, font: FontId) -> TextMetrics {
1436        TextMetrics {
1437            width: text.chars().count() as u32 * font.advance(),
1438            height: font.line_height(),
1439        }
1440    }
1441
1442    pub fn text_metrics_wrapped(text: &str, max_width: u32, wrap: TextWrap) -> TextMetrics {
1443        Self::text_metrics_wrapped_with_font(text, max_width, wrap, FontId::Tiny3x5)
1444    }
1445
1446    pub fn text_metrics_wrapped_with_font(
1447        text: &str,
1448        max_width: u32,
1449        wrap: TextWrap,
1450        font: FontId,
1451    ) -> TextMetrics {
1452        let max_chars = (max_width / font.advance()).max(1) as usize;
1453        let lines = count_lines(text, max_chars, wrap).max(1);
1454        let widest = widest_line(text, max_chars, wrap) as u32 * font.advance();
1455        TextMetrics {
1456            width: widest.min(max_width),
1457            height: lines as u32 * font.line_height() + lines.saturating_sub(1) as u32,
1458        }
1459    }
1460
1461    #[allow(clippy::too_many_arguments)]
1462    fn draw_chars_with_font(
1463        &mut self,
1464        x: i32,
1465        y: i32,
1466        text: &str,
1467        start: usize,
1468        len: usize,
1469        color: Rgb565,
1470        opacity: u8,
1471        font: FontId,
1472        kerning: bool,
1473    ) -> Result<(), D::Error> {
1474        let advance = font.advance() as i32;
1475        let mut cursor_x = x;
1476        let mut prev: Option<char> = None;
1477        for ch in text.chars().skip(start).take(len) {
1478            self.draw_char_with_font(cursor_x, y, ch, color, opacity, font)?;
1479            cursor_x += advance + kerning_adjust(prev, ch, kerning);
1480            prev = Some(ch);
1481        }
1482        Ok(())
1483    }
1484
1485    fn substring_width(
1486        &self,
1487        text: &str,
1488        start: usize,
1489        len: usize,
1490        font: FontId,
1491        kerning: bool,
1492    ) -> u32 {
1493        let mut width = 0u32;
1494        let mut prev = None;
1495        for ch in text.chars().skip(start).take(len) {
1496            width = width.saturating_add(font.advance());
1497            let adjust = kerning_adjust(prev, ch, kerning);
1498            if adjust < 0 {
1499                width = width.saturating_sub((-adjust) as u32);
1500            } else {
1501                width = width.saturating_add(adjust as u32);
1502            }
1503            prev = Some(ch);
1504        }
1505        width
1506    }
1507
1508    fn draw_line_segment_in(
1509        &mut self,
1510        rect: Rect,
1511        line: text::Line<'_>,
1512        start: usize,
1513        len: usize,
1514    ) -> Result<(), D::Error> {
1515        if rect.is_empty() || len == 0 {
1516            return Ok(());
1517        }
1518
1519        let line_w = self.line_segment_width(line, start, len);
1520        let x = match line.align {
1521            TextAlign::Left => rect.x,
1522            TextAlign::Center => rect.x + rect.w.saturating_sub(line_w) as i32 / 2,
1523            TextAlign::Right => rect.x + rect.w.saturating_sub(line_w) as i32,
1524        };
1525
1526        let old_clip = self.clip;
1527        self.clip = self.clip.intersection(rect);
1528        let result = self.draw_span_chars(x, rect.y, line, start, len);
1529        self.clip = old_clip;
1530        result
1531    }
1532
1533    fn draw_span_chars(
1534        &mut self,
1535        x: i32,
1536        y: i32,
1537        line: text::Line<'_>,
1538        start: usize,
1539        len: usize,
1540    ) -> Result<(), D::Error> {
1541        let mut cursor_x = x;
1542        for (idx, (ch, style)) in line
1543            .spans
1544            .iter()
1545            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style)))
1546            .enumerate()
1547        {
1548            if idx < start {
1549                continue;
1550            }
1551            if idx >= start + len {
1552                break;
1553            }
1554            if ch != '\n' {
1555                self.draw_char_with_font(cursor_x, y, ch, style.color, 255, style.font)?;
1556                cursor_x += style.font.advance() as i32;
1557            }
1558        }
1559        Ok(())
1560    }
1561
1562    fn line_segment_width(&self, line: text::Line<'_>, start: usize, len: usize) -> u32 {
1563        line.spans
1564            .iter()
1565            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style.font)))
1566            .enumerate()
1567            .filter_map(|(idx, (ch, font))| {
1568                if idx < start || idx >= start + len || ch == '\n' {
1569                    None
1570                } else {
1571                    Some(font.advance())
1572                }
1573            })
1574            .sum()
1575    }
1576
1577    fn draw_char_with_font(
1578        &mut self,
1579        x: i32,
1580        y: i32,
1581        ch: char,
1582        color: Rgb565,
1583        opacity: u8,
1584        font: FontId,
1585    ) -> Result<(), D::Error> {
1586        let glyph = glyph_rows(font, ch);
1587        match font {
1588            FontId::Tiny3x5 => {
1589                for (row, bits) in glyph.iter().enumerate() {
1590                    for col in 0..3 {
1591                        if bits & (1 << (2 - col)) != 0 {
1592                            self.pixel(x + col, y + row as i32, color, opacity)?;
1593                        }
1594                    }
1595                }
1596            }
1597            FontId::Medium4x7 => {
1598                for (row, bits) in glyph.iter().enumerate() {
1599                    for col in 0..3 {
1600                        if bits & (1 << (2 - col)) != 0 {
1601                            self.pixel(x + col, y + row as i32, color, opacity)?;
1602                        }
1603                    }
1604                }
1605            }
1606            FontId::Scaled6x10 => {
1607                for (row, bits) in glyph.iter().enumerate() {
1608                    for col in 0..3 {
1609                        if bits & (1 << (2 - col)) != 0 {
1610                            let px = x + (col * 2);
1611                            let py = y + (row as i32 * 2);
1612                            self.pixel(px, py, color, opacity)?;
1613                            self.pixel(px + 1, py, color, opacity)?;
1614                            self.pixel(px, py + 1, color, opacity)?;
1615                            self.pixel(px + 1, py + 1, color, opacity)?;
1616                        }
1617                    }
1618                }
1619            }
1620        }
1621        Ok(())
1622    }
1623
1624    fn pixel(&mut self, x: i32, y: i32, color: Rgb565, opacity: u8) -> Result<(), D::Error> {
1625        let (x, y) = self.current_transform().apply(x, y);
1626        if !self.clip.contains(x, y) {
1627            return Ok(());
1628        }
1629        if let Some(dirty) = self.dirty {
1630            if !dirty.contains(x, y) {
1631                return Ok(());
1632            }
1633        }
1634        let layer = self.current_layer();
1635        let combined_opacity = ((opacity as u16 * layer.opacity as u16) / 255) as u8;
1636        // The compositor policy (`Dither` vs `Blend`) decides how the pixel
1637        // lands: ordered dither for write-only targets, true alpha blend for
1638        // readback-capable ones. Zero-cost — resolved by `C` at monomorphization.
1639        C::plot(
1640            self.target,
1641            x,
1642            y,
1643            color,
1644            combined_opacity,
1645            layer.blend,
1646            layer.backdrop,
1647        )
1648    }
1649
1650    fn visible_rect(&self, rect: Rect) -> Rect {
1651        let mut draw = rect.intersection(self.clip);
1652        if let Some(dirty) = self.dirty {
1653            draw = draw.intersection(dirty);
1654        }
1655        draw
1656    }
1657
1658    fn current_transform(&self) -> Transform2D {
1659        self.transform_stack[self.transform_len - 1]
1660    }
1661
1662    fn current_layer(&self) -> LayerState {
1663        self.layer_stack[self.layer_len - 1]
1664    }
1665
1666    fn stroke_opacity(&self, style: StrokeStyle) -> u8 {
1667        if !style.antialias || matches!(style.antialias_mode, AntiAliasMode::None) {
1668            return 255;
1669        }
1670        match style.antialias_mode {
1671            AntiAliasMode::None => 255,
1672            AntiAliasMode::Coverage => match self.quality {
1673                RenderQuality::Low => 96,
1674                RenderQuality::Medium => 160,
1675                RenderQuality::High => 220,
1676            },
1677            AntiAliasMode::Subpixel => {
1678                if self.backend_caps.supports_subpixel {
1679                    match self.quality {
1680                        RenderQuality::Low => 128,
1681                        RenderQuality::Medium => 192,
1682                        RenderQuality::High => 240,
1683                    }
1684                } else {
1685                    match self.quality {
1686                        RenderQuality::Low => 96,
1687                        RenderQuality::Medium => 160,
1688                        RenderQuality::High => 220,
1689                    }
1690                }
1691            }
1692        }
1693    }
1694}
1695
1696impl<'a, D, C> RenderCtx<'a, D, C>
1697where
1698    D: DrawTarget<Color = Rgb565> + PixelRead,
1699    C: Compositor<D>,
1700{
1701    /// Alpha-composite `color` over whatever is already at `(x, y)` in the
1702    /// destination, using true per-pixel blending (`lerp_rgb565`) rather
1703    /// than the dithered approximation `pixel()` uses.
1704    fn pixel_blended(&mut self, x: i32, y: i32, color: Rgb565, alpha: u8) -> Result<(), D::Error> {
1705        let (x, y) = self.current_transform().apply(x, y);
1706        if !self.clip.contains(x, y) {
1707            return Ok(());
1708        }
1709        if let Some(dirty) = self.dirty {
1710            if !dirty.contains(x, y) {
1711                return Ok(());
1712            }
1713        }
1714        let layer = self.current_layer();
1715        let combined_alpha = ((alpha as u16 * layer.opacity as u16) / 255) as u8;
1716        if combined_alpha == 0 {
1717            return Ok(());
1718        }
1719        let backdrop = self.target.get_pixel(Point::new(x, y));
1720        let blended = lerp_rgb565(backdrop, color, combined_alpha);
1721        let blended = apply_blend_mode(blended, layer.blend, layer.backdrop);
1722        self.target.draw_iter([Pixel(Point::new(x, y), blended)])
1723    }
1724
1725    /// Like [`RenderCtx::fill_rect_alpha`], but alpha-composites against the
1726    /// destination's real current pixels instead of dithering.
1727    pub fn fill_rect_true_alpha(
1728        &mut self,
1729        rect: Rect,
1730        color: Rgb565,
1731        alpha: u8,
1732    ) -> Result<(), D::Error> {
1733        self.fill_rounded_rect_true_alpha(rect, 0, color, alpha)
1734    }
1735
1736    /// Like [`RenderCtx::fill_rounded_rect_alpha`], but alpha-composites
1737    /// against the destination's real current pixels instead of dithering.
1738    pub fn fill_rounded_rect_true_alpha(
1739        &mut self,
1740        rect: Rect,
1741        radius: u8,
1742        color: Rgb565,
1743        alpha: u8,
1744    ) -> Result<(), D::Error> {
1745        let draw = self.visible_rect(rect);
1746        if draw.is_empty() || alpha == 0 {
1747            return Ok(());
1748        }
1749        let radius = radius.min((rect.w.min(rect.h) / 2) as u8);
1750
1751        for y in draw.y..draw.bottom() {
1752            for x in draw.x..draw.right() {
1753                if !in_rounded_rect(x, y, rect, radius) {
1754                    continue;
1755                }
1756                self.pixel_blended(x, y, color, alpha)?;
1757            }
1758        }
1759        Ok(())
1760    }
1761}
1762
1763fn should_draw_at_opacity(x: i32, y: i32, opacity: u8) -> bool {
1764    if opacity == 255 {
1765        return true;
1766    }
1767    if opacity == 0 {
1768        return false;
1769    }
1770    let bayer4 = [
1771        [0u8, 8, 2, 10],
1772        [12, 4, 14, 6],
1773        [3, 11, 1, 9],
1774        [15, 7, 13, 5],
1775    ];
1776    let threshold = ((opacity as u16 * 16) / 255) as u8;
1777    let sample = bayer4[(y as usize) & 3][(x as usize) & 3];
1778    sample < threshold.max(1)
1779}
1780
1781fn lerp_rgb565(a: Rgb565, b: Rgb565, t: u8) -> Rgb565 {
1782    let t = t as u16;
1783    let inv = 255u16.saturating_sub(t);
1784    let r = ((a.r() as u16 * inv) + (b.r() as u16 * t)) / 255;
1785    let g = ((a.g() as u16 * inv) + (b.g() as u16 * t)) / 255;
1786    let bb = ((a.b() as u16 * inv) + (b.b() as u16 * t)) / 255;
1787    Rgb565::new(r as u8, g as u8, bb as u8)
1788}
1789
1790#[inline]
1791fn normalize_angle_deg(mut deg: f32) -> f32 {
1792    while deg < 0.0 {
1793        deg += 360.0;
1794    }
1795    while deg >= 360.0 {
1796        deg -= 360.0;
1797    }
1798    deg
1799}
1800
1801#[inline]
1802fn ccw_distance_deg(from: f32, to: f32) -> f32 {
1803    let mut d = normalize_angle_deg(to) - normalize_angle_deg(from);
1804    if d < 0.0 {
1805        d += 360.0;
1806    }
1807    d
1808}
1809
1810fn apply_blend_mode(src: Rgb565, mode: BlendMode, backdrop: Rgb565) -> Rgb565 {
1811    match mode {
1812        BlendMode::Normal => src,
1813        BlendMode::Add => Rgb565::new(
1814            src.r().saturating_add(backdrop.r()),
1815            src.g().saturating_add(backdrop.g()),
1816            src.b().saturating_add(backdrop.b()),
1817        ),
1818        BlendMode::Multiply => Rgb565::new(
1819            ((src.r() as u16 * backdrop.r() as u16) / 31) as u8,
1820            ((src.g() as u16 * backdrop.g() as u16) / 63) as u8,
1821            ((src.b() as u16 * backdrop.b() as u16) / 31) as u8,
1822        ),
1823        BlendMode::Screen => Rgb565::new(
1824            (31 - ((31 - src.r() as u16) * (31 - backdrop.r() as u16) / 31)) as u8,
1825            (63 - ((63 - src.g() as u16) * (63 - backdrop.g() as u16) / 63)) as u8,
1826            (31 - ((31 - src.b() as u16) * (31 - backdrop.b() as u16) / 31)) as u8,
1827        ),
1828    }
1829}
1830
1831fn in_rounded_rect(x: i32, y: i32, rect: Rect, radius: u8) -> bool {
1832    if rect.is_empty() {
1833        return false;
1834    }
1835    let radius = radius as i32;
1836    if radius <= 0 {
1837        return rect.contains(x, y);
1838    }
1839
1840    let left = rect.x;
1841    let top = rect.y;
1842    let right = rect.right() - 1;
1843    let bottom = rect.bottom() - 1;
1844    let inner_left = left + radius;
1845    let inner_right = right - radius;
1846    let inner_top = top + radius;
1847    let inner_bottom = bottom - radius;
1848
1849    if (x >= inner_left && x <= inner_right) || (y >= inner_top && y <= inner_bottom) {
1850        return rect.contains(x, y);
1851    }
1852
1853    let (cx, cy) = if x < inner_left && y < inner_top {
1854        (inner_left, inner_top)
1855    } else if x > inner_right && y < inner_top {
1856        (inner_right, inner_top)
1857    } else if x < inner_left && y > inner_bottom {
1858        (inner_left, inner_bottom)
1859    } else if x > inner_right && y > inner_bottom {
1860        (inner_right, inner_bottom)
1861    } else {
1862        return rect.contains(x, y);
1863    };
1864
1865    let dx = x - cx;
1866    let dy = y - cy;
1867    dx * dx + dy * dy <= radius * radius
1868}
1869
1870fn line_len_at(text: &str, start: usize, max_chars: usize, wrap: TextWrap) -> (usize, bool) {
1871    let mut len = 0;
1872    let limit = match wrap {
1873        TextWrap::None => usize::MAX,
1874        TextWrap::Character => max_chars.max(1),
1875        TextWrap::Word => max_chars.max(1),
1876    };
1877    let mut last_ws_break = None;
1878
1879    for ch in text.chars().skip(start) {
1880        if ch == '\n' {
1881            return (len, true);
1882        }
1883        if matches!(wrap, TextWrap::Word) && ch.is_whitespace() {
1884            last_ws_break = Some(len + 1);
1885        }
1886        if len >= limit {
1887            if matches!(wrap, TextWrap::Word) {
1888                if let Some(idx) = last_ws_break {
1889                    return (idx, false);
1890                }
1891            }
1892            return (len, false);
1893        }
1894        len += 1;
1895    }
1896
1897    (len, false)
1898}
1899
1900fn count_lines(text: &str, max_chars: usize, wrap: TextWrap) -> usize {
1901    if text.is_empty() {
1902        return 1;
1903    }
1904    let char_count = text.chars().count();
1905    let mut lines = 0;
1906    let mut start = 0;
1907    while start < char_count {
1908        let (len, consumed_newline) = line_len_at(text, start, max_chars, wrap);
1909        lines += 1;
1910        start += len + usize::from(consumed_newline);
1911        if len == 0 && !consumed_newline {
1912            break;
1913        }
1914    }
1915    lines
1916}
1917
1918fn widest_line(text: &str, max_chars: usize, wrap: TextWrap) -> usize {
1919    let char_count = text.chars().count();
1920    let mut widest = 0;
1921    let mut start = 0;
1922    while start < char_count {
1923        let (len, consumed_newline) = line_len_at(text, start, max_chars, wrap);
1924        widest = widest.max(len);
1925        start += len + usize::from(consumed_newline);
1926        if len == 0 && !consumed_newline {
1927            break;
1928        }
1929    }
1930    widest
1931}
1932
1933fn kerning_adjust(prev: Option<char>, next: char, enabled: bool) -> i32 {
1934    if !enabled {
1935        return 0;
1936    }
1937    match (prev, next) {
1938        (Some('A'), 'V') | (Some('A'), 'W') | (Some('T'), 'o') | (Some('L'), 'T') => -1,
1939        _ => 0,
1940    }
1941}