use anyhow::Result;
use egui::ColorImage;
use egui::TextureHandle;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc::Receiver;
use std::sync::mpsc::Sender;
use std::sync::mpsc::channel;
use std::thread::spawn;
use tracing::{debug, error, info, trace, warn};
use walkdir::WalkDir;
const MAX_ICON_SEARCH_DEPTH: usize = 4;
pub struct IconInfo {
pub(crate) path: PathBuf,
pub(crate) name: String,
}
#[derive(Clone)]
pub enum Icon {
Texture {
path: PathBuf,
name: String,
texture: TextureHandle,
},
Error {
path: PathBuf,
name: String,
error: String,
},
}
pub struct IconCache {
icon_receiver: Receiver<Icon>,
pub(crate) icons: Vec<Icon>,
pub(crate) total_icons_discovered: usize,
}
impl IconCache {
pub fn new(ctx: &egui::Context) -> Self {
let icon_infos = discover_icons();
let total_icons_discovered = icon_infos.len();
let (tx, rx) = channel();
let moved_ctx = ctx.clone();
spawn(move || {
load_icon_textures(icon_infos, &moved_ctx, tx);
});
IconCache {
icon_receiver: rx,
icons: Default::default(),
total_icons_discovered,
}
}
pub fn update_icon_vec(&mut self) {
for icon in self.icon_receiver.try_iter() {
self.icons.push(icon);
}
}
pub fn update_icon_vec_blocking(&mut self) {
for icon in self.icon_receiver.iter() {
self.icons.push(icon);
}
}
pub fn len(&mut self) -> usize {
self.update_icon_vec();
self.icons.len()
}
pub fn icons(&mut self) -> &Vec<Icon> {
self.update_icon_vec_blocking();
&self.icons
}
pub fn chunks(&mut self, chunk_size: usize) -> std::slice::Chunks<'_, Icon> {
self.update_icon_vec();
self.icons.chunks(chunk_size)
}
}
pub fn get_icon_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
paths.push(PathBuf::from("/run/current-system/sw/share/icons"));
paths.push(PathBuf::from("/run/current-system/sw/share/pixmaps"));
if let Ok(user) = std::env::var("USER") {
paths.push(PathBuf::from(format!(
"/etc/profiles/per-user/{user}/share/icons"
)));
paths.push(PathBuf::from(format!(
"/etc/profiles/per-user/{user}/share/pixmaps"
)));
}
if let Ok(home) = std::env::var("HOME") {
paths.push(PathBuf::from(format!("{}/.nix-profile/share/icons", home)));
paths.push(PathBuf::from(format!(
"{}/.nix-profile/share/pixmaps",
home
)));
}
paths.push(PathBuf::from("/usr/share/icons"));
paths.push(PathBuf::from("/usr/share/pixmaps"));
paths.push(PathBuf::from("/usr/local/share/icons"));
paths.push(PathBuf::from("/usr/local/share/pixmaps"));
if let Ok(home) = std::env::var("HOME") {
paths.push(PathBuf::from(format!("{}/.local/share/icons", home)));
paths.push(PathBuf::from(format!("{}/.icons", home)));
}
paths.push(PathBuf::from("/var/lib/flatpak/exports/share/icons"));
if let Ok(home) = std::env::var("HOME") {
paths.push(PathBuf::from(format!(
"{}/.local/share/flatpak/exports/share/icons",
home
)));
}
paths
}
pub fn discover_icons() -> Vec<IconInfo> {
info!("Starting icon discovery");
let icon_paths = get_icon_search_paths();
let mut found_icons = HashMap::new();
for search_path in icon_paths {
if !search_path.exists() {
warn!("Skipping non-existent path: {}", search_path.display());
continue;
}
debug!("Searching for icons in: {}", search_path.display());
for entry in WalkDir::new(&search_path)
.max_depth(MAX_ICON_SEARCH_DEPTH)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
if matches!(
ext.as_str(),
"png" | "svg" | "ico" | "xpm" | "jpg" | "jpeg" | "gif" | "bmp"
) {
if let Some(name) = path.file_stem() {
let name_str = name.to_string_lossy().to_string();
if !found_icons.contains_key(&name_str)
|| matches!(ext.as_str(), "png" | "svg")
{
trace!("Found icon: {} at {}", name_str, path.display());
found_icons.insert(name_str.clone(), path.to_path_buf());
}
}
}
}
}
}
let mut icons: Vec<IconInfo> = found_icons
.into_iter()
.map(|(name, path)| IconInfo { path, name })
.collect();
icons.sort_by(|a, b| a.name.cmp(&b.name));
info!(
"Icon discovery complete. Found {} unique icons",
icons.len()
);
icons
}
pub fn load_icon_textures(icon_infos: Vec<IconInfo>, ctx: &egui::Context, sender: Sender<Icon>) {
info!("Loading icon textures for {} icons", icon_infos.len());
let mut loaded_count = 0;
let mut error_count = 0;
for icon in icon_infos {
match load_icon_image(&icon.path) {
Ok(color_image) => {
trace!("Successfully loaded texture for: {}", icon.name);
loaded_count += 1;
let texture =
ctx.load_texture(&icon.name, color_image, egui::TextureOptions::default());
sender
.send(Icon::Texture {
path: icon.path,
name: icon.name,
texture: texture,
})
.expect("should be able to send icon texture");
}
Err(e) => {
error!("Failed to load icon {}: {}", icon.name, e);
error_count += 1;
sender
.send(Icon::Error {
path: icon.path,
name: icon.name,
error: format!("Failed to load: {}", e),
})
.expect("should be able to send icon error");
}
}
}
info!(
"Texture loading complete: {} loaded, {} errors",
loaded_count, error_count
);
drop(sender);
}
pub fn load_icon_image(path: &Path) -> Result<ColorImage> {
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
debug!("Loading image: {} (type: {})", path.display(), extension);
match extension.as_str() {
"svg" => load_svg_image(path),
_ => load_raster_image(path),
}
}
fn load_svg_image(path: &Path) -> Result<ColorImage> {
trace!("Loading SVG image: {}", path.display());
let svg_data = std::fs::read_to_string(path)?;
debug!("Parsing SVG tree...");
let usvg_tree = usvg::Tree::from_str(&svg_data, &usvg::Options::default())?;
let size = usvg_tree.size();
let width = size.width() as u32;
let height = size.height() as u32;
let (width, height) = if width > 256 || height > 256 {
let scale = 256.0 / width.max(height) as f32;
debug!(
"Scaling SVG from {}x{} to {}x{}",
size.width() as u32,
size.height() as u32,
(width as f32 * scale) as u32,
(height as f32 * scale) as u32
);
(
(width as f32 * scale) as u32,
(height as f32 * scale) as u32,
)
} else {
(width, height)
};
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height)
.ok_or_else(|| anyhow::anyhow!("Failed to create pixmap"))?;
resvg::render(
&usvg_tree,
resvg::tiny_skia::Transform::from_scale(
width as f32 / size.width(),
height as f32 / size.height(),
),
&mut pixmap.as_mut(),
);
let pixels = pixmap.data();
let mut rgba_pixels = Vec::with_capacity(pixels.len());
for chunk in pixels.chunks_exact(4) {
rgba_pixels.push(chunk[2]); rgba_pixels.push(chunk[1]); rgba_pixels.push(chunk[0]); rgba_pixels.push(chunk[3]); }
Ok(ColorImage::from_rgba_unmultiplied(
[width as usize, height as usize],
&rgba_pixels,
))
}
fn load_raster_image(path: &Path) -> Result<ColorImage> {
trace!("Loading raster image: {}", path.display());
let img = image::open(path)?;
let img = if img.width() > 256 || img.height() > 256 {
debug!(
"Resizing raster image from {}x{} to max 256x256",
img.width(),
img.height()
);
img.resize(256, 256, image::imageops::FilterType::Lanczos3)
} else {
img
};
let img = img.to_rgba8();
let (width, height) = img.dimensions();
Ok(ColorImage::from_rgba_unmultiplied(
[width as usize, height as usize],
img.as_raw(),
))
}