From 3019eaf89c0d1b1e1ce734f0b1252ada0b07f5b2 Mon Sep 17 00:00:00 2001 From: JustForFun88 Date: Sun, 4 May 2025 23:36:09 +0500 Subject: [PATCH] refactor(bar): app widget and icon caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1439 authored and submitted by @JustForFun88 I understand this PR combines two areas of work — refactoring the Applications widget and introducing a new icon caching system — which would ideally be submitted separately. Originally, I only intended to reduce allocations and simplify icon loading in `applications.rs`, but as I worked through it, it became clear that a more general-purpose caching system was needed. One improvement led to another ... šŸ˜„ Apologies for bundling these changes together. If needed, I’m happy to split this PR into smaller, focused ones. Key Changes - Introduced `IconsCache` with unified in-memory image & texture management. - Added `ImageIcon` and `ImageIconId` (based on path or HWND) for caching and reuse. - `Icon::Image` now wraps `ImageIcon`, decoupled from direct `RgbaImage` usage. - Extracted app launch logic into `UserCommand` with built-in cooldown. - Simplified config parsing and UI hover rendering in `App`. - Replaced legacy `ICON_CACHE` in `KomorebiNotificationStateContainerInformation` → Now uses the shared `ImageIcon::try_load(hwnd, ..)` with caching and fallback. Motivation - Reduce redundant image copies and avoid repeated pixel-to-texture conversions. - Cleanly separate concerns for launching and icon handling. - Reuse icons across `Applications`, Komorebi windows, and potentially more in the future. Tested - Works on Windows 11. - Verified path/exe/HWND icon loading and fallback. --- komorebi-bar/src/main.rs | 7 - komorebi-bar/src/widgets/applications.rs | 229 ++++++++++++++--------- komorebi-bar/src/widgets/komorebi.rs | 87 ++------- komorebi-bar/src/widgets/mod.rs | 159 ++++++++++++++++ 4 files changed, 317 insertions(+), 165 deletions(-) diff --git a/komorebi-bar/src/main.rs b/komorebi-bar/src/main.rs index 543fd75a..3f06b6c0 100644 --- a/komorebi-bar/src/main.rs +++ b/komorebi-bar/src/main.rs @@ -15,12 +15,10 @@ use eframe::egui::ViewportBuilder; use font_loader::system_fonts; use hotwatch::EventKind; use hotwatch::Hotwatch; -use image::RgbaImage; use komorebi_client::replace_env_in_path; use komorebi_client::PathExt; use komorebi_client::SocketMessage; use komorebi_client::SubscribeOptions; -use std::collections::HashMap; use std::io::BufReader; use std::io::Read; use std::path::PathBuf; @@ -28,8 +26,6 @@ use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicU32; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use std::sync::LazyLock; -use std::sync::Mutex; use std::time::Duration; use tracing_subscriber::EnvFilter; use windows::Win32::Foundation::HWND; @@ -53,9 +49,6 @@ pub static DEFAULT_PADDING: f32 = 10.0; pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0); pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0); -pub static ICON_CACHE: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - #[derive(Parser)] #[clap(author, about, version)] struct Opts { diff --git a/komorebi-bar/src/widgets/applications.rs b/komorebi-bar/src/widgets/applications.rs index c242a262..1ec8e2a1 100644 --- a/komorebi-bar/src/widgets/applications.rs +++ b/komorebi-bar/src/widgets/applications.rs @@ -1,4 +1,4 @@ -use super::komorebi::img_to_texture; +use super::ImageIcon; use crate::render::RenderConfig; use crate::selected_frame::SelectableFrame; use crate::widgets::widget::BarWidget; @@ -17,14 +17,13 @@ use eframe::egui::Stroke; use eframe::egui::StrokeKind; use eframe::egui::Ui; use eframe::egui::Vec2; -use image::DynamicImage; -use image::RgbaImage; use komorebi_client::PathExt; use serde::Deserialize; use serde::Serialize; +use std::borrow::Cow; use std::path::Path; -use std::path::PathBuf; use std::process::Command; +use std::sync::Arc; use std::time::Duration; use std::time::Instant; use tracing; @@ -119,43 +118,32 @@ impl BarWidget for Applications { impl From<&ApplicationsConfig> for Applications { fn from(applications_config: &ApplicationsConfig) -> Self { - // Allow immediate launch by initializing last_launch in the past. - let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL; - let mut applications_config = applications_config.clone(); let items = applications_config .items - .iter_mut() + .iter() .enumerate() - .map(|(index, app_config)| { - app_config.command = app_config - .command - .replace_env() - .to_string_lossy() - .to_string(); - - if let Some(icon) = &mut app_config.icon { - *icon = icon.replace_env().to_string_lossy().to_string(); - } + .map(|(index, config)| { + let command = UserCommand::new(&config.command); App { - enable: app_config.enable.unwrap_or(applications_config.enable), + enable: config.enable.unwrap_or(applications_config.enable), #[allow(clippy::obfuscated_if_else)] - name: app_config + name: config .name .is_empty() .then(|| format!("App {}", index + 1)) - .unwrap_or_else(|| app_config.name.clone()), - icon: Icon::try_from(app_config), - command: app_config.command.clone(), - display: app_config + .unwrap_or_else(|| config.name.clone()), + icon: Icon::try_from_path(config.icon.as_deref()) + .or_else(|| Icon::try_from_command(&command)), + command, + display: config .display .or(applications_config.display) .unwrap_or_default(), - show_command_on_hover: app_config + show_command_on_hover: config .show_command_on_hover .or(applications_config.show_command_on_hover) .unwrap_or(false), - last_launch, } }) .collect(); @@ -178,13 +166,11 @@ pub struct App { /// Icon to display for this application, if available. pub icon: Option, /// Command to execute when the application is launched. - pub command: String, + pub command: UserCommand, /// Display format (icon, text, or both). pub display: DisplayFormat, /// Whether to show the launch command on hover. pub show_command_on_hover: bool, - /// Last time this application was launched (used for cooldown control). - pub last_launch: Instant, } impl App { @@ -206,17 +192,15 @@ impl App { } // Add hover text with command information + let response = ui.response(); if self.show_command_on_hover { - ui.response() - .on_hover_text(format!("Launch: {}", self.command)); - } else { - ui.response(); + response.on_hover_text(format!("Launch: {}", self.command.as_ref())); } }) .clicked() { // Launch the application when clicked - self.launch_if_ready(); + self.command.launch_if_ready(); } } @@ -236,84 +220,75 @@ impl App { fn draw_name(&self, ui: &mut Ui) { ui.add(Label::new(&self.name).selectable(false)); } - - /// Attempts to launch the specified command in a separate thread if enough time has passed - /// since the last launch. This prevents repeated launches from rapid consecutive clicks. - /// - /// Errors during launch are logged using the `tracing` crate. - pub fn launch_if_ready(&mut self) { - let now = Instant::now(); - if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL { - return; - } - - self.last_launch = now; - let command_string = self.command.clone(); - // Launch the application in a separate thread to avoid blocking the UI - std::thread::spawn(move || { - if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() { - tracing::error!("Failed to launch command '{}': {}", command_string, e); - } - }); - } } -/// Holds decoded image data to be used as an icon in the UI. +/// Holds image/text data to be used as an icon in the UI. +/// This represents source icon data before rendering. #[derive(Clone, Debug)] pub enum Icon { /// RGBA image used for rendering the icon. - Image(RgbaImage), + Image(ImageIcon), /// Text-based icon, e.g. from a font like Nerd Fonts. Text(String), } impl Icon { - /// Attempts to create an `Icon` from the given `AppConfig`. - /// Loads the image from a specified icon path or extracts it from the application's - /// executable if the command points to a valid executable file. + /// Attempts to create an [`Icon`] from a string path or text glyph/glyphs. + /// + /// - Environment variables in the path are resolved using [`PathExt::replace_env`]. + /// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved path. + /// - If the path is invalid but the string is non-empty, it is interpreted as a text-based icon and + /// returned as [`Icon::Text`]. + /// - Returns `None` if the input is empty, `None`, or image loading fails. #[inline] - pub fn try_from(config: &AppConfig) -> Option { - if let Some(icon) = config.icon.as_deref().map(str::trim) { - if !icon.is_empty() { - let path = Path::new(&icon); - if path.is_file() { - match image::open(path).as_ref().map(DynamicImage::to_rgba8) { - Ok(image) => return Some(Icon::Image(image)), - Err(err) => { - tracing::error!("Failed to load icon from {}, error: {}", icon, err) - } - } - } else { - return Some(Icon::Text(icon.to_owned())); - } + pub fn try_from_path(icon: Option<&str>) -> Option { + let icon = icon.map(str::trim)?; + if icon.is_empty() { + return None; + } + + let path = icon.replace_env(); + if !path.is_file() { + return Some(Icon::Text(icon.to_owned())); + } + + let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) { + Ok(img) => Some(img), + Err(err) => { + tracing::error!("Failed to load icon from {:?}, error: {}", path, err); + None } - } + })?; - let binary = PathBuf::from(config.command.split(".exe").next()?); - let path = if binary.is_file() { - Some(binary) - } else { - which(binary).ok() - }; - - match path { - Some(path) => windows_icons::get_icon_by_path(&path.to_string_lossy()) - .or_else(|| windows_icons_fallback::get_icon_by_path(&path.to_string_lossy())) - .map(Icon::Image), - None => None, - } + Some(Icon::Image(image_icon)) } - /// Renders the icon in the given `Ui` context with the specified size. + /// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`]. + /// + /// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path. + /// - Returns [`Icon::Image`] if an icon is successfully extracted. + /// - Returns `None` if the executable path is unavailable or icon extraction fails. + #[inline] + pub fn try_from_command(command: &UserCommand) -> Option { + let path = command.get_executable()?; + let image_icon = ImageIcon::try_load(path.as_ref(), || { + let path_str = path.to_str()?; + windows_icons::get_icon_by_path(path_str) + .or_else(|| windows_icons_fallback::get_icon_by_path(path_str)) + })?; + Some(Icon::Image(image_icon)) + } + + /// Renders the icon in the given [`Ui`] using the provided [`IconConfig`]. #[inline] pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) { match self { - Icon::Image(image) => { + Icon::Image(image_icon) => { Frame::NONE .inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8)) .show(ui, |ui| { ui.add( - Image::from(&img_to_texture(ctx, image)) + Image::from_texture(&image_icon.texture(ctx)) .maintain_aspect_ratio(true) .fit_to_exact_size(Vec2::splat(icon_config.size)), ); @@ -355,3 +330,77 @@ pub struct IconConfig { /// Color of the icon used for text-based icons pub color: Color32, } + +/// A structure to manage command execution with cooldown prevention. +#[derive(Clone, Debug)] +pub struct UserCommand { + /// The command string to execute + pub command: Arc, + /// Last time this command was executed (used for cooldown control) + pub last_launch: Instant, +} + +impl AsRef for UserCommand { + #[inline] + fn as_ref(&self) -> &str { + &self.command + } +} + +impl UserCommand { + /// Creates a new [`UserCommand`] with environment variables in the command path + /// resolved using [`PathExt::replace_env`]. + #[inline] + pub fn new(command: &str) -> Self { + // Allow immediate launch by initializing last_launch in the past + let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL; + + Self { + command: Arc::from(command.replace_env().to_str().unwrap_or_default()), + last_launch, + } + } + + /// Attempts to resolve the executable path from the command string. + /// + /// Resolution logic: + /// - Splits the command by ".exe" and checks if the first part is an existing file. + /// - If not, attempts to locate the binary using [`which`] on this name. + /// - If still unresolved, takes the first word (separated by whitespace) and attempts + /// to find it in the system `PATH` using [`which`]. + /// + /// Returns `None` if no executable path can be determined. + #[inline] + pub fn get_executable(&self) -> Option> { + if let Some(binary) = self.command.split(".exe").next().map(Path::new) { + if binary.is_file() { + return Some(Cow::Borrowed(binary)); + } else if let Ok(binary) = which(binary) { + return Some(Cow::Owned(binary)); + } + } + + which(self.command.split(' ').next()?).ok().map(Cow::Owned) + } + + /// Attempts to launch the specified command in a separate thread if enough time has passed + /// since the last launch. This prevents repeated launches from rapid consecutive clicks. + /// + /// Errors during launch are logged using the `tracing` crate. + pub fn launch_if_ready(&mut self) { + let now = Instant::now(); + // Check if enough time has passed since the last launch + if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL { + return; + } + + self.last_launch = now; + let command_string = self.command.clone(); + // Launch the application in a separate thread to avoid blocking the UI + std::thread::spawn(move || { + if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() { + tracing::error!("Failed to launch command '{}': {}", command_string, e); + } + }); + } +} diff --git a/komorebi-bar/src/widgets/komorebi.rs b/komorebi-bar/src/widgets/komorebi.rs index 36659494..31541a13 100644 --- a/komorebi-bar/src/widgets/komorebi.rs +++ b/komorebi-bar/src/widgets/komorebi.rs @@ -1,3 +1,4 @@ +use super::ImageIcon; use crate::bar::apply_theme; use crate::config::DisplayFormat; use crate::config::KomobarTheme; @@ -8,14 +9,12 @@ use crate::selected_frame::SelectableFrame; use crate::ui::CustomUi; use crate::widgets::komorebi_layout::KomorebiLayout; use crate::widgets::widget::BarWidget; -use crate::ICON_CACHE; use crate::MAX_LABEL_WIDTH; use crate::MONITOR_INDEX; use eframe::egui::text::LayoutJob; use eframe::egui::vec2; use eframe::egui::Align; use eframe::egui::Color32; -use eframe::egui::ColorImage; use eframe::egui::Context; use eframe::egui::CornerRadius; use eframe::egui::Frame; @@ -27,11 +26,8 @@ use eframe::egui::Sense; use eframe::egui::Stroke; use eframe::egui::StrokeKind; use eframe::egui::TextFormat; -use eframe::egui::TextureHandle; -use eframe::egui::TextureOptions; use eframe::egui::Ui; use eframe::egui::Vec2; -use image::RgbaImage; use komorebi_client::Container; use komorebi_client::NotificationEvent; use komorebi_client::PathExt; @@ -233,7 +229,7 @@ impl BarWidget for Komorebi { for (is_focused, container) in containers { for icon in container.icons.iter().flatten().collect::>() { ui.add( - Image::from(&img_to_texture(ctx, icon)) + Image::from(&icon.texture(ctx)) .maintain_aspect_ratio(true) .fit_to_exact_size(if *is_focused { icon_size } else { text_size }), ); @@ -604,7 +600,7 @@ impl BarWidget for Komorebi { )) .show(ui, |ui| { let response = ui.add( - Image::from(&img_to_texture(ctx, img)) + Image::from(&img.texture(ctx) ) .maintain_aspect_ratio(true) .fit_to_exact_size(icon_size), ); @@ -670,13 +666,6 @@ impl BarWidget for Komorebi { } } -pub(super) fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle { - let size = [rgba_image.width() as usize, rgba_image.height() as usize]; - let pixels = rgba_image.as_flat_samples(); - let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()); - ctx.load_texture("icon", color_image, TextureOptions::default()) -} - #[allow(clippy::type_complexity)] #[derive(Clone, Debug)] pub struct KomorebiNotificationState { @@ -868,7 +857,7 @@ impl KomorebiNotificationState { #[derive(Clone, Debug)] pub struct KomorebiNotificationStateContainerInformation { pub titles: Vec, - pub icons: Vec>, + pub icons: Vec>, pub focused_window_idx: usize, } @@ -895,34 +884,17 @@ impl From<&Workspace> for KomorebiNotificationStateContainerInformation { impl From<&Container> for KomorebiNotificationStateContainerInformation { fn from(value: &Container) -> Self { let windows = value.windows().iter().collect::>(); - let mut icons = vec![]; - for window in windows { - let mut icon_cache = ICON_CACHE.lock().unwrap(); - let mut update_cache = false; - let hwnd = window.hwnd; - - match icon_cache.get(&hwnd) { - None => { - let icon = match windows_icons::get_icon_by_hwnd(window.hwnd) { - None => windows_icons_fallback::get_icon_by_process_id(window.process_id()), - Some(icon) => Some(icon), - }; - - icons.push(icon); - update_cache = true; - } - Some(icon) => { - icons.push(Some(icon.clone())); - } - } - - if update_cache { - if let Some(Some(icon)) = icons.last() { - icon_cache.insert(hwnd, icon.clone()); - } - } - } + let icons = windows + .iter() + .map(|window| { + ImageIcon::try_load(window.hwnd, || { + windows_icons::get_icon_by_hwnd(window.hwnd).or_else(|| { + windows_icons_fallback::get_icon_by_process_id(window.process_id()) + }) + }) + }) + .collect::>(); Self { titles: value @@ -938,35 +910,14 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation { impl From<&Window> for KomorebiNotificationStateContainerInformation { fn from(value: &Window) -> Self { - let mut icon_cache = ICON_CACHE.lock().unwrap(); - let mut update_cache = false; - let mut icons = vec![]; - let hwnd = value.hwnd; - - match icon_cache.get(&hwnd) { - None => { - let icon = match windows_icons::get_icon_by_hwnd(hwnd) { - None => windows_icons_fallback::get_icon_by_process_id(value.process_id()), - Some(icon) => Some(icon), - }; - - icons.push(icon); - update_cache = true; - } - Some(icon) => { - icons.push(Some(icon.clone())); - } - } - - if update_cache { - if let Some(Some(icon)) = icons.last() { - icon_cache.insert(hwnd, icon.clone()); - } - } + let icons = ImageIcon::try_load(value.hwnd, || { + windows_icons::get_icon_by_hwnd(value.hwnd) + .or_else(|| windows_icons_fallback::get_icon_by_process_id(value.process_id())) + }); Self { titles: vec![value.title().unwrap_or_default()], - icons, + icons: vec![icons], focused_window_idx: 0, } } diff --git a/komorebi-bar/src/widgets/mod.rs b/komorebi-bar/src/widgets/mod.rs index 17e9ee82..e75b76d2 100644 --- a/komorebi-bar/src/widgets/mod.rs +++ b/komorebi-bar/src/widgets/mod.rs @@ -1,3 +1,14 @@ +use eframe::egui::ColorImage; +use eframe::egui::Context; +use eframe::egui::TextureHandle; +use eframe::egui::TextureOptions; +use image::RgbaImage; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; +use std::sync::LazyLock; +use std::sync::RwLock; + pub mod applications; pub mod battery; pub mod cpu; @@ -12,3 +23,151 @@ pub mod storage; pub mod time; pub mod update; pub mod widget; + +/// Global cache for icon images and their associated GPU textures. +pub static ICONS_CACHE: IconsCache = IconsCache::new(); + +/// In-memory cache for icon images and their associated GPU textures. +/// +/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`]. +/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes. +#[allow(clippy::type_complexity)] +pub struct IconsCache { + textures: LazyLock, HashMap)>>, + images: LazyLock>>>, +} + +impl IconsCache { + /// Creates a new empty IconsCache instance. + #[inline] + pub const fn new() -> Self { + Self { + textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))), + images: LazyLock::new(|| RwLock::new(HashMap::new())), + } + } + + /// Retrieves or creates a texture handle for the given icon ID and image. + /// + /// If a texture for the given ID already exists for the current [`Context`], it is reused. + /// Otherwise, a new texture is created, inserted into the cache, and returned. + /// The cache is reset if the [`Context`] has changed. + #[inline] + pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc) -> TextureHandle { + if let Some(texture) = self.get_texture(ctx, id) { + return texture; + } + let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default()); + self.insert_texture(ctx, id.clone(), texture_handle.clone()); + texture_handle + } + + /// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`]. + pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option { + let textures_lock = self.textures.read().unwrap(); + if textures_lock.0.as_ref() == Some(ctx) { + return textures_lock.1.get(id).cloned(); + } + None + } + + /// Inserts a texture handle, resetting the cache if the [`Context`] has changed. + pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) { + let mut textures_lock = self.textures.write().unwrap(); + + if textures_lock.0.as_ref() != Some(ctx) { + textures_lock.0 = Some(ctx.clone()); + textures_lock.1.clear(); + } + + textures_lock.1.insert(id, texture); + } + + /// Returns the cached image for the given icon ID, if available. + pub fn get_image(&self, id: &ImageIconId) -> Option> { + self.images.read().unwrap().get(id).cloned() + } + + /// Caches a raw [`ColorImage`] associated with the given icon ID. + pub fn insert_image(&self, id: ImageIconId, image: Arc) { + self.images.write().unwrap().insert(id, image); + } +} + +#[inline] +fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage { + let size = [rgba_image.width() as usize, rgba_image.height() as usize]; + let pixels = rgba_image.as_flat_samples(); + ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()) +} + +/// Represents an image-based icon with a unique ID and pixel data. +#[derive(Clone, Debug)] +pub struct ImageIcon { + /// Unique identifier for the image icon, used for texture caching. + pub id: ImageIconId, + /// Shared pixel data of the icon in `ColorImage` format. + pub image: Arc, +} + +impl ImageIcon { + /// Creates a new [`ImageIcon`] from the given ID and image data. + #[inline] + pub fn new(id: ImageIconId, image: Arc) -> Self { + Self { id, image } + } + + /// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached. + /// The loaded image is converted to a [`ColorImage`], cached, and returned. + #[inline] + pub fn try_load(id: impl Into, loader: F) -> Option + where + F: FnOnce() -> Option, + I: Into, + { + let id = id.into(); + let image = ICONS_CACHE.get_image(&id).or_else(|| { + let img = loader()?; + let img = Arc::new(rgba_to_color_image(&img.into())); + ICONS_CACHE.insert_image(id.clone(), img.clone()); + Some(img) + })?; + + Some(ImageIcon::new(id, image)) + } + + /// Returns a texture handle for the icon, using the given [`Context`]. + /// + /// If the texture is already cached in [`ICONS_CACHE`], it is reused. + /// Otherwise, a new texture is created from the [`ColorImage`] and cached. + #[inline] + pub fn texture(&self, ctx: &Context) -> TextureHandle { + ICONS_CACHE.texture(ctx, &self.id, &self.image) + } +} + +/// Unique identifier for an image-based icon. +/// +/// Used to distinguish cached images and textures by either a file path +/// or a Windows window handle. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum ImageIconId { + /// Identifier based on a file system path. + Path(Arc), + /// Windows HWND handle. + Hwnd(isize), +} + +impl From<&Path> for ImageIconId { + #[inline] + fn from(value: &Path) -> Self { + Self::Path(value.into()) + } +} + +impl From for ImageIconId { + #[inline] + fn from(value: isize) -> Self { + Self::Hwnd(value) + } +}