nandi/jolt-nativepublic Fork 0
789bb134c91bfdee346df2dc9656bf2e7f9bbd1a
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

app_icon.rs · 171 lines · 5.4 KBRust Blame HistoryRaw
Bring vidya in cfd3e36 nandi 19d ago1//! Embed a window / taskbar app icon from PNG bytes.
2//!
3//! Apps typically `include_bytes!` a 128–256px PNG and pass it through
4//! [`with_app_icon`] / [`with_app_icon_id`] when building the eframe viewport:
5//!
6//! ```ignore
7//! use egui::ViewportBuilder;
8//! use vidya::with_app_icon_id;
9//!
10//! let viewport = with_app_icon_id(
11//! ViewportBuilder::default().with_title("My App"),
12//! "my-app", // Wayland app_id — match .desktop StartupWMClass / file id
13//! include_bytes!("../assets/icon-256.png"),
14//! );
15//! ```
16//!
17//! On Wayland, [`with_app_icon_id`] is preferred: the compositor matches
18//! `app_id` to a FreeDesktop entry’s `Icon=` when the `.desktop` is on
19//! `XDG_DATA_DIRS`. The embedded icon still sets the client-side window icon
20//! where the platform supports it (X11, Windows, …).
21
22use std::io::Cursor;
23use std::sync::Arc;
24
25use egui::{IconData, ViewportBuilder};
26
27/// Error decoding an app icon PNG.
28#[derive(Debug, Clone)]
29pub struct AppIconError(String);
30
31impl AppIconError {
32 pub fn message(&self) -> &str {
33 &self.0
34 }
35}
36
37impl std::fmt::Display for AppIconError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(f, "app icon: {}", self.0)
40 }
41}
42
43impl std::error::Error for AppIconError {}
44
45/// Decode PNG bytes into egui [`IconData`] (RGBA8, unpremultiplied).
46///
47/// Intended for `include_bytes!("…png")` assets used as the native window icon.
48pub fn icon_data_from_png(png_bytes: &[u8]) -> Result<IconData, AppIconError> {
49 let (width, height, rgba) = decode_png_rgba(png_bytes)?;
50 Ok(IconData {
51 rgba,
52 width,
53 height,
54 })
55}
56
57/// Attach an embedded PNG as the viewport window icon.
58///
59/// On decode failure the viewport is returned unchanged so a bad asset never
60/// blocks app launch.
61pub fn with_app_icon(viewport: ViewportBuilder, png_bytes: &[u8]) -> ViewportBuilder {
62 match icon_data_from_png(png_bytes) {
63 Ok(icon) => viewport.with_icon(Arc::new(icon)),
64 Err(_) => viewport,
65 }
66}
67
68/// Like [`with_app_icon`], and set Wayland `app_id`.
69///
70/// `app_id` should match the FreeDesktop application id / `StartupWMClass`
71/// (e.g. `"usage"` for `usage.desktop`).
72pub fn with_app_icon_id(
73 viewport: ViewportBuilder,
74 app_id: impl Into<String>,
75 png_bytes: &[u8],
76) -> ViewportBuilder {
77 with_app_icon(viewport.with_app_id(app_id), png_bytes)
78}
79
80/// Fallible variant of [`with_app_icon`].
81pub fn try_with_app_icon(
82 viewport: ViewportBuilder,
83 png_bytes: &[u8],
84) -> Result<ViewportBuilder, AppIconError> {
85 let icon = icon_data_from_png(png_bytes)?;
86 Ok(viewport.with_icon(Arc::new(icon)))
87}
88
89/// Fallible variant of [`with_app_icon_id`].
90pub fn try_with_app_icon_id(
91 viewport: ViewportBuilder,
92 app_id: impl Into<String>,
93 png_bytes: &[u8],
94) -> Result<ViewportBuilder, AppIconError> {
95 try_with_app_icon(viewport.with_app_id(app_id), png_bytes)
96}
97
98fn decode_png_rgba(bytes: &[u8]) -> Result<(u32, u32, Vec<u8>), AppIconError> {
99 let mut decoder = png::Decoder::new(Cursor::new(bytes));
100 decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::ALPHA);
101 let mut reader = decoder
102 .read_info()
103 .map_err(|e| AppIconError(format!("png header: {e}")))?;
104 let mut buf = vec![0; reader.output_buffer_size()];
105 let info = reader
106 .next_frame(&mut buf)
107 .map_err(|e| AppIconError(format!("png frame: {e}")))?;
108 let w = info.width;
109 let h = info.height;
110 let raw = &buf[..info.buffer_size()];
111 let rgba: Vec<u8> = match info.color_type {
112 png::ColorType::Rgba => raw.to_vec(),
113 png::ColorType::Rgb => {
114 let mut out = Vec::with_capacity((w * h * 4) as usize);
Clear the clippy backlog the new CI enforces 4956d1e nandi 13d ago115 for chunk in raw.as_chunks::<3>().0 {
Bring vidya in cfd3e36 nandi 19d ago116 out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
117 }
118 out
119 }
120 other => {
121 return Err(AppIconError(format!(
122 "unsupported png color type: {other:?}"
123 )));
124 }
125 };
126 if rgba.len() != (w * h * 4) as usize {
127 return Err(AppIconError(format!(
128 "rgba length {} != {}×{}×4",
129 rgba.len(),
130 w,
131 h
132 )));
133 }
134 Ok((w, h, rgba))
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 /// 1×1 red RGB PNG (valid CRC).
142 const TINY_PNG: &[u8] = &[
143 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
144 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
145 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0xf8,
146 0xcf, 0xc0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xc9, 0xfe, 0x92, 0xef, 0x00, 0x00, 0x00,
147 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
148 ];
149
150 #[test]
151 fn icon_data_from_tiny_png() {
152 let icon = icon_data_from_png(TINY_PNG).expect("decode");
153 assert_eq!(icon.width, 1);
154 assert_eq!(icon.height, 1);
155 assert_eq!(icon.rgba.len(), 4);
156 }
157
158 #[test]
159 fn with_app_icon_id_sets_app_id() {
160 let vp = with_app_icon_id(ViewportBuilder::default(), "usage", TINY_PNG);
161 assert_eq!(vp.app_id.as_deref(), Some("usage"));
162 assert!(vp.icon.is_some());
163 }
164
165 #[test]
166 fn bad_png_leaves_viewport_unchanged() {
167 let vp = with_app_icon(ViewportBuilder::default(), b"not a png");
168 assert!(vp.icon.is_none());
169 assert!(icon_data_from_png(b"not a png").is_err());
170 }
171}