Krolyxon
Commit 3d0b2eb

Branches

Refs →

budctlcratesbudctl-guisrcicons.rs

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