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 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}