hyprcorrect_ui/icon.rs
1//! Hyprcorrect's app icon, rasterized from a bundled SVG.
2//!
3//! The SVG includes a `<text>` element so it relies on usvg's font
4//! database — populated from the host's system fonts at first use.
5//! A static `OnceLock` keeps that one-time discovery off the prefs
6//! window's hot path.
7
8use std::sync::OnceLock;
9
10const APP_ICON_SVG: &[u8] = include_bytes!("../assets/icons/svg/hyprcorrect.svg");
11/// Brand blue from the bundled SVG. Recolor uses this as the search
12/// token so the tray can swap to white without a second asset file.
13const BRAND_FILL: &str = "#4a86c0";
14/// Color the tray icon uses. Matches the white system-status glyphs
15/// on dark Waybar bars (Bluetooth, Wi-Fi, sound, battery). Vernier
16/// uses the same convention for its `*-symbolic` tray asset.
17const TRAY_FILL: &str = "#ffffff";
18/// Single pixmap size we publish to the SNI tray. Mirrors Vernier's
19/// approach: one large pixmap lets the SNI host downscale crisply
20/// for whatever slot size the user's bar uses, rather than us
21/// guessing 22/44 and possibly missing the bar's actual slot.
22const TRAY_PIXMAP_SIZE: u32 = 64;
23
24/// Raw bytes of the bundled SVG. Used by the autostart writer to
25/// drop a copy at a known path so the generated `.desktop` can
26/// reference our icon with an absolute path — Walker / other XDG
27/// launchers then show our brand instead of the system theme's
28/// `tools-check-spelling` fallback.
29pub fn app_icon_svg_bytes() -> &'static [u8] {
30 APP_ICON_SVG
31}
32
33/// One pixmap for the SNI tray. ARGB32 in network (big-endian) byte
34/// order — on a little-endian CPU that means bytes laid out as
35/// A, R, G, B per pixel. `paused` halves the alpha channel so the
36/// tray icon dims to "I'm here but not listening" without needing
37/// a second SVG asset.
38pub struct TrayPixmap {
39 pub size: u32,
40 pub argb: Vec<u8>,
41}
42
43/// Rasterize the app icon for the SNI tray as a single large
44/// pixmap. The bundled SVG's brand blue is swapped for white at
45/// render time so the mark matches the system-status convention
46/// most Wayland bars (Bluetooth, Wi-Fi, sound) follow on dark
47/// surfaces.
48///
49/// Returns a `Vec<TrayPixmap>` so the platform layer's existing
50/// shape (multiple pixmaps to choose from) stays a one-liner;
51/// here it just has a single entry.
52pub fn tray_pixmaps(_sizes: &[u32], paused: bool) -> Vec<TrayPixmap> {
53 let rgba = render_recolored_rgba(TRAY_PIXMAP_SIZE, TRAY_FILL);
54 let argb = rgba_to_argb_with_alpha(&rgba, paused);
55 vec![TrayPixmap {
56 size: TRAY_PIXMAP_SIZE,
57 argb,
58 }]
59}
60
61/// Render the bundled SVG with [`BRAND_FILL`] swapped for `fill`.
62/// Used by [`tray_pixmaps`] to produce a monochrome variant from
63/// the one source asset; falls back to the unmodified blue if the
64/// SVG bytes aren't valid UTF-8 (they always are, but the guard
65/// keeps us from blowing up at render time).
66fn render_recolored_rgba(size: u32, fill: &str) -> Vec<u8> {
67 let Ok(text) = std::str::from_utf8(APP_ICON_SVG) else {
68 return render_app_icon_rgba(size);
69 };
70 let recolored = text.replace(BRAND_FILL, fill);
71 render_svg_bytes_rgba(recolored.as_bytes(), size)
72}
73
74/// Convert RGBA8 → ARGB32 big-endian (network byte order). If
75/// `paused`, halve each pixel's alpha — gives the tray a muted
76/// look without requiring a second SVG asset.
77fn rgba_to_argb_with_alpha(rgba: &[u8], paused: bool) -> Vec<u8> {
78 let mut out = Vec::with_capacity(rgba.len());
79 for chunk in rgba.chunks_exact(4) {
80 let [r, g, b, a] = [chunk[0], chunk[1], chunk[2], chunk[3]];
81 let a = if paused { a / 2 } else { a };
82 out.extend_from_slice(&[a, r, g, b]);
83 }
84 out
85}
86
87/// Render the app icon to an RGBA8 buffer of `size`×`size` pixels.
88/// Returns an all-transparent buffer if the SVG fails to parse — the
89/// prefs sidebar gracefully falls back to the bare heading.
90pub fn render_app_icon_rgba(size: u32) -> Vec<u8> {
91 render_svg_bytes_rgba(APP_ICON_SVG, size)
92}
93
94/// Shared rasterizer used by both the brand-color and recolored
95/// (tray) paths. Returns an all-transparent buffer on parse failure
96/// so callers degrade gracefully.
97fn render_svg_bytes_rgba(svg: &[u8], size: u32) -> Vec<u8> {
98 let opts = usvg::Options {
99 fontdb: fontdb().clone(),
100 ..usvg::Options::default()
101 };
102 let Ok(tree) = usvg::Tree::from_data(svg, &opts) else {
103 return vec![0; (size as usize) * (size as usize) * 4];
104 };
105 let mut pixmap = tiny_skia::Pixmap::new(size, size)
106 .unwrap_or_else(|| tiny_skia::Pixmap::new(1, 1).expect("1x1 pixmap"));
107 let svg_size = tree.size();
108 let scale_x = size as f32 / svg_size.width();
109 let scale_y = size as f32 / svg_size.height();
110 let scale = scale_x.min(scale_y);
111 let transform = tiny_skia::Transform::from_scale(scale, scale);
112 resvg::render(&tree, transform, &mut pixmap.as_mut());
113 pixmap.take()
114}
115
116fn fontdb() -> &'static std::sync::Arc<usvg::fontdb::Database> {
117 static DB: OnceLock<std::sync::Arc<usvg::fontdb::Database>> = OnceLock::new();
118 DB.get_or_init(|| {
119 let mut db = usvg::fontdb::Database::new();
120 db.load_system_fonts();
121 std::sync::Arc::new(db)
122 })
123}