Coverage Report

Created: 2026-10-23 22:23

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
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
}