status bar
git clone https://git.lucas.co/cce-status-interface.git
src/icons.rs (4.3K)
1 //! Bundled cce-icons glyphs, tinted for the bar.
2 //!
3 //! The stat modules draw a cce-icons glyph with their value superimposed on it,
4 //! and the glyph has to wear the same color as the number — `module
5 //! { text_color }`, the battery's accent when it is low or charging, the
6 //! volume's `disabled_color` while muted. `cce_ui::upload_icon` cannot do that:
7 //! a `Prim::Image` carries alpha but no color, and the bundled artwork is
8 //! white. So this rasterizes the SVG itself, multiplies the pixels by the
9 //! color, and uploads the result — one texture per `(name, px, color)`,
10 //! cached for the life of the process (the key space is a handful of colors
11 //! times one size, so nothing is ever freed).
12 //!
13 //! The artwork comes from the cce-icons crate via [`cce_ui::icons_dir`]
14 //! (`$CCE_ICONS_DIR`, else `~/projects/cce/cce-icons/svg`). A glyph that is
15 //! missing or unparsable yields `None` — the caller keeps its text readout as
16 //! the fallback — and is logged once, since the miss is cached too.
17
18 use std::collections::HashMap;
19 use std::sync::Mutex;
20
21 /// `(name, px, tint)` → the uploaded texture, or `None` for a glyph that could
22 /// not be loaded (the miss is cached too, so the warning is logged once).
23 type Key = (String, u32, [u8; 3]);
24 static CACHE: Mutex<Option<HashMap<Key, Option<(u32, u32, u32)>>>> = Mutex::new(None);
25
26 /// Rasterize `<name>.svg` from cce-icons at `px` on its longer side, tinted
27 /// to `rgb` (raw sRGB, like every text color here — uploaded images are
28 /// sampled as sRGB), and upload it as a renderer texture. Returns the image
29 /// id plus the pixel size for `PaintCtx::image`.
30 pub(crate) fn tinted_icon(name: &str, px: u32, rgb: [u8; 3]) -> Option<(u32, u32, u32)> {
31 let key = (name.to_string(), px, rgb);
32 let mut guard = CACHE.lock().unwrap();
33 let cache = guard.get_or_insert_with(HashMap::new);
34 if let Some(hit) = cache.get(&key) {
35 return *hit;
36 }
37 let loaded = (|| {
38 let path = format!("{}/{name}.svg", cce_ui::icons_dir());
39 let data = match std::fs::read(&path) {
40 Ok(d) => d,
41 Err(e) => {
42 log::warn!("[icons] {path}: {e} — falling back to the text readout");
43 return None;
44 }
45 };
46 let (mut rgba, w, h) = cce_ui::rasterize_svg(&data, px).or_else(|| {
47 log::warn!("[icons] {path}: unparsable SVG — falling back to the text readout");
48 None
49 })?;
50 // The artwork is white, so multiplying is tinting; anything the
51 // glyph shades darker (a cut-through keyhole) stays proportionally
52 // darker in the tint.
53 for p in rgba.chunks_exact_mut(4) {
54 for (c, &t) in p[..3].iter_mut().zip(rgb.iter()) {
55 *c = ((*c as u16 * t as u16 + 127) / 255) as u8;
56 }
57 }
58 Some((cce_ui::vk::upload_rgba(rgba, w, h), w, h))
59 })();
60 cache.insert(key, loaded);
61 loaded
62 }
63
64 /// The `[u8; 3]` a text color becomes for tinting — the same conversion
65 /// `StyledLabel` applies to its color, so glyph and number match exactly.
66 pub(crate) fn tint_of(color: [f32; 4]) -> [u8; 3] {
67 [
68 (color[0] * 255.0).round() as u8,
69 (color[1] * 255.0).round() as u8,
70 (color[2] * 255.0).round() as u8,
71 ]
72 }
73
74 /// Forget every uploaded glyph, freeing its texture.
75 ///
76 /// The cache holds **renderer** image ids, and a renderer does not outlive its
77 /// session: `cce-ui`'s `window_runner` repairs a lost Wayland transport by
78 /// opening a new session around the same `Application`, which rebuilds the
79 /// renderer and with it the image table. The cached ids then name images that
80 /// no longer exist, and a draw for an unknown id is skipped rather than
81 /// reported — so a bar that reconnected came back with its numbers and no
82 /// glyphs at all, until the process was restarted.
83 ///
84 /// Called from `Application::renderer_init` when the renderer it is handed is a
85 /// *replacement*; the first renderer of the process is the one the uploads
86 /// queued from `new()` are waiting for, so dropping them there would only
87 /// upload, destroy and re-upload the same five glyphs before the first frame.
88 pub(crate) fn drop_textures() {
89 let mut guard = CACHE.lock().unwrap();
90 let Some(cache) = guard.as_mut() else { return };
91 for (id, _, _) in cache.values().flatten() {
92 cce_ui::vk::free_image(*id);
93 }
94 cache.clear();
95 }