Krolyxon
Commit 832675a

Branches

Refs →

budctlcratesbudctl-guisrcicons.rs

306 lines · 9.1 KB · Rust

1//! Icons, drawn as vector paths so they take any theme color and stay sharp at
2//! any scale. Icons are designed on a 24 × 24 grid.
3
4use iced::widget::canvas::{self, Frame, LineCap, LineJoin, Path, Stroke, path::Arc};
5use iced::{Color, Element, Point, Radians, Rectangle, Renderer, Size, Theme, mouse};
6
7use crate::kept::Kept;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Icon {
11 Equalizer,
12 Gestures,
13 Earbud,
14 Settings,
15 NoiseOff,
16 Anc,
17 Transparency,
18 Game,
19 Spatial,
20 Bass,
21 Wind,
22 Dual,
23 Chevron,
24 Bolt,
25 Find,
26 Info,
27 Chip,
28 Keys,
29 Pencil,
30 Trash,
31 Laptop,
32 Phone,
33}
34
35/// `icon` at `size` pixels, in `color`.
36pub fn icon<'a, Message: 'a>(icon: Icon, size: f32, color: Color) -> Element<'a, Message> {
37 iced::widget::canvas(Glyph { icon, color })
38 .width(size)
39 .height(size)
40 .into()
41}
42
43#[derive(Clone, Copy, PartialEq)]
44struct Glyph {
45 icon: Icon,
46 color: Color,
47}
48
49impl<Message> canvas::Program<Message> for Glyph {
50 type State = Kept<Glyph>;
51
52 fn draw(
53 &self,
54 kept: &Kept<Glyph>,
55 renderer: &Renderer,
56 _theme: &Theme,
57 bounds: Rectangle,
58 _cursor: mouse::Cursor,
59 ) -> Vec<canvas::Geometry> {
60 let geometry = kept.draw(renderer, bounds.size(), *self, |frame| {
61 let mut pen = Pen::new(frame, Point::ORIGIN, bounds.width, self.color);
62 draw_icon(&mut pen, self.icon);
63 });
64 vec![geometry]
65 }
66}
67
68/// Draws on a 24 × 24 grid placed at `origin`, `size` pixels wide.
69struct Pen<'f> {
70 frame: &'f mut Frame,
71 origin: Point,
72 scale: f32,
73 color: Color,
74 width: f32,
75}
76
77impl<'f> Pen<'f> {
78 fn new(frame: &'f mut Frame, origin: Point, size: f32, color: Color) -> Self {
79 let scale = size / 24.0;
80 Pen {
81 frame,
82 origin,
83 scale,
84 color,
85 width: 1.9 * scale,
86 }
87 }
88
89 fn at(&self, x: f32, y: f32) -> Point {
90 Point::new(self.origin.x + x * self.scale, self.origin.y + y * self.scale)
91 }
92
93 fn stroke(&mut self, path: &Path) {
94 let stroke = Stroke::default()
95 .with_color(self.color)
96 .with_width(self.width)
97 .with_line_cap(LineCap::Round)
98 .with_line_join(LineJoin::Round);
99 self.frame.stroke(path, stroke);
100 }
101
102 fn polyline(&mut self, points: &[(f32, f32)], closed: bool) {
103 let path = Path::new(|b| {
104 for (n, &(x, y)) in points.iter().enumerate() {
105 if n == 0 {
106 b.move_to(self.at(x, y));
107 } else {
108 b.line_to(self.at(x, y));
109 }
110 }
111 if closed {
112 b.close();
113 }
114 });
115 self.stroke(&path);
116 }
117
118 fn line(&mut self, from: (f32, f32), to: (f32, f32)) {
119 self.polyline(&[from, to], false);
120 }
121
122 fn circle(&mut self, x: f32, y: f32, r: f32) {
123 let path = Path::circle(self.at(x, y), r * self.scale);
124 self.stroke(&path);
125 }
126
127 fn dot(&mut self, x: f32, y: f32, r: f32) {
128 let path = Path::circle(self.at(x, y), r * self.scale);
129 self.frame.fill(&path, self.color);
130 }
131
132 /// Degrees, clockwise from three o'clock. `to` may be below `from` to go the other way.
133 fn arc(&mut self, x: f32, y: f32, r: f32, from: f32, to: f32) {
134 let path = Path::new(|b| {
135 b.arc(Arc {
136 center: self.at(x, y),
137 radius: r * self.scale,
138 start_angle: Radians(from.to_radians()),
139 end_angle: Radians(to.to_radians()),
140 });
141 });
142 self.stroke(&path);
143 }
144
145 fn rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
146 let path = self.rounded(x, y, w, h, r);
147 self.stroke(&path);
148 }
149
150 fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
151 let path = self.rounded(x, y, w, h, r);
152 self.frame.fill(&path, self.color);
153 }
154
155 fn rounded(&self, x: f32, y: f32, w: f32, h: f32, r: f32) -> Path {
156 Path::rounded_rectangle(
157 self.at(x, y),
158 Size::new(w * self.scale, h * self.scale),
159 (r * self.scale).into(),
160 )
161 }
162}
163
164fn draw_icon(pen: &mut Pen, icon: Icon) {
165 match icon {
166 Icon::Equalizer => {
167 for (x, knob) in [(6.0, 14.0), (12.0, 8.0), (18.0, 16.0)] {
168 pen.line((x, 3.5), (x, 20.5));
169 pen.line((x - 2.5, knob), (x + 2.5, knob));
170 }
171 }
172 Icon::Gestures => {
173 pen.rect(9.5, 11.0, 5.0, 11.0, 2.5);
174 pen.arc(12.0, 11.5, 4.0, 200.0, 340.0);
175 pen.arc(12.0, 11.5, 7.5, 210.0, 330.0);
176 }
177 Icon::Earbud => earbud(pen),
178 Icon::Settings => {
179 for (y, knob) in [(6.0, 15.0), (12.0, 8.0), (18.0, 13.0)] {
180 pen.line((3.5, y), (20.5, y));
181 pen.dot(knob, y, 2.6);
182 }
183 }
184 Icon::NoiseOff => {
185 pen.circle(12.0, 12.0, 8.5);
186 pen.line((6.0, 6.0), (18.0, 18.0));
187 }
188 Icon::Anc => {
189 pen.circle(12.0, 12.0, 8.5);
190 pen.circle(12.0, 12.0, 4.5);
191 pen.dot(12.0, 12.0, 1.4);
192 }
193 Icon::Transparency => {
194 for n in 0..12u8 {
195 let angle = f32::from(n) * 30f32.to_radians();
196 pen.dot(12.0 + 8.5 * angle.cos(), 12.0 + 8.5 * angle.sin(), 1.15);
197 }
198 pen.dot(12.0, 12.0, 2.2);
199 }
200 Icon::Game | Icon::Bolt => pen.polyline(
201 &[
202 (13.5, 2.5),
203 (5.0, 13.5),
204 (11.5, 13.5),
205 (10.5, 21.5),
206 (19.0, 10.5),
207 (12.5, 10.5),
208 ],
209 true,
210 ),
211 Icon::Spatial => {
212 pen.dot(12.0, 12.0, 2.4);
213 for (r, spread) in [(5.5, 40.0), (9.5, 35.0)] {
214 pen.arc(12.0, 12.0, r, -spread, spread);
215 pen.arc(12.0, 12.0, r, 180.0 - spread, 180.0 + spread);
216 }
217 }
218 Icon::Bass => {
219 pen.polyline(
220 &[
221 (3.5, 9.5),
222 (7.5, 9.5),
223 (12.5, 5.0),
224 (12.5, 19.0),
225 (7.5, 14.5),
226 (3.5, 14.5),
227 ],
228 true,
229 );
230 pen.arc(13.0, 12.0, 4.0, -50.0, 50.0);
231 pen.arc(13.0, 12.0, 8.0, -50.0, 50.0);
232 }
233 Icon::Wind => {
234 pen.line((3.0, 8.5), (13.0, 8.5));
235 pen.arc(13.0, 6.0, 2.5, 90.0, -180.0);
236 pen.line((3.0, 12.5), (17.5, 12.5));
237 pen.arc(17.5, 15.5, 3.0, -90.0, 180.0);
238 pen.line((3.0, 16.5), (9.0, 16.5));
239 }
240 Icon::Dual => {
241 pen.rect(2.5, 5.0, 12.5, 9.0, 1.5);
242 pen.line((1.5, 17.5), (16.0, 17.5));
243 pen.rect(17.5, 8.0, 4.5, 11.0, 1.2);
244 }
245 Icon::Chevron => pen.polyline(&[(9.0, 5.5), (15.5, 12.0), (9.0, 18.5)], false),
246 Icon::Find => {
247 pen.circle(12.0, 12.0, 6.5);
248 pen.dot(12.0, 12.0, 2.0);
249 for (from, to) in [
250 ((12.0, 2.0), (12.0, 5.0)),
251 ((12.0, 19.0), (12.0, 22.0)),
252 ((2.0, 12.0), (5.0, 12.0)),
253 ((19.0, 12.0), (22.0, 12.0)),
254 ] {
255 pen.line(from, to);
256 }
257 }
258 Icon::Pencil => {
259 pen.polyline(&[(4.5, 19.5), (5.5, 15.0), (15.5, 5.0), (19.0, 8.5), (9.0, 18.5)], true);
260 pen.line((13.0, 7.5), (16.5, 11.0));
261 }
262 Icon::Trash => {
263 pen.line((4.0, 6.5), (20.0, 6.5));
264 pen.polyline(&[(9.0, 6.5), (9.5, 3.5), (14.5, 3.5), (15.0, 6.5)], false);
265 pen.polyline(&[(6.0, 6.5), (7.0, 20.5), (17.0, 20.5), (18.0, 6.5)], false);
266 pen.line((10.0, 10.5), (10.0, 16.5));
267 pen.line((14.0, 10.5), (14.0, 16.5));
268 }
269 Icon::Info => {
270 pen.circle(12.0, 12.0, 9.0);
271 pen.line((12.0, 11.0), (12.0, 16.5));
272 pen.dot(12.0, 7.8, 1.2);
273 }
274 Icon::Chip => {
275 pen.rect(6.5, 6.5, 11.0, 11.0, 2.0);
276 for at in [9.5, 14.5] {
277 pen.line((at, 2.5), (at, 6.5));
278 pen.line((at, 17.5), (at, 21.5));
279 pen.line((2.5, at), (6.5, at));
280 pen.line((17.5, at), (21.5, at));
281 }
282 }
283 Icon::Keys => {
284 pen.rect(2.5, 6.0, 19.0, 12.0, 2.5);
285 for x in [6.5, 10.0, 13.5, 17.0] {
286 pen.dot(x, 10.0, 0.9);
287 }
288 pen.line((8.0, 14.0), (16.0, 14.0));
289 }
290 Icon::Laptop => {
291 pen.rect(4.0, 5.0, 16.0, 11.0, 1.5);
292 pen.line((2.0, 19.0), (22.0, 19.0));
293 }
294 Icon::Phone => {
295 pen.rect(7.0, 2.5, 10.0, 19.0, 2.0);
296 pen.line((10.5, 18.5), (13.5, 18.5));
297 }
298 }
299}
300
301/// A filled earbud: the head, the ear tip pointing in, and the stem going down.
302fn earbud(pen: &mut Pen) {
303 pen.fill_rect(14.5, 5.0, 6.0, 7.0, 2.8);
304 pen.dot(11.0, 8.5, 6.3);
305 pen.fill_rect(6.6, 10.0, 4.4, 12.5, 2.2);
306}