Skip to main content

denise_render/
icon.rs

1//! Shapes a widget can draw when the font has no glyph for them.
2//!
3//! # Why this exists
4//!
5//! A `⌫` on a Backspace key is a picture of an idea, and whether it can be
6//! drawn at all currently depends on which font happens to be installed. The
7//! answers differ more than one would guess: DejaVu has `⌫`, `⇥`, `⏎` and the
8//! cursor triangles; a Mac's Arial has none of them and no triangle either; and
9//! the face that ships with this crate has twenty-three non-ASCII glyphs of
10//! which not one is either. A key that says "back" is legible everywhere and
11//! looks like a compromise; a key that says `⌫` looks right and is a box on the
12//! machine least able to spare one.
13//!
14//! An icon is drawn rather than looked up, so it is the same on every machine.
15//!
16//! # Filled polygons, and nothing else
17//!
18//! There is no path builder here and still is not one. An [`Icon`] is a short
19//! list of closed polygons on a [`GRID`]-square box, scaled into whatever
20//! rectangle it is asked for — which is enough for every shape a key or a
21//! toolbar wants, and stops well short of a vector format this crate would have
22//! to support forever.
23//!
24//! Strokes are absent for a reason rather than an oversight:
25//! [`Canvas::draw_line`](crate::Canvas::draw_line) has no thickness and
26//! deliberately does not, so a one-pixel outline on a 48-pixel key would be
27//! invisible. A shape that reads as an outline is drawn as a filled polygon
28//! with the middle knocked back out in [`Ink::Back`] — which is also how the
29//! `×` inside `⌫` is made.
30//!
31//! # Coordinates
32//!
33//! Integers on a `0..=`[`GRID`] box, y downwards, scaled with integer
34//! arithmetic. No floating point anywhere: this crate has neither `std` nor
35//! `libm`, and the whole rasteriser is built on that.
36
37pub use denise::icon::{GRID, Icon, Ink, MAX_SHAPES, Shape, fx_along};
38
39use denise::{Color, Rect};
40
41use crate::canvas::Canvas;
42
43impl Canvas<'_> {
44    /// Draws an icon into `rect`, scaled from its grid.
45    ///
46    /// `fore` is the content colour and `back` is whatever the icon is sitting
47    /// on — a shape marked [`Ink::Back`] is drawn in it, which is how an outline
48    /// or a cut-out is made.
49    ///
50    /// The icon is scaled to `rect` and **not** kept square: give it a square
51    /// rectangle if you want it square. Anything beyond
52    /// [`MAX_SHAPES`] is ignored rather than drawn wrong.
53    pub fn draw_icon(&mut self, icon: &Icon, rect: Rect, fore: Color, back: Color) {
54        crate::painter::Painter::draw_icon(self, icon, rect, fore, back);
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::testing::TestCanvas;
62
63    const FORE: Color = Color::rgb(255, 255, 255);
64    const BACK: Color = Color::rgb(0, 0, 0);
65
66    /// The whole box, so a fill covers everything and a hole is unmistakable.
67    static SQUARE: Icon = Icon::new(&[Shape::fore(&[(0, 0), (100, 0), (100, 100), (0, 100)])]);
68
69    /// A square with the middle taken back out.
70    static RING: Icon = Icon::new(&[
71        Shape::fore(&[(0, 0), (100, 0), (100, 100), (0, 100)]),
72        Shape::back(&[(30, 30), (70, 30), (70, 70), (30, 70)]),
73    ]);
74
75    #[test]
76    fn an_icon_fills_the_rectangle_it_is_given() {
77        let mut c = TestCanvas::new(40, 40);
78        c.canvas()
79            .draw_icon(&SQUARE, Rect::new(8, 8, 24, 24), FORE, BACK);
80
81        assert_eq!(c.at(20, 20), FORE.to_argb8888(), "the middle is not filled");
82        assert_eq!(c.at(2, 2), 0, "it painted outside its rectangle");
83        assert_eq!(c.at(37, 37), 0, "it painted outside its rectangle");
84    }
85
86    /// The reason [`Ink::Back`] exists: the filler has no stroke and no
87    /// even-odd rule, so an outline is a fill with the middle knocked out.
88    #[test]
89    fn a_back_shape_knocks_a_hole_in_the_one_before_it() {
90        let mut c = TestCanvas::new(40, 40);
91        c.canvas()
92            .draw_icon(&RING, Rect::new(0, 0, 40, 40), FORE, BACK);
93
94        assert_eq!(c.at(4, 20), FORE.to_argb8888(), "the ring is missing");
95        assert_eq!(c.at(20, 20), BACK.to_argb8888(), "the hole was not punched");
96    }
97
98    /// Order is drawing order: a hole before its shape is painted over.
99    #[test]
100    fn a_hole_before_its_shape_is_covered_by_it() {
101        static WRONG: Icon = Icon::new(&[
102            Shape::back(&[(30, 30), (70, 30), (70, 70), (30, 70)]),
103            Shape::fore(&[(0, 0), (100, 0), (100, 100), (0, 100)]),
104        ]);
105        let mut c = TestCanvas::new(40, 40);
106        c.canvas()
107            .draw_icon(&WRONG, Rect::new(0, 0, 40, 40), FORE, BACK);
108        assert_eq!(
109            c.at(20, 20),
110            FORE.to_argb8888(),
111            "shapes are drawn in order, so this hole should have been covered"
112        );
113    }
114
115    /// The same icon at twice the size is the same icon, not a bigger sample of
116    /// it. This is what a mask could not do and the reason these are polygons.
117    #[test]
118    fn an_icon_scales_rather_than_magnifies() {
119        let mut small = TestCanvas::new(32, 32);
120        small
121            .canvas()
122            .draw_icon(&RING, Rect::new(0, 0, 16, 16), FORE, BACK);
123        let mut large = TestCanvas::new(32, 32);
124        large
125            .canvas()
126            .draw_icon(&RING, Rect::new(0, 0, 32, 32), FORE, BACK);
127
128        // Proportionally the same places: the ring at a tenth in, the hole in
129        // the middle.
130        for (c, side) in [(&small, 16), (&large, 32)] {
131            let edge = side / 10;
132            assert_eq!(
133                c.at(edge, side / 2),
134                FORE.to_argb8888(),
135                "the ring is missing at {side}px"
136            );
137            assert_eq!(
138                c.at(side / 2, side / 2),
139                BACK.to_argb8888(),
140                "the hole is missing at {side}px"
141            );
142        }
143    }
144
145    /// An empty rectangle draws nothing rather than dividing by zero.
146    #[test]
147    fn an_empty_rectangle_is_not_drawn() {
148        let mut c = TestCanvas::new(8, 8);
149        c.canvas()
150            .draw_icon(&SQUARE, Rect::new(2, 2, 0, 6), FORE, BACK);
151        c.canvas()
152            .draw_icon(&SQUARE, Rect::new(2, 2, 6, 0), FORE, BACK);
153        assert!(c.pixels().iter().all(|&p| p == 0), "something was drawn");
154    }
155
156    /// A shape with fewer than three points is skipped, not drawn wrong.
157    #[test]
158    fn a_degenerate_shape_is_skipped() {
159        static LINE: Icon = Icon::new(&[
160            Shape::fore(&[(0, 0), (100, 100)]),
161            Shape::fore(&[(0, 40), (100, 40), (100, 60), (0, 60)]),
162        ]);
163        let mut c = TestCanvas::new(20, 20);
164        c.canvas()
165            .draw_icon(&LINE, Rect::new(0, 0, 20, 20), FORE, BACK);
166        // The band still drew, so the skip did not abandon the rest.
167        assert_eq!(c.at(10, 10), FORE.to_argb8888(), "the valid shape was lost");
168    }
169}