super-stt-app/src/ui/icons.rs
Line | Count | Source |
1 | | // SPDX-License-Identifier: GPL-3.0-only |
2 | | |
3 | | //! Phosphor icons (regular weight) embedded for the settings UI. |
4 | | //! |
5 | | //! Source: <https://github.com/phosphor-icons/core>, copied verbatim from |
6 | | //! `raw/regular/` — the stroked source art, not the outlined `assets/regular/` |
7 | | //! export, which draws the same glyphs as filled paths and sits visibly heavier |
8 | | //! beside these. Take any new icon from `raw/regular/` too. The SVGs use |
9 | | //! `currentColor`, so they pick up the active theme via the symbolic flag. |
10 | | |
11 | | use std::sync::LazyLock; |
12 | | |
13 | | use cosmic::iced::Length; |
14 | | use cosmic::iced::widget::svg; |
15 | | use cosmic::widget::icon::{self, Icon}; |
16 | | |
17 | | pub const GEAR: &[u8] = include_bytes!("../../resources/icons/phosphor/gear.svg"); |
18 | | pub const MICROPHONE: &[u8] = include_bytes!("../../resources/icons/phosphor/microphone.svg"); |
19 | | pub const KEYBOARD: &[u8] = include_bytes!("../../resources/icons/phosphor/keyboard.svg"); |
20 | | pub const BRAIN: &[u8] = include_bytes!("../../resources/icons/phosphor/brain.svg"); |
21 | | pub const PLUG: &[u8] = include_bytes!("../../resources/icons/phosphor/plug.svg"); |
22 | | pub const WARNING: &[u8] = include_bytes!("../../resources/icons/phosphor/warning.svg"); |
23 | | pub const CPU: &[u8] = include_bytes!("../../resources/icons/phosphor/cpu.svg"); |
24 | | pub const GRAPHICS_CARD: &[u8] = include_bytes!("../../resources/icons/phosphor/graphics-card.svg"); |
25 | | pub const CLOUD: &[u8] = include_bytes!("../../resources/icons/phosphor/cloud.svg"); |
26 | | pub const DOTS_THREE_VERTICAL: &[u8] = |
27 | | include_bytes!("../../resources/icons/phosphor/dots-three-vertical.svg"); |
28 | | pub const ARROWS_CLOCKWISE: &[u8] = |
29 | | include_bytes!("../../resources/icons/phosphor/arrows-clockwise.svg"); |
30 | | pub const PLAY: &[u8] = include_bytes!("../../resources/icons/phosphor/play.svg"); |
31 | | pub const STOP: &[u8] = include_bytes!("../../resources/icons/phosphor/stop.svg"); |
32 | | pub const GIT_BRANCH: &[u8] = include_bytes!("../../resources/icons/phosphor/git-branch.svg"); |
33 | | pub const BOOKS: &[u8] = include_bytes!("../../resources/icons/phosphor/books.svg"); |
34 | | pub const CARDS: &[u8] = include_bytes!("../../resources/icons/phosphor/cards.svg"); |
35 | | pub const X: &[u8] = include_bytes!("../../resources/icons/phosphor/x.svg"); |
36 | | pub const CIRCLE_NOTCH: &[u8] = include_bytes!("../../resources/icons/phosphor/circle-notch.svg"); |
37 | | |
38 | | /// The Super STT app logo, full-color artwork. Not a Phosphor glyph, so it |
39 | | /// lives at the app resources root rather than the phosphor set; shown beside |
40 | | /// the app name in the window header. |
41 | | pub const APP_LOGO: &[u8] = include_bytes!("../../resources/super-stt-icon.svg"); |
42 | | |
43 | | /// Build a themable [`Icon`] from one of the embedded Phosphor SVGs. |
44 | 0 | pub fn phosphor(bytes: &'static [u8]) -> Icon { |
45 | 0 | icon::from_svg_bytes(bytes).symbolic(true).icon() |
46 | 0 | } |
47 | | |
48 | | /// The symbolic [`Handle`](icon::Handle) for one of the embedded Phosphor SVGs, |
49 | | /// for widgets that take a handle directly (e.g. `button::icon`). |
50 | 0 | pub fn phosphor_handle(bytes: &'static [u8]) -> icon::Handle { |
51 | 0 | icon::from_svg_bytes(bytes).symbolic(true) |
52 | 0 | } |
53 | | |
54 | | /// Shared builder for a fixed-size symbolic Phosphor [`Svg`](cosmic::widget::Svg) |
55 | | /// tinted by a caller-supplied [`Svg`](cosmic::theme::Svg) color class. The |
56 | | /// plain [`Icon`] wrapper hides the `svg::Style::color` knob that does the |
57 | | /// tinting, so the tinted variants return the bare `Svg` widget instead. |
58 | 0 | fn tinted_svg( |
59 | 0 | bytes: &'static [u8], |
60 | 0 | size: f32, |
61 | 0 | class: cosmic::theme::Svg, |
62 | 0 | ) -> cosmic::widget::Svg<'static, cosmic::Theme> { |
63 | 0 | cosmic::widget::Svg::<cosmic::Theme>::new(svg::Handle::from_memory(bytes)) |
64 | 0 | .symbolic(true) |
65 | 0 | .class(class) |
66 | 0 | .width(Length::Fixed(size)) |
67 | 0 | .height(Length::Fixed(size)) |
68 | 0 | } |
69 | | |
70 | | /// A Phosphor icon tinted with the theme's *destructive* (red) color — used |
71 | | /// for unmet-requirement warnings inside a backend card. |
72 | 0 | pub fn phosphor_destructive( |
73 | 0 | bytes: &'static [u8], |
74 | 0 | size: f32, |
75 | 0 | ) -> cosmic::widget::Svg<'static, cosmic::Theme> { |
76 | 0 | tinted_svg( |
77 | 0 | bytes, |
78 | 0 | size, |
79 | 0 | cosmic::theme::Svg::custom(|t| svg::Style { |
80 | 0 | color: Some(t.cosmic().destructive.base.into()), |
81 | 0 | }), |
82 | | ) |
83 | 0 | } |
84 | | |
85 | | /// A Phosphor icon tinted with the theme's *warning* (yellow) color — used |
86 | | /// for the advisory "model may not fit in GPU memory" warning. |
87 | 0 | pub fn phosphor_warning( |
88 | 0 | bytes: &'static [u8], |
89 | 0 | size: f32, |
90 | 0 | ) -> cosmic::widget::Svg<'static, cosmic::Theme> { |
91 | 0 | tinted_svg( |
92 | 0 | bytes, |
93 | 0 | size, |
94 | 0 | cosmic::theme::Svg::custom(|t| svg::Style { |
95 | 0 | color: Some(t.cosmic().warning.base.into()), |
96 | 0 | }), |
97 | | ) |
98 | 0 | } |
99 | | |
100 | | /// A Phosphor icon tinted with an explicit `color` the caller resolves from |
101 | | /// the active theme. Used by the backend-capability chips, whose tone |
102 | | /// (accent / neutral) is chosen at view-build time. |
103 | 0 | pub fn phosphor_tinted( |
104 | 0 | bytes: &'static [u8], |
105 | 0 | size: f32, |
106 | 0 | color: cosmic::iced::Color, |
107 | 0 | ) -> cosmic::widget::Svg<'static, cosmic::Theme> { |
108 | 0 | tinted_svg( |
109 | 0 | bytes, |
110 | 0 | size, |
111 | 0 | cosmic::theme::Svg::custom(move |_| svg::Style { color: Some(color) }), |
112 | | ) |
113 | 0 | } |
114 | | |
115 | | /// How many angles the [`CIRCLE_NOTCH`] spinner is drawn at. |
116 | | /// |
117 | | /// Divides 360 exactly, so every frame lands on a whole number of degrees. |
118 | | pub const SPINNER_FRAME_COUNT: usize = 24; |
119 | | |
120 | | /// How long each spinner frame holds — [`SPINNER_FRAME_COUNT`] of these make |
121 | | /// one turn a second. |
122 | | pub const SPINNER_FRAME: std::time::Duration = std::time::Duration::from_millis(42); |
123 | | |
124 | | /// [`CIRCLE_NOTCH`] as SVG source, pre-turned to each of the |
125 | | /// [`SPINNER_FRAME_COUNT`] angles. |
126 | | /// |
127 | | /// Rotating at draw time is the obvious way and the wrong one here: iced |
128 | | /// rasterizes an SVG once at its layout size, caches that bitmap under |
129 | | /// `(id, width, height, color)` — rotation is not in the key — and then spins |
130 | | /// it in the shader sampling *nearest-neighbour*. An 18px glyph turned that way |
131 | | /// comes out visibly chewed. Turning the geometry instead means resvg draws |
132 | | /// each angle from the path with its own antialiasing. The frames are a fixed |
133 | | /// set of byte strings and the cache keys off their hash, so this costs exactly |
134 | | /// `SPINNER_FRAME_COUNT` small rasters rather than one per angle ever shown. |
135 | 0 | static SPINNER_FRAMES: LazyLock<Vec<Vec<u8>>> = LazyLock::new(|| { |
136 | 0 | let inner = svg_inner(CIRCLE_NOTCH); |
137 | 0 | (0..SPINNER_FRAME_COUNT) |
138 | 0 | .map(|i| { |
139 | | // Whole degrees: 360 / 24 = 15, so no rounding creeps in and the |
140 | | // frame at index 0 is the source art untouched. |
141 | 0 | let deg = 360 * i / SPINNER_FRAME_COUNT; |
142 | 0 | format!( |
143 | | "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\">\ |
144 | | <g transform=\"rotate({deg} 128 128)\">{inner}</g></svg>" |
145 | | ) |
146 | 0 | .into_bytes() |
147 | 0 | }) |
148 | 0 | .collect() |
149 | 0 | }); |
150 | | |
151 | | /// The drawing inside an embedded Phosphor SVG, without its root element, so it |
152 | | /// can be re-wrapped in one carrying a transform. |
153 | | /// |
154 | | /// Every file in `resources/icons/phosphor/` is one `<svg …>…</svg>` on a |
155 | | /// single line, which is what makes this a slice rather than a parse. |
156 | 0 | fn svg_inner(bytes: &'static [u8]) -> &'static str { |
157 | 0 | let s = std::str::from_utf8(bytes).expect("embedded Phosphor SVGs are UTF-8"); |
158 | 0 | let body = s.find('>').map_or(s, |i| &s[i + 1..]); |
159 | 0 | body.trim_end().trim_end_matches("</svg>") |
160 | 0 | } |
161 | | |
162 | | /// The [`CIRCLE_NOTCH`] spinner at `frame`, which the caller advances on its |
163 | | /// own tick. Indices past the last frame wrap, so a counter can just count up. |
164 | 0 | pub fn spinner( |
165 | 0 | size: f32, |
166 | 0 | color: cosmic::iced::Color, |
167 | 0 | frame: usize, |
168 | 0 | ) -> cosmic::widget::Svg<'static, cosmic::Theme> { |
169 | 0 | let bytes = SPINNER_FRAMES[frame % SPINNER_FRAME_COUNT].as_slice(); |
170 | 0 | tinted_svg( |
171 | 0 | bytes, |
172 | 0 | size, |
173 | 0 | cosmic::theme::Svg::custom(move |_| svg::Style { color: Some(color) }), |
174 | | ) |
175 | 0 | } |
176 | | |
177 | | /// The Super STT logo ([`APP_LOGO`]) rendered at `size` px in its own colors. |
178 | | /// |
179 | | /// The artwork is multi-color, so it is deliberately neither marked symbolic |
180 | | /// nor given a tinting class: an explicit `svg::Style::color` is applied to the |
181 | | /// whole image regardless of the symbolic flag, which would flatten the logo to |
182 | | /// a single-color silhouette. |
183 | 0 | pub fn app_logo(size: f32) -> cosmic::widget::Svg<'static, cosmic::Theme> { |
184 | 0 | cosmic::widget::Svg::<cosmic::Theme>::new(svg::Handle::from_memory(APP_LOGO)) |
185 | 0 | .width(Length::Fixed(size)) |
186 | 0 | .height(Length::Fixed(size)) |
187 | 0 | } |