Skip to main content

zest_simulator/
lib.rs

1//! Desktop simulator [`Platform`] for `zest`.
2//!
3//! Shapes are rendered with anti-aliasing via `tiny-skia` into an
4//! RGBA pixmap, then converted to RGB565 for the SDL2-backed
5//! `embedded-graphics-simulator` window. Text stays pixel-perfect
6//! (bitmap `MonoFont`) to match the on-device look.
7
8use embassy_time::{Duration, Timer};
9use embedded_graphics::{
10    Pixel,
11    mono_font::{MonoFont, MonoTextStyle},
12    pixelcolor::Rgb565,
13    prelude::*,
14    primitives::Rectangle,
15    text::{Alignment, Text},
16};
17use embedded_graphics_simulator::{
18    OutputSettings, OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window,
19    sdl2::{Keycode, MouseButton, MouseWheelDirection},
20};
21use std::{convert::Infallible, vec::Vec};
22use tiny_skia::{
23    Color as SkColor, FillRule, Mask, Paint, PathBuilder, Pixmap, Rect as SkRect, Stroke, Transform,
24};
25use zest_core::{
26    ButtonState, DirtyRegion, EncoderEvent, InputEvent, Key, KeyEvent, Platform,
27    PlatformCapabilities, RenderError, Renderer, TouchEvent, TouchPhase,
28};
29
30/// Default display width — matches the CYD R3 panel.
31pub const DEFAULT_WIDTH: u32 = 320;
32/// Default display height — matches the CYD R3 panel.
33pub const DEFAULT_HEIGHT: u32 = 240;
34/// Default window scaling factor.
35pub const DEFAULT_SCALE: u32 = 2;
36/// Default per-pixel gap. Anti-aliased rendering supplies its own
37/// smoothness, so the default is `0` (sharp upscale).
38pub const DEFAULT_PIXEL_SPACING: u32 = 0;
39/// Default event-poll interval in milliseconds (≈ 60 fps).
40pub const DEFAULT_POLL_MS: u64 = 16;
41
42/// Configurable builder for [`SimulatorPlatform`].
43pub struct SimulatorPlatformBuilder {
44    title: String,
45    size: Size,
46    scale: u32,
47    pixel_spacing: u32,
48    poll_ms: u64,
49    show_dirty: bool,
50}
51
52impl SimulatorPlatformBuilder {
53    /// New builder with the given window title; defaults otherwise.
54    #[must_use]
55    pub fn new(title: impl Into<String>) -> Self {
56        Self {
57            title: title.into(),
58            size: Size::new(DEFAULT_WIDTH, DEFAULT_HEIGHT),
59            scale: DEFAULT_SCALE,
60            pixel_spacing: DEFAULT_PIXEL_SPACING,
61            poll_ms: DEFAULT_POLL_MS,
62            show_dirty: false,
63        }
64    }
65
66    /// Override display size (default: 320×240).
67    #[must_use]
68    pub fn size(mut self, size: Size) -> Self {
69        self.size = size;
70        self
71    }
72
73    /// Override window scale (default: 2).
74    #[must_use]
75    pub fn scale(mut self, scale: u32) -> Self {
76        self.scale = scale;
77        self
78    }
79
80    /// Override per-pixel gap (default: 0).
81    ///
82    /// `1` mimics a real LCD's pixel grid by drawing each emulated
83    /// pixel as a `scale × scale` block followed by a 1-px gap.
84    /// Combined with anti-aliased shapes this can look over-busy;
85    /// keep it `0` for a flat upscale.
86    #[must_use]
87    pub fn pixel_spacing(mut self, pixel_spacing: u32) -> Self {
88        self.pixel_spacing = pixel_spacing;
89        self
90    }
91
92    /// Override SDL event-poll interval in ms (default: 16 = 60 fps).
93    #[must_use]
94    pub fn poll_ms(mut self, poll_ms: u64) -> Self {
95        self.poll_ms = poll_ms;
96        self
97    }
98
99    /// Draw an outline over dirty rectangles after each partial redraw.
100    #[must_use]
101    pub fn show_dirty(mut self, show_dirty: bool) -> Self {
102        self.show_dirty = show_dirty;
103        self
104    }
105
106    /// Build.
107    #[must_use]
108    pub fn build(self) -> SimulatorPlatform {
109        let settings: OutputSettings = OutputSettingsBuilder::new()
110            .scale(self.scale)
111            .pixel_spacing(self.pixel_spacing)
112            .build();
113        let pixmap = Pixmap::new(self.size.width, self.size.height).expect("non-zero pixmap size");
114        SimulatorPlatform {
115            display: SimulatorDisplay::new(self.size),
116            window: Window::new(&self.title, &settings),
117            pixmap,
118            size: self.size,
119            mouse_down: false,
120            poll_ms: self.poll_ms,
121            show_dirty: self.show_dirty,
122            pending: None,
123        }
124    }
125}
126
127/// `Platform` implementation backed by SDL2.
128pub struct SimulatorPlatform {
129    display: SimulatorDisplay<Rgb565>,
130    window: Window,
131    pixmap: Pixmap,
132    size: Size,
133    mouse_down: bool,
134    poll_ms: u64,
135    show_dirty: bool,
136    /// One-slot buffer holding a discrete Down/Up event that arrived in the
137    /// same poll batch as coalesced moves, so it is delivered on the next
138    /// `next_event` call rather than dropped.
139    pending: Option<InputEvent>,
140}
141
142impl SimulatorPlatform {
143    /// Convenience: default 320×240 RGB565 at 2× scale, 60 fps polling.
144    #[must_use]
145    pub fn new(title: impl Into<String>) -> Self {
146        SimulatorPlatformBuilder::new(title).build()
147    }
148
149    /// Builder for custom configuration.
150    #[must_use]
151    pub fn builder(title: impl Into<String>) -> SimulatorPlatformBuilder {
152        SimulatorPlatformBuilder::new(title)
153    }
154
155    fn key_event(keycode: Keycode, repeat: bool, pressed: bool) -> Option<InputEvent> {
156        let state = if pressed {
157            if repeat {
158                ButtonState::Repeated
159            } else {
160                ButtonState::Pressed
161            }
162        } else {
163            ButtonState::Released
164        };
165
166        let key = match keycode {
167            Keycode::Return | Keycode::KpEnter => Key::Enter,
168            Keycode::Tab => Key::Tab,
169            Keycode::Backspace => Key::Backspace,
170            Keycode::Delete => Key::Delete,
171            Keycode::Left => Key::Left,
172            Keycode::Right => Key::Right,
173            Keycode::Up => Key::Up,
174            Keycode::Down => Key::Down,
175            Keycode::Home => Key::Home,
176            Keycode::End => Key::End,
177            Keycode::PageUp => Key::PageUp,
178            Keycode::PageDown => Key::PageDown,
179            Keycode::Space => Key::Char(' '),
180            Keycode::A => Key::Char('a'),
181            Keycode::B => Key::Char('b'),
182            Keycode::C => Key::Char('c'),
183            Keycode::D => Key::Char('d'),
184            Keycode::E => Key::Char('e'),
185            Keycode::F => Key::Char('f'),
186            Keycode::G => Key::Char('g'),
187            Keycode::H => Key::Char('h'),
188            Keycode::I => Key::Char('i'),
189            Keycode::J => Key::Char('j'),
190            Keycode::K => Key::Char('k'),
191            Keycode::L => Key::Char('l'),
192            Keycode::M => Key::Char('m'),
193            Keycode::N => Key::Char('n'),
194            Keycode::O => Key::Char('o'),
195            Keycode::P => Key::Char('p'),
196            Keycode::Q => Key::Char('q'),
197            Keycode::R => Key::Char('r'),
198            Keycode::S => Key::Char('s'),
199            Keycode::T => Key::Char('t'),
200            Keycode::U => Key::Char('u'),
201            Keycode::V => Key::Char('v'),
202            Keycode::W => Key::Char('w'),
203            Keycode::X => Key::Char('x'),
204            Keycode::Y => Key::Char('y'),
205            Keycode::Z => Key::Char('z'),
206            Keycode::Num0 | Keycode::Kp0 => Key::Char('0'),
207            Keycode::Num1 | Keycode::Kp1 => Key::Char('1'),
208            Keycode::Num2 | Keycode::Kp2 => Key::Char('2'),
209            Keycode::Num3 | Keycode::Kp3 => Key::Char('3'),
210            Keycode::Num4 | Keycode::Kp4 => Key::Char('4'),
211            Keycode::Num5 | Keycode::Kp5 => Key::Char('5'),
212            Keycode::Num6 | Keycode::Kp6 => Key::Char('6'),
213            Keycode::Num7 | Keycode::Kp7 => Key::Char('7'),
214            Keycode::Num8 | Keycode::Kp8 => Key::Char('8'),
215            Keycode::Num9 | Keycode::Kp9 => Key::Char('9'),
216            _ => return None,
217        };
218
219        Some(InputEvent::Key(KeyEvent { key, state }))
220    }
221
222    fn encoder_event(scroll_delta: Point, direction: MouseWheelDirection) -> Option<InputEvent> {
223        let axis = if scroll_delta.y != 0 {
224            scroll_delta.y
225        } else {
226            scroll_delta.x
227        };
228        if axis == 0 {
229            return None;
230        }
231
232        let delta = match direction {
233            MouseWheelDirection::Flipped => -axis,
234            _ => axis,
235        };
236        Some(InputEvent::Encoder(EncoderEvent { delta }))
237    }
238}
239
240impl Platform for SimulatorPlatform {
241    type Color = Rgb565;
242    type Error = Infallible;
243
244    async fn next_event(&mut self) -> Option<InputEvent> {
245        loop {
246            // Deliver a Down/Up buffered from a previous batch first.
247            if let Some(ev) = self.pending.take() {
248                return Some(ev);
249            }
250
251            // Drain the whole SDL batch, coalescing consecutive mouse moves
252            // into just the latest position. Returning on the first move
253            // (the old behaviour) left the rest queued, so during a fast
254            // drag the rendered position trailed the cursor and the backlog
255            // grew — visible as lag. A discrete Down/Up that lands in the
256            // same batch as moves is buffered in `self.pending` and the
257            // pending move is flushed first, so no event is lost.
258            let mut latest_move: Option<Point> = None;
259            for sim_event in self.window.events() {
260                match sim_event {
261                    SimulatorEvent::Quit => return None,
262                    SimulatorEvent::MouseButtonDown {
263                        mouse_btn: MouseButton::Left,
264                        point,
265                    } => {
266                        self.mouse_down = true;
267                        let down = InputEvent::Touch(TouchEvent {
268                            phase: TouchPhase::Down,
269                            point,
270                        });
271                        if let Some(p) = latest_move.take() {
272                            self.pending = Some(down);
273                            return Some(InputEvent::Touch(TouchEvent {
274                                phase: TouchPhase::Moved,
275                                point: p,
276                            }));
277                        }
278                        return Some(down);
279                    }
280                    SimulatorEvent::MouseButtonUp {
281                        mouse_btn: MouseButton::Left,
282                        point,
283                    } => {
284                        self.mouse_down = false;
285                        let up = InputEvent::Touch(TouchEvent {
286                            phase: TouchPhase::Up,
287                            point,
288                        });
289                        if let Some(p) = latest_move.take() {
290                            self.pending = Some(up);
291                            return Some(InputEvent::Touch(TouchEvent {
292                                phase: TouchPhase::Moved,
293                                point: p,
294                            }));
295                        }
296                        return Some(up);
297                    }
298                    SimulatorEvent::MouseMove { point } if self.mouse_down => {
299                        latest_move = Some(point);
300                    }
301                    SimulatorEvent::KeyDown {
302                        keycode, repeat, ..
303                    } => {
304                        if let Some(event) = Self::key_event(keycode, repeat, true) {
305                            return Some(event);
306                        }
307                    }
308                    SimulatorEvent::KeyUp {
309                        keycode, repeat, ..
310                    } => {
311                        if let Some(event) = Self::key_event(keycode, repeat, false) {
312                            return Some(event);
313                        }
314                    }
315                    SimulatorEvent::MouseWheel {
316                        scroll_delta,
317                        direction,
318                    } => {
319                        if let Some(event) = Self::encoder_event(scroll_delta, direction) {
320                            return Some(event);
321                        }
322                    }
323                    _ => {}
324                }
325            }
326            if let Some(point) = latest_move {
327                return Some(InputEvent::Touch(TouchEvent {
328                    phase: TouchPhase::Moved,
329                    point,
330                }));
331            }
332            Timer::after(Duration::from_millis(self.poll_ms)).await;
333        }
334    }
335
336    async fn render_with<F>(&mut self, draw: F) -> Result<(), Self::Error>
337    where
338        F: FnOnce(&mut dyn Renderer<Self::Color>) -> Result<(), RenderError>,
339    {
340        // Reset pixmap to opaque black so AA edges blend against a
341        // defined background; the runtime will paint the theme bg first.
342        self.pixmap.fill(SkColor::BLACK);
343        {
344            let mut renderer = TinySkiaRenderer {
345                pixmap: &mut self.pixmap,
346                clip_mask: None,
347                clip_rect: None,
348                clip_stack: Vec::new(),
349            };
350            let _ = draw(&mut renderer);
351        }
352
353        // RGBA8 → Rgb565 blit into the SDL framebuffer.
354        let area = Rectangle::new(Point::zero(), self.size);
355        let data = self.pixmap.data();
356        let colors = (0..(self.size.width * self.size.height) as usize).map(|i| {
357            let off = i * 4;
358            rgba8_to_rgb565(data[off], data[off + 1], data[off + 2])
359        });
360        let _ = self.display.fill_contiguous(&area, colors);
361
362        self.window.update(&self.display);
363        Ok(())
364    }
365
366    async fn render_with_dirty<F>(
367        &mut self,
368        dirty: &DirtyRegion,
369        draw: F,
370    ) -> Result<(), Self::Error>
371    where
372        F: FnOnce(&mut dyn Renderer<Self::Color>) -> Result<(), RenderError>,
373    {
374        self.pixmap.fill(SkColor::BLACK);
375        {
376            let mut renderer = TinySkiaRenderer {
377                pixmap: &mut self.pixmap,
378                clip_mask: None,
379                clip_rect: None,
380                clip_stack: Vec::new(),
381            };
382            let _ = draw(&mut renderer);
383        }
384
385        match dirty {
386            DirtyRegion::None => {}
387            DirtyRegion::Full => self.blit_full(),
388            DirtyRegion::Rects(rects) => {
389                if self.show_dirty {
390                    self.overlay_dirty(rects);
391                }
392                for rect in rects {
393                    self.blit_rect(*rect);
394                }
395            }
396        }
397
398        self.window.update(&self.display);
399        Ok(())
400    }
401
402    fn viewport(&self) -> Size {
403        self.size
404    }
405
406    fn capabilities(&self) -> PlatformCapabilities {
407        PlatformCapabilities {
408            supports_clip: true,
409            supports_partial_flush: true,
410            supports_semantic_input: false,
411            prefers_full_redraw: false,
412        }
413    }
414}
415
416impl SimulatorPlatform {
417    fn blit_full(&mut self) {
418        let area = Rectangle::new(Point::zero(), self.size);
419        let data = self.pixmap.data();
420        let colors = (0..(self.size.width * self.size.height) as usize).map(|i| {
421            let off = i * 4;
422            rgba8_to_rgb565(data[off], data[off + 1], data[off + 2])
423        });
424        let _ = self.display.fill_contiguous(&area, colors);
425    }
426
427    fn blit_rect(&mut self, rect: Rectangle) {
428        let x0 = rect.top_left.x.max(0) as u32;
429        let y0 = rect.top_left.y.max(0) as u32;
430        let x1 = (rect.top_left.x + rect.size.width as i32).clamp(0, self.size.width as i32) as u32;
431        let y1 =
432            (rect.top_left.y + rect.size.height as i32).clamp(0, self.size.height as i32) as u32;
433        if x1 <= x0 || y1 <= y0 {
434            return;
435        }
436
437        let width = x1 - x0;
438        let height = y1 - y0;
439        let area = Rectangle::new(Point::new(x0 as i32, y0 as i32), Size::new(width, height));
440        let stride = self.size.width as usize * 4;
441        let data = self.pixmap.data();
442        let mut colors = Vec::with_capacity((width * height) as usize);
443        for y in y0..y1 {
444            let row = y as usize * stride;
445            for x in x0..x1 {
446                let off = row + x as usize * 4;
447                colors.push(rgba8_to_rgb565(data[off], data[off + 1], data[off + 2]));
448            }
449        }
450        let _ = self.display.fill_contiguous(&area, colors.into_iter());
451    }
452
453    fn overlay_dirty(&mut self, rects: &[Rectangle]) {
454        let mut renderer = TinySkiaRenderer {
455            pixmap: &mut self.pixmap,
456            clip_mask: None,
457            clip_rect: None,
458            clip_stack: Vec::new(),
459        };
460        for rect in rects {
461            let _ = renderer.stroke_rect(*rect, Rgb565::MAGENTA);
462        }
463    }
464}
465
466struct TinySkiaRenderer<'p> {
467    pixmap: &'p mut Pixmap,
468    clip_mask: Option<Mask>,
469    clip_rect: Option<Rectangle>,
470    clip_stack: Vec<(Option<Mask>, Option<Rectangle>)>,
471}
472
473impl<'p> Renderer<Rgb565> for TinySkiaRenderer<'p> {
474    fn fill_rect(&mut self, rect: Rectangle, color: Rgb565) -> Result<(), RenderError> {
475        let Some(sk_rect) = SkRect::from_xywh(
476            rect.top_left.x as f32,
477            rect.top_left.y as f32,
478            rect.size.width as f32,
479            rect.size.height as f32,
480        ) else {
481            return Ok(());
482        };
483        let mut paint = Paint::default();
484        paint.set_color(rgb565_to_skia(color));
485        // Snap to pixel grid; blocks of solid color don't benefit from AA.
486        paint.anti_alias = false;
487        self.pixmap.fill_rect(
488            sk_rect,
489            &paint,
490            Transform::identity(),
491            self.clip_mask.as_ref(),
492        );
493        Ok(())
494    }
495
496    fn stroke_rect(&mut self, rect: Rectangle, color: Rgb565) -> Result<(), RenderError> {
497        let Some(sk_rect) = SkRect::from_xywh(
498            rect.top_left.x as f32 + 0.5,
499            rect.top_left.y as f32 + 0.5,
500            rect.size.width.saturating_sub(1) as f32,
501            rect.size.height.saturating_sub(1) as f32,
502        ) else {
503            return Ok(());
504        };
505        let path = PathBuilder::from_rect(sk_rect);
506        let mut paint = Paint::default();
507        paint.set_color(rgb565_to_skia(color));
508        paint.anti_alias = false;
509        let mut stroke = Stroke::default();
510        stroke.width = 1.0;
511        self.pixmap.stroke_path(
512            &path,
513            &paint,
514            &stroke,
515            Transform::identity(),
516            self.clip_mask.as_ref(),
517        );
518        Ok(())
519    }
520
521    fn fill_circle(
522        &mut self,
523        center: Point,
524        radius: u32,
525        color: Rgb565,
526    ) -> Result<(), RenderError> {
527        if radius == 0 {
528            return Ok(());
529        }
530        let mut pb = PathBuilder::new();
531        pb.push_circle(center.x as f32 + 0.5, center.y as f32 + 0.5, radius as f32);
532        let Some(path) = pb.finish() else {
533            return Ok(());
534        };
535        let mut paint = Paint::default();
536        paint.set_color(rgb565_to_skia(color));
537        paint.anti_alias = true;
538        self.pixmap.fill_path(
539            &path,
540            &paint,
541            FillRule::Winding,
542            Transform::identity(),
543            self.clip_mask.as_ref(),
544        );
545        Ok(())
546    }
547
548    fn stroke_line(
549        &mut self,
550        start: Point,
551        end: Point,
552        color: Rgb565,
553        width: u32,
554    ) -> Result<(), RenderError> {
555        if width == 0 {
556            return Ok(());
557        }
558        let mut pb = PathBuilder::new();
559        pb.move_to(start.x as f32 + 0.5, start.y as f32 + 0.5);
560        pb.line_to(end.x as f32 + 0.5, end.y as f32 + 0.5);
561        let Some(path) = pb.finish() else {
562            return Ok(());
563        };
564        let mut paint = Paint::default();
565        paint.set_color(rgb565_to_skia(color));
566        paint.anti_alias = true;
567        let mut stroke = Stroke::default();
568        stroke.width = width as f32;
569        self.pixmap.stroke_path(
570            &path,
571            &paint,
572            &stroke,
573            Transform::identity(),
574            self.clip_mask.as_ref(),
575        );
576        Ok(())
577    }
578
579    fn stroke_arc(
580        &mut self,
581        center: Point,
582        radius: u32,
583        start_deg: i32,
584        sweep_deg: i32,
585        width: u32,
586        color: Rgb565,
587    ) -> Result<(), RenderError> {
588        if radius == 0 || width == 0 || sweep_deg == 0 {
589            return Ok(());
590        }
591        let Some(path) = arc_path(center, radius, start_deg, sweep_deg, false) else {
592            return Ok(());
593        };
594        let mut paint = Paint::default();
595        paint.set_color(rgb565_to_skia(color));
596        paint.anti_alias = true;
597        let mut stroke = Stroke::default();
598        stroke.width = width as f32;
599        stroke.line_cap = tiny_skia::LineCap::Round;
600        self.pixmap.stroke_path(
601            &path,
602            &paint,
603            &stroke,
604            Transform::identity(),
605            self.clip_mask.as_ref(),
606        );
607        Ok(())
608    }
609
610    fn fill_arc(
611        &mut self,
612        center: Point,
613        radius: u32,
614        start_deg: i32,
615        sweep_deg: i32,
616        color: Rgb565,
617    ) -> Result<(), RenderError> {
618        if radius == 0 || sweep_deg == 0 {
619            return Ok(());
620        }
621        let Some(path) = arc_path(center, radius, start_deg, sweep_deg, true) else {
622            return Ok(());
623        };
624        let mut paint = Paint::default();
625        paint.set_color(rgb565_to_skia(color));
626        paint.anti_alias = true;
627        self.pixmap.fill_path(
628            &path,
629            &paint,
630            FillRule::Winding,
631            Transform::identity(),
632            self.clip_mask.as_ref(),
633        );
634        Ok(())
635    }
636
637    fn draw_text(
638        &mut self,
639        text: &str,
640        position: Point,
641        font: &MonoFont<'_>,
642        color: Rgb565,
643        alignment: Alignment,
644    ) -> Result<(), RenderError> {
645        let style = MonoTextStyle::new(font, color);
646        let mut adapter = PixmapDrawTarget {
647            pixmap: &mut *self.pixmap,
648            clip: self.clip_rect,
649        };
650        Text::with_alignment(text, position, style, alignment)
651            .draw(&mut adapter)
652            .map(|_| ())
653            .map_err(|_| RenderError)
654    }
655
656    fn draw_image(
657        &mut self,
658        top_left: Point,
659        size: Size,
660        pixels: &[Rgb565],
661    ) -> Result<(), RenderError> {
662        let w = size.width as i32;
663        if w == 0 {
664            return Ok(());
665        }
666        let pw = self.pixmap.width() as i32;
667        let ph = self.pixmap.height() as i32;
668        let stride = self.pixmap.width() as usize * 4;
669        let (cx1, cy1, cx2, cy2) = match self.clip_rect {
670            Some(r) => (
671                r.top_left.x,
672                r.top_left.y,
673                r.top_left.x + r.size.width as i32,
674                r.top_left.y + r.size.height as i32,
675            ),
676            None => (0, 0, pw, ph),
677        };
678        let data = self.pixmap.data_mut();
679        for (i, color) in pixels.iter().enumerate() {
680            let x = top_left.x + (i as i32 % w);
681            let y = top_left.y + (i as i32 / w);
682            if x < cx1 || y < cy1 || x >= cx2 || y >= cy2 {
683                continue;
684            }
685            if x < 0 || y < 0 || x >= pw || y >= ph {
686                continue;
687            }
688            let off = y as usize * stride + x as usize * 4;
689            let (r, g, b) = rgb565_components(*color);
690            data[off] = r;
691            data[off + 1] = g;
692            data[off + 2] = b;
693            data[off + 3] = 255;
694        }
695        Ok(())
696    }
697
698    fn push_clip(&mut self, rect: Rectangle) {
699        let new_rect = match self.clip_rect {
700            Some(existing) => intersect(existing, rect),
701            None => rect,
702        };
703        let new_mask = if new_rect.size.width == 0 || new_rect.size.height == 0 {
704            // Empty clip — build a fully-black mask (nothing draws).
705            Some(Mask::new(self.pixmap.width(), self.pixmap.height()).expect("mask"))
706        } else {
707            build_rect_mask(self.pixmap.width(), self.pixmap.height(), new_rect)
708        };
709        let prev_mask = core::mem::replace(&mut self.clip_mask, new_mask);
710        let prev_rect = self.clip_rect.replace(new_rect);
711        self.clip_stack.push((prev_mask, prev_rect));
712    }
713
714    fn pop_clip(&mut self) {
715        if let Some((mask, rect)) = self.clip_stack.pop() {
716            self.clip_mask = mask;
717            self.clip_rect = rect;
718        }
719    }
720}
721
722fn intersect(a: Rectangle, b: Rectangle) -> Rectangle {
723    let ax2 = a.top_left.x + a.size.width as i32;
724    let ay2 = a.top_left.y + a.size.height as i32;
725    let bx2 = b.top_left.x + b.size.width as i32;
726    let by2 = b.top_left.y + b.size.height as i32;
727    let x1 = a.top_left.x.max(b.top_left.x);
728    let y1 = a.top_left.y.max(b.top_left.y);
729    let x2 = ax2.min(bx2);
730    let y2 = ay2.min(by2);
731    if x2 <= x1 || y2 <= y1 {
732        Rectangle::new(Point::new(x1, y1), Size::zero())
733    } else {
734        Rectangle::new(
735            Point::new(x1, y1),
736            Size::new((x2 - x1) as u32, (y2 - y1) as u32),
737        )
738    }
739}
740
741/// Build a tiny-skia path for a circular arc centered at `center`.
742///
743/// Sweeps `sweep_deg` degrees from `start_deg` (0° points right,
744/// positive sweep is counter-clockwise on screen). When `pie` is true
745/// the path is closed through the center to form a fillable sector;
746/// otherwise it is an open polyline suitable for stroking.
747fn arc_path(
748    center: Point,
749    radius: u32,
750    start_deg: i32,
751    sweep_deg: i32,
752    pie: bool,
753) -> Option<tiny_skia::Path> {
754    let total = sweep_deg.unsigned_abs().min(360);
755    let step: f32 = if sweep_deg >= 0 { 1.0 } else { -1.0 };
756    let r = radius as f32;
757    let cx = center.x as f32 + 0.5;
758    let cy = center.y as f32 + 0.5;
759
760    let point_at = |deg: f32| -> (f32, f32) {
761        let rad = deg * core::f32::consts::PI / 180.0;
762        // Screen y grows downward, so negate the sine.
763        (cx + rad.cos() * r, cy - rad.sin() * r)
764    };
765
766    let mut pb = PathBuilder::new();
767    if pie {
768        pb.move_to(cx, cy);
769        let (x0, y0) = point_at(start_deg as f32);
770        pb.line_to(x0, y0);
771    } else {
772        let (x0, y0) = point_at(start_deg as f32);
773        pb.move_to(x0, y0);
774    }
775    for i in 1..=total {
776        let (x, y) = point_at(start_deg as f32 + i as f32 * step);
777        pb.line_to(x, y);
778    }
779    if pie {
780        pb.close();
781    }
782    pb.finish()
783}
784
785fn build_rect_mask(pixmap_w: u32, pixmap_h: u32, rect: Rectangle) -> Option<Mask> {
786    let mut mask = Mask::new(pixmap_w, pixmap_h)?;
787    let sk_rect = SkRect::from_xywh(
788        rect.top_left.x as f32,
789        rect.top_left.y as f32,
790        rect.size.width as f32,
791        rect.size.height as f32,
792    )?;
793    let path = PathBuilder::from_rect(sk_rect);
794    mask.fill_path(&path, FillRule::Winding, false, Transform::identity());
795    Some(mask)
796}
797
798/// `DrawTarget<Color = Rgb565>` adapter so embedded-graphics text and
799/// other built-in primitives can write directly into the tiny-skia
800/// pixmap. Used only for text in this backend; shapes go through
801/// `TinySkiaRenderer` for AA.
802struct PixmapDrawTarget<'p> {
803    pixmap: &'p mut Pixmap,
804    clip: Option<Rectangle>,
805}
806
807impl<'p> OriginDimensions for PixmapDrawTarget<'p> {
808    fn size(&self) -> Size {
809        Size::new(self.pixmap.width(), self.pixmap.height())
810    }
811}
812
813impl<'p> DrawTarget for PixmapDrawTarget<'p> {
814    type Color = Rgb565;
815    type Error = Infallible;
816
817    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
818    where
819        I: IntoIterator<Item = Pixel<Self::Color>>,
820    {
821        let w = self.pixmap.width() as i32;
822        let h = self.pixmap.height() as i32;
823        let stride = self.pixmap.width() as usize * 4;
824        let (cx1, cy1, cx2, cy2) = match self.clip {
825            Some(r) => (
826                r.top_left.x,
827                r.top_left.y,
828                r.top_left.x + r.size.width as i32,
829                r.top_left.y + r.size.height as i32,
830            ),
831            None => (0, 0, w, h),
832        };
833        let data = self.pixmap.data_mut();
834        for Pixel(p, c) in pixels {
835            if p.x < cx1 || p.y < cy1 || p.x >= cx2 || p.y >= cy2 {
836                continue;
837            }
838            if p.x < 0 || p.y < 0 || p.x >= w || p.y >= h {
839                continue;
840            }
841            let off = p.y as usize * stride + p.x as usize * 4;
842            let (r, g, b) = rgb565_components(c);
843            data[off] = r;
844            data[off + 1] = g;
845            data[off + 2] = b;
846            data[off + 3] = 255;
847        }
848        Ok(())
849    }
850}
851
852fn rgb565_components(c: Rgb565) -> (u8, u8, u8) {
853    // Upscale 5/6/5-bit channels to 8 by replicating high bits into the lows.
854    let r5 = c.r();
855    let g6 = c.g();
856    let b5 = c.b();
857    let r = (r5 << 3) | (r5 >> 2);
858    let g = (g6 << 2) | (g6 >> 4);
859    let b = (b5 << 3) | (b5 >> 2);
860    (r, g, b)
861}
862
863fn rgb565_to_skia(c: Rgb565) -> SkColor {
864    let (r, g, b) = rgb565_components(c);
865    SkColor::from_rgba8(r, g, b, 255)
866}
867
868fn rgba8_to_rgb565(r: u8, g: u8, b: u8) -> Rgb565 {
869    Rgb565::new(r >> 3, g >> 2, b >> 3)
870}