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}