Krolyxon
Commit 6e8f058

Branches

Refs →

budctlcratesbudctl-guisrcicons.rs

399 lines · 12.0 KB · Rust

1//! Icons and battery rings, 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 std::f32::consts::{FRAC_PI_2, TAU};
5
6use iced::widget::canvas::{self, Frame, LineCap, LineJoin, Path, Stroke, path::Arc};
7use iced::{Color, Element, Point, Radians, Rectangle, Renderer, Size, Theme, mouse};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Icon {
11 Overview,
12 Equalizer,
13 Gestures,
14 Earbud,
15 Settings,
16 NoiseOff,
17 Anc,
18 Transparency,
19 Game,
20 Spatial,
21 Bass,
22 Wind,
23 Dual,
24 Chevron,
25 Bolt,
26 Check,
27 Find,
28 Info,
29 Chip,
30 Keys,
31 Pencil,
32 Trash,
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
43struct Glyph {
44 icon: Icon,
45 color: Color,
46}
47
48impl<Message> canvas::Program<Message> for Glyph {
49 type State = ();
50
51 fn draw(
52 &self,
53 _state: &(),
54 renderer: &Renderer,
55 _theme: &Theme,
56 bounds: Rectangle,
57 _cursor: mouse::Cursor,
58 ) -> Vec<canvas::Geometry> {
59 let mut frame = Frame::new(renderer, bounds.size());
60 let mut pen = Pen::new(&mut frame, Point::ORIGIN, bounds.width, self.color);
61 draw_icon(&mut pen, self.icon);
62 vec![frame.into_geometry()]
63 }
64}
65
66/// Draws on a 24 × 24 grid placed at `origin`, `size` pixels wide.
67struct Pen<'f> {
68 frame: &'f mut Frame,
69 origin: Point,
70 scale: f32,
71 color: Color,
72 width: f32,
73}
74
75impl<'f> Pen<'f> {
76 fn new(frame: &'f mut Frame, origin: Point, size: f32, color: Color) -> Self {
77 let scale = size / 24.0;
78 Pen {
79 frame,
80 origin,
81 scale,
82 color,
83 width: 1.9 * scale,
84 }
85 }
86
87 fn at(&self, x: f32, y: f32) -> Point {
88 Point::new(self.origin.x + x * self.scale, self.origin.y + y * self.scale)
89 }
90
91 fn stroke(&mut self, path: &Path) {
92 let stroke = Stroke::default()
93 .with_color(self.color)
94 .with_width(self.width)
95 .with_line_cap(LineCap::Round)
96 .with_line_join(LineJoin::Round);
97 self.frame.stroke(path, stroke);
98 }
99
100 fn polyline(&mut self, points: &[(f32, f32)], closed: bool) {
101 let path = Path::new(|b| {
102 for (n, &(x, y)) in points.iter().enumerate() {
103 if n == 0 {
104 b.move_to(self.at(x, y));
105 } else {
106 b.line_to(self.at(x, y));
107 }
108 }
109 if closed {
110 b.close();
111 }
112 });
113 self.stroke(&path);
114 }
115
116 fn line(&mut self, from: (f32, f32), to: (f32, f32)) {
117 self.polyline(&[from, to], false);
118 }
119
120 fn circle(&mut self, x: f32, y: f32, r: f32) {
121 let path = Path::circle(self.at(x, y), r * self.scale);
122 self.stroke(&path);
123 }
124
125 fn dot(&mut self, x: f32, y: f32, r: f32) {
126 let path = Path::circle(self.at(x, y), r * self.scale);
127 self.frame.fill(&path, self.color);
128 }
129
130 /// Degrees, clockwise from three o'clock. `to` may be below `from` to go the other way.
131 fn arc(&mut self, x: f32, y: f32, r: f32, from: f32, to: f32) {
132 let path = Path::new(|b| {
133 b.arc(Arc {
134 center: self.at(x, y),
135 radius: r * self.scale,
136 start_angle: Radians(from.to_radians()),
137 end_angle: Radians(to.to_radians()),
138 });
139 });
140 self.stroke(&path);
141 }
142
143 fn rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
144 let path = self.rounded(x, y, w, h, r);
145 self.stroke(&path);
146 }
147
148 fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
149 let path = self.rounded(x, y, w, h, r);
150 self.frame.fill(&path, self.color);
151 }
152
153 fn rounded(&self, x: f32, y: f32, w: f32, h: f32, r: f32) -> Path {
154 Path::rounded_rectangle(
155 self.at(x, y),
156 Size::new(w * self.scale, h * self.scale),
157 (r * self.scale).into(),
158 )
159 }
160}
161
162fn draw_icon(pen: &mut Pen, icon: Icon) {
163 match icon {
164 Icon::Overview => {
165 for (x, y) in [(3.0, 3.0), (13.5, 3.0), (3.0, 13.5), (13.5, 13.5)] {
166 pen.rect(x, y, 7.5, 7.5, 2.0);
167 }
168 }
169 Icon::Equalizer => {
170 for (x, knob) in [(6.0, 14.0), (12.0, 8.0), (18.0, 16.0)] {
171 pen.line((x, 3.5), (x, 20.5));
172 pen.line((x - 2.5, knob), (x + 2.5, knob));
173 }
174 }
175 Icon::Gestures => {
176 pen.rect(9.5, 11.0, 5.0, 11.0, 2.5);
177 pen.arc(12.0, 11.5, 4.0, 200.0, 340.0);
178 pen.arc(12.0, 11.5, 7.5, 210.0, 330.0);
179 }
180 Icon::Earbud => earbud(pen, false, None),
181 Icon::Settings => {
182 for (y, knob) in [(6.0, 15.0), (12.0, 8.0), (18.0, 13.0)] {
183 pen.line((3.5, y), (20.5, y));
184 pen.dot(knob, y, 2.6);
185 }
186 }
187 Icon::NoiseOff => {
188 pen.circle(12.0, 12.0, 8.5);
189 pen.line((6.0, 6.0), (18.0, 18.0));
190 }
191 Icon::Anc => {
192 pen.circle(12.0, 12.0, 8.5);
193 pen.circle(12.0, 12.0, 4.5);
194 pen.dot(12.0, 12.0, 1.4);
195 }
196 Icon::Transparency => {
197 for n in 0..12u8 {
198 let angle = f32::from(n) * 30f32.to_radians();
199 pen.dot(12.0 + 8.5 * angle.cos(), 12.0 + 8.5 * angle.sin(), 1.15);
200 }
201 pen.dot(12.0, 12.0, 2.2);
202 }
203 Icon::Game | Icon::Bolt => pen.polyline(
204 &[
205 (13.5, 2.5),
206 (5.0, 13.5),
207 (11.5, 13.5),
208 (10.5, 21.5),
209 (19.0, 10.5),
210 (12.5, 10.5),
211 ],
212 true,
213 ),
214 Icon::Spatial => {
215 pen.dot(12.0, 12.0, 2.4);
216 for (r, spread) in [(5.5, 40.0), (9.5, 35.0)] {
217 pen.arc(12.0, 12.0, r, -spread, spread);
218 pen.arc(12.0, 12.0, r, 180.0 - spread, 180.0 + spread);
219 }
220 }
221 Icon::Bass => {
222 pen.polyline(
223 &[
224 (3.5, 9.5),
225 (7.5, 9.5),
226 (12.5, 5.0),
227 (12.5, 19.0),
228 (7.5, 14.5),
229 (3.5, 14.5),
230 ],
231 true,
232 );
233 pen.arc(13.0, 12.0, 4.0, -50.0, 50.0);
234 pen.arc(13.0, 12.0, 8.0, -50.0, 50.0);
235 }
236 Icon::Wind => {
237 pen.line((3.0, 8.5), (13.0, 8.5));
238 pen.arc(13.0, 6.0, 2.5, 90.0, -180.0);
239 pen.line((3.0, 12.5), (17.5, 12.5));
240 pen.arc(17.5, 15.5, 3.0, -90.0, 180.0);
241 pen.line((3.0, 16.5), (9.0, 16.5));
242 }
243 Icon::Dual => {
244 pen.rect(2.5, 5.0, 12.5, 9.0, 1.5);
245 pen.line((1.5, 17.5), (16.0, 17.5));
246 pen.rect(17.5, 8.0, 4.5, 11.0, 1.2);
247 }
248 Icon::Chevron => pen.polyline(&[(9.0, 5.5), (15.5, 12.0), (9.0, 18.5)], false),
249 Icon::Find => {
250 pen.circle(12.0, 12.0, 6.5);
251 pen.dot(12.0, 12.0, 2.0);
252 for (from, to) in [
253 ((12.0, 2.0), (12.0, 5.0)),
254 ((12.0, 19.0), (12.0, 22.0)),
255 ((2.0, 12.0), (5.0, 12.0)),
256 ((19.0, 12.0), (22.0, 12.0)),
257 ] {
258 pen.line(from, to);
259 }
260 }
261 Icon::Pencil => {
262 pen.polyline(&[(4.5, 19.5), (5.5, 15.0), (15.5, 5.0), (19.0, 8.5), (9.0, 18.5)], true);
263 pen.line((13.0, 7.5), (16.5, 11.0));
264 }
265 Icon::Trash => {
266 pen.line((4.0, 6.5), (20.0, 6.5));
267 pen.polyline(&[(9.0, 6.5), (9.5, 3.5), (14.5, 3.5), (15.0, 6.5)], false);
268 pen.polyline(&[(6.0, 6.5), (7.0, 20.5), (17.0, 20.5), (18.0, 6.5)], false);
269 pen.line((10.0, 10.5), (10.0, 16.5));
270 pen.line((14.0, 10.5), (14.0, 16.5));
271 }
272 Icon::Info => {
273 pen.circle(12.0, 12.0, 9.0);
274 pen.line((12.0, 11.0), (12.0, 16.5));
275 pen.dot(12.0, 7.8, 1.2);
276 }
277 Icon::Chip => {
278 pen.rect(6.5, 6.5, 11.0, 11.0, 2.0);
279 for at in [9.5, 14.5] {
280 pen.line((at, 2.5), (at, 6.5));
281 pen.line((at, 17.5), (at, 21.5));
282 pen.line((2.5, at), (6.5, at));
283 pen.line((17.5, at), (21.5, at));
284 }
285 }
286 Icon::Keys => {
287 pen.rect(2.5, 6.0, 19.0, 12.0, 2.5);
288 for x in [6.5, 10.0, 13.5, 17.0] {
289 pen.dot(x, 10.0, 0.9);
290 }
291 pen.line((8.0, 14.0), (16.0, 14.0));
292 }
293 Icon::Check => pen.polyline(&[(4.5, 12.5), (9.5, 17.5), (19.5, 7.0)], false),
294 }
295}
296
297/// A filled earbud: the head, the ear tip pointing in, and the stem going down. The tip is
298/// drawn in `tip`, or in the part's color when there is none.
299fn earbud(pen: &mut Pen, mirrored: bool, tip: Option<Color>) {
300 let x = |x: f32, w: f32| if mirrored { 24.0 - x - w } else { x };
301 let color = pen.color;
302 pen.color = tip.unwrap_or(color);
303 pen.fill_rect(x(14.5, 6.0), 5.0, 6.0, 7.0, 2.8);
304 pen.color = color;
305 pen.dot(x(11.0, 0.0), 8.5, 6.3);
306 pen.fill_rect(x(6.6, 4.4), 10.0, 4.4, 12.5, 2.2);
307}
308
309/// What a battery ring shows in its middle.
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum Part {
312 Left,
313 Right,
314 Case,
315}
316
317/// A battery ring: the track, an arc for the charge (none when unknown) and the part in the
318/// middle.
319pub fn battery_ring<'a, Message: 'a>(
320 part: Part,
321 level: Option<u8>,
322 size: f32,
323 colors: RingColors,
324) -> Element<'a, Message> {
325 iced::widget::canvas(Ring { part, level, colors })
326 .width(size)
327 .height(size)
328 .into()
329}
330
331#[derive(Debug, Clone, Copy)]
332pub struct RingColors {
333 pub track: Color,
334 pub charge: Color,
335 pub part: Color,
336 /// The earbuds' ear tips.
337 pub tip: Color,
338 /// Details cut into the case (the lid line, the light).
339 pub cutout: Color,
340}
341
342struct Ring {
343 part: Part,
344 level: Option<u8>,
345 colors: RingColors,
346}
347
348impl<Message> canvas::Program<Message> for Ring {
349 type State = ();
350
351 fn draw(
352 &self,
353 _state: &(),
354 renderer: &Renderer,
355 _theme: &Theme,
356 bounds: Rectangle,
357 _cursor: mouse::Cursor,
358 ) -> Vec<canvas::Geometry> {
359 let mut frame = Frame::new(renderer, bounds.size());
360 let center = frame.center();
361 let width = bounds.width * 0.06;
362 let radius = bounds.width / 2.0 - width / 2.0 - 1.0;
363 let ring = |color: Color| {
364 Stroke::default()
365 .with_color(color)
366 .with_width(width)
367 .with_line_cap(LineCap::Round)
368 };
369 frame.stroke(&Path::circle(center, radius), ring(self.colors.track));
370 if let Some(level) = self.level.filter(|&level| level > 0) {
371 let sweep = f32::from(level.min(100)) / 100.0 * TAU;
372 let arc = Path::new(|b| {
373 b.arc(Arc {
374 center,
375 radius,
376 start_angle: Radians(-FRAC_PI_2),
377 end_angle: Radians(-FRAC_PI_2 + sweep),
378 });
379 });
380 frame.stroke(&arc, ring(self.colors.charge));
381 }
382
383 let art = bounds.width * 0.44;
384 let origin = Point::new(center.x - art / 2.0, center.y - art / 2.0);
385 let mut pen = Pen::new(&mut frame, origin, art, self.colors.part);
386 match self.part {
387 Part::Left => earbud(&mut pen, false, Some(self.colors.tip)),
388 Part::Right => earbud(&mut pen, true, Some(self.colors.tip)),
389 Part::Case => {
390 pen.fill_rect(2.0, 5.5, 20.0, 13.5, 6.0);
391 pen.color = self.colors.cutout;
392 pen.width = 0.9 * pen.scale;
393 pen.line((2.0, 10.0), (22.0, 10.0));
394 pen.dot(12.0, 14.5, 1.0);
395 }
396 }
397 vec![frame.into_geometry()]
398 }
399}