Skip to main content

embedded_gui/
render.rs

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