Skip to main content

zest_core/
renderer.rs

1//! Object-safe rendering abstraction over `embedded-graphics`'s [`DrawTarget`].
2//!
3//! The `Widget` trait must be object-safe so
4//! `Element` can hold `Box<dyn Widget>`.
5//! `DrawTarget` is generic over its error type, which prevents direct use
6//! in trait objects. [`Renderer`] erases the error type into a single
7//! [`RenderError`] and exposes only the operations widgets need.
8//!
9//! [`DrawTargetRenderer`] is the standard adapter from any concrete
10//! `DrawTarget` to `Renderer`.
11
12use core::fmt;
13use embedded_graphics::{
14    mono_font::{MonoFont, MonoTextStyle},
15    pixelcolor::PixelColor,
16    prelude::*,
17    primitives::{Circle, Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, StrokeAlignment},
18    text::{Alignment, Text},
19};
20
21/// Type-erased rendering error.
22///
23/// The underlying [`DrawTarget::Error`] is discarded. Widgets needing rich
24/// error information should implement a custom [`Renderer`] that preserves it.
25#[derive(Copy, Clone, Debug)]
26pub struct RenderError;
27
28impl fmt::Display for RenderError {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.write_str("render error")
31    }
32}
33
34/// Object-safe rendering operations. Widgets call these instead of touching
35/// [`DrawTarget`] directly, so `Widget` can be a
36/// trait object.
37pub trait Renderer<C: PixelColor> {
38    /// Fill `rect` with a solid color.
39    fn fill_rect(&mut self, rect: Rectangle, color: C) -> Result<(), RenderError>;
40    /// Draw a 1-pixel inside-aligned border around `rect`.
41    fn stroke_rect(&mut self, rect: Rectangle, color: C) -> Result<(), RenderError>;
42    /// Fill circle with a solid color.
43    fn fill_circle(&mut self, center: Point, radius: u32, color: C) -> Result<(), RenderError>;
44    /// Create a stroke line.
45    fn stroke_line(
46        &mut self,
47        start: Point,
48        end: Point,
49        color: C,
50        width: u32,
51    ) -> Result<(), RenderError>;
52    /// Render text with a mono font at `position` with the given alignment.
53    fn draw_text(
54        &mut self,
55        text: &str,
56        position: Point,
57        font: &MonoFont<'_>,
58        color: C,
59        alignment: Alignment,
60    ) -> Result<(), RenderError>;
61
62    /// Used to draw an Image widget.
63    fn draw_image(
64        &mut self,
65        _top_left: Point,
66        _size: Size,
67        _pixels: &[C],
68    ) -> Result<(), RenderError> {
69        Ok(())
70    }
71
72    /// Stroke a circular arc centered at `center` with the given
73    /// `radius`, beginning at `start_deg` and sweeping `sweep_deg`
74    /// degrees (positive = counter-clockwise in screen space, i.e.
75    /// toward the top, since 0° points right). `width` is the stroke
76    /// thickness in pixels.
77    ///
78    /// The default implementation approximates the arc with short
79    /// [`stroke_line`](Self::stroke_line) segments using the trig-free
80    /// per-degree [`sin/cos lookup table`](arc_sin_cos), so it works on
81    /// any backend — including the headless default — without pulling in
82    /// floating-point trig. Backends with native arc/path support
83    /// (e.g. the tiny-skia simulator) override this for anti-aliased
84    /// output.
85    fn stroke_arc(
86        &mut self,
87        center: Point,
88        radius: u32,
89        start_deg: i32,
90        sweep_deg: i32,
91        width: u32,
92        color: C,
93    ) -> Result<(), RenderError> {
94        if radius == 0 || width == 0 || sweep_deg == 0 {
95            return Ok(());
96        }
97        // One segment per degree keeps the polyline smooth at the radii
98        // typical for embedded displays while staying cheap. Clamp the
99        // sweep to a full turn so a huge value can't loop forever.
100        let total = sweep_deg.unsigned_abs().min(360);
101        let step: i32 = if sweep_deg >= 0 { 1 } else { -1 };
102        let r = radius as f32;
103
104        let point_at = |deg: i32| -> Point {
105            let (s, c) = arc_sin_cos(deg);
106            // Screen y grows downward, so subtract the sine to make a
107            // positive sweep travel counter-clockwise visually.
108            Point::new(center.x + (c * r) as i32, center.y - (s * r) as i32)
109        };
110
111        let mut prev = point_at(start_deg);
112        for i in 1..=total as i32 {
113            let next = point_at(start_deg + i * step);
114            self.stroke_line(prev, next, color, width)?;
115            prev = next;
116        }
117        Ok(())
118    }
119
120    /// Fill a circular sector (pie slice) centered at `center` with the
121    /// given `radius`, from `start_deg` sweeping `sweep_deg` degrees.
122    ///
123    /// The default implementation rasterizes the sector as a fan of
124    /// triangle-like spokes drawn with [`stroke_line`](Self::stroke_line)
125    /// from the center to each arc point — trig-free via
126    /// [`arc_sin_cos`]. Backends with native fill support override this.
127    fn fill_arc(
128        &mut self,
129        center: Point,
130        radius: u32,
131        start_deg: i32,
132        sweep_deg: i32,
133        color: C,
134    ) -> Result<(), RenderError> {
135        if radius == 0 || sweep_deg == 0 {
136            return Ok(());
137        }
138        let total = sweep_deg.unsigned_abs().min(360);
139        let step: i32 = if sweep_deg >= 0 { 1 } else { -1 };
140        let r = radius as f32;
141        // Spoke width 2 closes the seams between adjacent spokes at the
142        // rim without the gaps a 1px line would leave.
143        for i in 0..=total as i32 {
144            let (s, c) = arc_sin_cos(start_deg + i * step);
145            let end = Point::new(center.x + (c * r) as i32, center.y - (s * r) as i32);
146            self.stroke_line(center, end, color, 2)?;
147        }
148        Ok(())
149    }
150
151    /// Push a clipping rectangle. Subsequent draw calls are restricted
152    /// to the intersection of all currently-pushed clip rects.
153    ///
154    /// Used by `Scrollable` and other viewport-aware widgets. Default
155    /// implementation is a no-op (suitable for backends without
156    /// clip support); the desktop tiny-skia backend implements it
157    /// properly via masks.
158    fn push_clip(&mut self, _rect: Rectangle) {}
159
160    /// Pop the topmost clip rect previously pushed via
161    /// [`push_clip`](Self::push_clip).
162    fn pop_clip(&mut self) {}
163}
164
165/// Adapter implementing [`Renderer`] over any [`DrawTarget`].
166pub struct DrawTargetRenderer<'d, D> {
167    target: &'d mut D,
168}
169
170impl<'d, D> DrawTargetRenderer<'d, D> {
171    /// Wrap a mutable reference to a `DrawTarget`.
172    pub fn new(target: &'d mut D) -> Self {
173        Self { target }
174    }
175}
176
177impl<'d, C, D> Renderer<C> for DrawTargetRenderer<'d, D>
178where
179    C: PixelColor,
180    D: DrawTarget<Color = C>,
181{
182    fn fill_rect(&mut self, rect: Rectangle, color: C) -> Result<(), RenderError> {
183        rect.into_styled(PrimitiveStyle::with_fill(color))
184            .draw(self.target)
185            .map_err(|_| RenderError)
186    }
187
188    fn stroke_rect(&mut self, rect: Rectangle, color: C) -> Result<(), RenderError> {
189        let style = PrimitiveStyleBuilder::new()
190            .stroke_color(color)
191            .stroke_width(1)
192            .stroke_alignment(StrokeAlignment::Inside)
193            .build();
194        rect.into_styled(style)
195            .draw(self.target)
196            .map_err(|_| RenderError)
197    }
198
199    fn fill_circle(&mut self, center: Point, radius: u32, color: C) -> Result<(), RenderError> {
200        // `Circle::with_center` takes a diameter, so double here.
201        Circle::with_center(center, radius * 2)
202            .into_styled(PrimitiveStyle::with_fill(color))
203            .draw(self.target)
204            .map_err(|_| RenderError)
205    }
206
207    fn stroke_line(
208        &mut self,
209        start: Point,
210        end: Point,
211        color: C,
212        width: u32,
213    ) -> Result<(), RenderError> {
214        Line::new(start, end)
215            .into_styled(PrimitiveStyle::with_stroke(color, width))
216            .draw(self.target)
217            .map_err(|_| RenderError)
218    }
219
220    fn draw_text(
221        &mut self,
222        text: &str,
223        position: Point,
224        font: &MonoFont<'_>,
225        color: C,
226        alignment: Alignment,
227    ) -> Result<(), RenderError> {
228        let style = MonoTextStyle::new(font, color);
229        Text::with_alignment(text, position, style, alignment)
230            .draw(self.target)
231            .map(|_| ())
232            .map_err(|_| RenderError)
233    }
234
235    fn draw_image(&mut self, top_left: Point, size: Size, pixels: &[C]) -> Result<(), RenderError> {
236        let area = Rectangle::new(top_left, size);
237        self.target
238            .fill_contiguous(&area, pixels.iter().copied())
239            .map_err(|_| RenderError)
240    }
241}
242
243/// Per-degree sine lookup, index `0..360` ⇒ `sin(deg)`.
244///
245/// Precomputed so [`Renderer::stroke_arc`] and [`Renderer::fill_arc`]
246/// stay trig-free in `no_std` (no `libm` dependency). Cosine is obtained
247/// from the same table via the identity `cos(x) = sin(x + 90°)`.
248static SIN_TABLE: [f32; 360] = [
249    0.0,
250    0.01745241,
251    0.0348995,
252    0.05233596,
253    0.06975647,
254    0.08715574,
255    0.1045285,
256    0.1218693,
257    0.1391731,
258    0.1564345,
259    0.1736482,
260    0.190809,
261    0.2079117,
262    0.2249511,
263    0.2419219,
264    0.258819,
265    0.2756374,
266    0.2923717,
267    0.309017,
268    0.3255682,
269    0.3420201,
270    0.3583679,
271    0.3746066,
272    0.3907311,
273    0.4067366,
274    0.4226183,
275    0.4383711,
276    0.4539905,
277    0.4694716,
278    0.4848096,
279    0.5,
280    0.5150381,
281    0.5299193,
282    0.544639,
283    0.5591929,
284    0.5735764,
285    0.5877853,
286    0.601815,
287    0.6156615,
288    0.6293204,
289    0.6427876,
290    0.656059,
291    0.6691306,
292    0.6819984,
293    0.6946584,
294    0.7071068,
295    0.7193398,
296    0.7313537,
297    0.7431448,
298    0.7547096,
299    0.7660444,
300    0.777146,
301    0.7880108,
302    0.7986355,
303    0.809017,
304    0.819152,
305    0.8290376,
306    0.8386706,
307    0.8480481,
308    0.8571673,
309    0.8660254,
310    0.8746197,
311    0.8829476,
312    0.8910065,
313    0.898794,
314    0.9063078,
315    0.9135455,
316    0.9205049,
317    0.9271839,
318    0.9335804,
319    0.9396926,
320    0.9455186,
321    0.9510565,
322    0.9563048,
323    0.9612617,
324    0.9659258,
325    0.9702957,
326    0.9743701,
327    0.9781476,
328    0.9816272,
329    0.9848078,
330    0.9876883,
331    0.9902681,
332    0.9925462,
333    0.9945219,
334    0.9961947,
335    0.9975641,
336    0.9986295,
337    0.9993908,
338    0.9998477,
339    1.0,
340    0.9998477,
341    0.9993908,
342    0.9986295,
343    0.9975641,
344    0.9961947,
345    0.9945219,
346    0.9925462,
347    0.9902681,
348    0.9876883,
349    0.9848078,
350    0.9816272,
351    0.9781476,
352    0.9743701,
353    0.9702957,
354    0.9659258,
355    0.9612617,
356    0.9563048,
357    0.9510565,
358    0.9455186,
359    0.9396926,
360    0.9335804,
361    0.9271839,
362    0.9205049,
363    0.9135455,
364    0.9063078,
365    0.898794,
366    0.8910065,
367    0.8829476,
368    0.8746197,
369    0.8660254,
370    0.8571673,
371    0.8480481,
372    0.8386706,
373    0.8290376,
374    0.819152,
375    0.809017,
376    0.7986355,
377    0.7880108,
378    0.777146,
379    0.7660444,
380    0.7547096,
381    0.7431448,
382    0.7313537,
383    0.7193398,
384    0.7071068,
385    0.6946584,
386    0.6819984,
387    0.6691306,
388    0.656059,
389    0.6427876,
390    0.6293204,
391    0.6156615,
392    0.601815,
393    0.5877853,
394    0.5735764,
395    0.5591929,
396    0.544639,
397    0.5299193,
398    0.5150381,
399    0.5,
400    0.4848096,
401    0.4694716,
402    0.4539905,
403    0.4383711,
404    0.4226183,
405    0.4067366,
406    0.3907311,
407    0.3746066,
408    0.3583679,
409    0.3420201,
410    0.3255682,
411    0.309017,
412    0.2923717,
413    0.2756374,
414    0.258819,
415    0.2419219,
416    0.2249511,
417    0.2079117,
418    0.190809,
419    0.1736482,
420    0.1564345,
421    0.1391731,
422    0.1218693,
423    0.1045285,
424    0.08715574,
425    0.06975647,
426    0.05233596,
427    0.0348995,
428    0.01745241,
429    0.0,
430    -0.01745241,
431    -0.0348995,
432    -0.05233596,
433    -0.06975647,
434    -0.08715574,
435    -0.1045285,
436    -0.1218693,
437    -0.1391731,
438    -0.1564345,
439    -0.1736482,
440    -0.190809,
441    -0.2079117,
442    -0.2249511,
443    -0.2419219,
444    -0.258819,
445    -0.2756374,
446    -0.2923717,
447    -0.309017,
448    -0.3255682,
449    -0.3420201,
450    -0.3583679,
451    -0.3746066,
452    -0.3907311,
453    -0.4067366,
454    -0.4226183,
455    -0.4383711,
456    -0.4539905,
457    -0.4694716,
458    -0.4848096,
459    -0.5,
460    -0.5150381,
461    -0.5299193,
462    -0.544639,
463    -0.5591929,
464    -0.5735764,
465    -0.5877853,
466    -0.601815,
467    -0.6156615,
468    -0.6293204,
469    -0.6427876,
470    -0.656059,
471    -0.6691306,
472    -0.6819984,
473    -0.6946584,
474    -0.7071068,
475    -0.7193398,
476    -0.7313537,
477    -0.7431448,
478    -0.7547096,
479    -0.7660444,
480    -0.777146,
481    -0.7880108,
482    -0.7986355,
483    -0.809017,
484    -0.819152,
485    -0.8290376,
486    -0.8386706,
487    -0.8480481,
488    -0.8571673,
489    -0.8660254,
490    -0.8746197,
491    -0.8829476,
492    -0.8910065,
493    -0.898794,
494    -0.9063078,
495    -0.9135455,
496    -0.9205049,
497    -0.9271839,
498    -0.9335804,
499    -0.9396926,
500    -0.9455186,
501    -0.9510565,
502    -0.9563048,
503    -0.9612617,
504    -0.9659258,
505    -0.9702957,
506    -0.9743701,
507    -0.9781476,
508    -0.9816272,
509    -0.9848078,
510    -0.9876883,
511    -0.9902681,
512    -0.9925462,
513    -0.9945219,
514    -0.9961947,
515    -0.9975641,
516    -0.9986295,
517    -0.9993908,
518    -0.9998477,
519    -1.0,
520    -0.9998477,
521    -0.9993908,
522    -0.9986295,
523    -0.9975641,
524    -0.9961947,
525    -0.9945219,
526    -0.9925462,
527    -0.9902681,
528    -0.9876883,
529    -0.9848078,
530    -0.9816272,
531    -0.9781476,
532    -0.9743701,
533    -0.9702957,
534    -0.9659258,
535    -0.9612617,
536    -0.9563048,
537    -0.9510565,
538    -0.9455186,
539    -0.9396926,
540    -0.9335804,
541    -0.9271839,
542    -0.9205049,
543    -0.9135455,
544    -0.9063078,
545    -0.898794,
546    -0.8910065,
547    -0.8829476,
548    -0.8746197,
549    -0.8660254,
550    -0.8571673,
551    -0.8480481,
552    -0.8386706,
553    -0.8290376,
554    -0.819152,
555    -0.809017,
556    -0.7986355,
557    -0.7880108,
558    -0.777146,
559    -0.7660444,
560    -0.7547096,
561    -0.7431448,
562    -0.7313537,
563    -0.7193398,
564    -0.7071068,
565    -0.6946584,
566    -0.6819984,
567    -0.6691306,
568    -0.656059,
569    -0.6427876,
570    -0.6293204,
571    -0.6156615,
572    -0.601815,
573    -0.5877853,
574    -0.5735764,
575    -0.5591929,
576    -0.544639,
577    -0.5299193,
578    -0.5150381,
579    -0.5,
580    -0.4848096,
581    -0.4694716,
582    -0.4539905,
583    -0.4383711,
584    -0.4226183,
585    -0.4067366,
586    -0.3907311,
587    -0.3746066,
588    -0.3583679,
589    -0.3420201,
590    -0.3255682,
591    -0.309017,
592    -0.2923717,
593    -0.2756374,
594    -0.258819,
595    -0.2419219,
596    -0.2249511,
597    -0.2079117,
598    -0.190809,
599    -0.1736482,
600    -0.1564345,
601    -0.1391731,
602    -0.1218693,
603    -0.1045285,
604    -0.08715574,
605    -0.06975647,
606    -0.05233596,
607    -0.0348995,
608    -0.01745241,
609];
610
611/// Trig-free `(sin, cos)` of `deg` degrees via a precomputed sine table.
612///
613/// Accepts any integer degree (negative or > 360); it is reduced modulo
614/// 360 first. This is the primitive the default arc implementations use
615/// so `zest-core` needs no `libm`/`std` math.
616#[must_use]
617pub fn arc_sin_cos(deg: i32) -> (f32, f32) {
618    let d = deg.rem_euclid(360) as usize;
619    let c = (deg + 90).rem_euclid(360) as usize;
620    (SIN_TABLE[d], SIN_TABLE[c])
621}