mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-07-11 07:32:51 +02:00
refactor(bar): app widget and icon caching
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.
This commit is contained in:
@@ -15,12 +15,10 @@ use eframe::egui::ViewportBuilder;
|
|||||||
use font_loader::system_fonts;
|
use font_loader::system_fonts;
|
||||||
use hotwatch::EventKind;
|
use hotwatch::EventKind;
|
||||||
use hotwatch::Hotwatch;
|
use hotwatch::Hotwatch;
|
||||||
use image::RgbaImage;
|
|
||||||
use komorebi_client::replace_env_in_path;
|
use komorebi_client::replace_env_in_path;
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use komorebi_client::SocketMessage;
|
use komorebi_client::SocketMessage;
|
||||||
use komorebi_client::SubscribeOptions;
|
use komorebi_client::SubscribeOptions;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -28,8 +26,6 @@ use std::sync::atomic::AtomicI32;
|
|||||||
use std::sync::atomic::AtomicU32;
|
use std::sync::atomic::AtomicU32;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::LazyLock;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use windows::Win32::Foundation::HWND;
|
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_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
|
||||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(author, about, version)]
|
#[clap(author, about, version)]
|
||||||
struct Opts {
|
struct Opts {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::komorebi::img_to_texture;
|
use super::ImageIcon;
|
||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
@@ -17,14 +17,13 @@ use eframe::egui::Stroke;
|
|||||||
use eframe::egui::StrokeKind;
|
use eframe::egui::StrokeKind;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
use image::DynamicImage;
|
|
||||||
use image::RgbaImage;
|
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing;
|
use tracing;
|
||||||
@@ -119,43 +118,32 @@ impl BarWidget for Applications {
|
|||||||
|
|
||||||
impl From<&ApplicationsConfig> for Applications {
|
impl From<&ApplicationsConfig> for Applications {
|
||||||
fn from(applications_config: &ApplicationsConfig) -> Self {
|
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
|
let items = applications_config
|
||||||
.items
|
.items
|
||||||
.iter_mut()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, app_config)| {
|
.map(|(index, config)| {
|
||||||
app_config.command = app_config
|
let command = UserCommand::new(&config.command);
|
||||||
.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();
|
|
||||||
}
|
|
||||||
|
|
||||||
App {
|
App {
|
||||||
enable: app_config.enable.unwrap_or(applications_config.enable),
|
enable: config.enable.unwrap_or(applications_config.enable),
|
||||||
#[allow(clippy::obfuscated_if_else)]
|
#[allow(clippy::obfuscated_if_else)]
|
||||||
name: app_config
|
name: config
|
||||||
.name
|
.name
|
||||||
.is_empty()
|
.is_empty()
|
||||||
.then(|| format!("App {}", index + 1))
|
.then(|| format!("App {}", index + 1))
|
||||||
.unwrap_or_else(|| app_config.name.clone()),
|
.unwrap_or_else(|| config.name.clone()),
|
||||||
icon: Icon::try_from(app_config),
|
icon: Icon::try_from_path(config.icon.as_deref())
|
||||||
command: app_config.command.clone(),
|
.or_else(|| Icon::try_from_command(&command)),
|
||||||
display: app_config
|
command,
|
||||||
|
display: config
|
||||||
.display
|
.display
|
||||||
.or(applications_config.display)
|
.or(applications_config.display)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
show_command_on_hover: app_config
|
show_command_on_hover: config
|
||||||
.show_command_on_hover
|
.show_command_on_hover
|
||||||
.or(applications_config.show_command_on_hover)
|
.or(applications_config.show_command_on_hover)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
last_launch,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -178,13 +166,11 @@ pub struct App {
|
|||||||
/// Icon to display for this application, if available.
|
/// Icon to display for this application, if available.
|
||||||
pub icon: Option<Icon>,
|
pub icon: Option<Icon>,
|
||||||
/// Command to execute when the application is launched.
|
/// Command to execute when the application is launched.
|
||||||
pub command: String,
|
pub command: UserCommand,
|
||||||
/// Display format (icon, text, or both).
|
/// Display format (icon, text, or both).
|
||||||
pub display: DisplayFormat,
|
pub display: DisplayFormat,
|
||||||
/// Whether to show the launch command on hover.
|
/// Whether to show the launch command on hover.
|
||||||
pub show_command_on_hover: bool,
|
pub show_command_on_hover: bool,
|
||||||
/// Last time this application was launched (used for cooldown control).
|
|
||||||
pub last_launch: Instant,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -206,17 +192,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add hover text with command information
|
// Add hover text with command information
|
||||||
|
let response = ui.response();
|
||||||
if self.show_command_on_hover {
|
if self.show_command_on_hover {
|
||||||
ui.response()
|
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
|
||||||
.on_hover_text(format!("Launch: {}", self.command));
|
|
||||||
} else {
|
|
||||||
ui.response();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
// Launch the application when 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) {
|
fn draw_name(&self, ui: &mut Ui) {
|
||||||
ui.add(Label::new(&self.name).selectable(false));
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Icon {
|
pub enum Icon {
|
||||||
/// RGBA image used for rendering the icon.
|
/// RGBA image used for rendering the icon.
|
||||||
Image(RgbaImage),
|
Image(ImageIcon),
|
||||||
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
||||||
Text(String),
|
Text(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
/// Attempts to create an `Icon` from the given `AppConfig`.
|
/// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
|
||||||
/// 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.
|
/// - 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]
|
#[inline]
|
||||||
pub fn try_from(config: &AppConfig) -> Option<Self> {
|
pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
|
||||||
if let Some(icon) = config.icon.as_deref().map(str::trim) {
|
let icon = icon.map(str::trim)?;
|
||||||
if !icon.is_empty() {
|
if icon.is_empty() {
|
||||||
let path = Path::new(&icon);
|
return None;
|
||||||
if path.is_file() {
|
}
|
||||||
match image::open(path).as_ref().map(DynamicImage::to_rgba8) {
|
|
||||||
Ok(image) => return Some(Icon::Image(image)),
|
let path = icon.replace_env();
|
||||||
Err(err) => {
|
if !path.is_file() {
|
||||||
tracing::error!("Failed to load icon from {}, error: {}", icon, err)
|
return Some(Icon::Text(icon.to_owned()));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
|
||||||
return Some(Icon::Text(icon.to_owned()));
|
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()?);
|
Some(Icon::Image(image_icon))
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<Self> {
|
||||||
|
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]
|
#[inline]
|
||||||
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||||
match self {
|
match self {
|
||||||
Icon::Image(image) => {
|
Icon::Image(image_icon) => {
|
||||||
Frame::NONE
|
Frame::NONE
|
||||||
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.add(
|
ui.add(
|
||||||
Image::from(&img_to_texture(ctx, image))
|
Image::from_texture(&image_icon.texture(ctx))
|
||||||
.maintain_aspect_ratio(true)
|
.maintain_aspect_ratio(true)
|
||||||
.fit_to_exact_size(Vec2::splat(icon_config.size)),
|
.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
|
/// Color of the icon used for text-based icons
|
||||||
pub color: Color32,
|
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<str>,
|
||||||
|
/// Last time this command was executed (used for cooldown control)
|
||||||
|
pub last_launch: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> 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<Cow<'_, Path>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::ImageIcon;
|
||||||
use crate::bar::apply_theme;
|
use crate::bar::apply_theme;
|
||||||
use crate::config::DisplayFormat;
|
use crate::config::DisplayFormat;
|
||||||
use crate::config::KomobarTheme;
|
use crate::config::KomobarTheme;
|
||||||
@@ -8,14 +9,12 @@ use crate::selected_frame::SelectableFrame;
|
|||||||
use crate::ui::CustomUi;
|
use crate::ui::CustomUi;
|
||||||
use crate::widgets::komorebi_layout::KomorebiLayout;
|
use crate::widgets::komorebi_layout::KomorebiLayout;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use crate::ICON_CACHE;
|
|
||||||
use crate::MAX_LABEL_WIDTH;
|
use crate::MAX_LABEL_WIDTH;
|
||||||
use crate::MONITOR_INDEX;
|
use crate::MONITOR_INDEX;
|
||||||
use eframe::egui::text::LayoutJob;
|
use eframe::egui::text::LayoutJob;
|
||||||
use eframe::egui::vec2;
|
use eframe::egui::vec2;
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Color32;
|
use eframe::egui::Color32;
|
||||||
use eframe::egui::ColorImage;
|
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::CornerRadius;
|
use eframe::egui::CornerRadius;
|
||||||
use eframe::egui::Frame;
|
use eframe::egui::Frame;
|
||||||
@@ -27,11 +26,8 @@ use eframe::egui::Sense;
|
|||||||
use eframe::egui::Stroke;
|
use eframe::egui::Stroke;
|
||||||
use eframe::egui::StrokeKind;
|
use eframe::egui::StrokeKind;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::TextureHandle;
|
|
||||||
use eframe::egui::TextureOptions;
|
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
use image::RgbaImage;
|
|
||||||
use komorebi_client::Container;
|
use komorebi_client::Container;
|
||||||
use komorebi_client::NotificationEvent;
|
use komorebi_client::NotificationEvent;
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
@@ -233,7 +229,7 @@ impl BarWidget for Komorebi {
|
|||||||
for (is_focused, container) in containers {
|
for (is_focused, container) in containers {
|
||||||
for icon in container.icons.iter().flatten().collect::<Vec<_>>() {
|
for icon in container.icons.iter().flatten().collect::<Vec<_>>() {
|
||||||
ui.add(
|
ui.add(
|
||||||
Image::from(&img_to_texture(ctx, icon))
|
Image::from(&icon.texture(ctx))
|
||||||
.maintain_aspect_ratio(true)
|
.maintain_aspect_ratio(true)
|
||||||
.fit_to_exact_size(if *is_focused { icon_size } else { text_size }),
|
.fit_to_exact_size(if *is_focused { icon_size } else { text_size }),
|
||||||
);
|
);
|
||||||
@@ -604,7 +600,7 @@ impl BarWidget for Komorebi {
|
|||||||
))
|
))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let response = ui.add(
|
let response = ui.add(
|
||||||
Image::from(&img_to_texture(ctx, img))
|
Image::from(&img.texture(ctx) )
|
||||||
.maintain_aspect_ratio(true)
|
.maintain_aspect_ratio(true)
|
||||||
.fit_to_exact_size(icon_size),
|
.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)]
|
#[allow(clippy::type_complexity)]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KomorebiNotificationState {
|
pub struct KomorebiNotificationState {
|
||||||
@@ -868,7 +857,7 @@ impl KomorebiNotificationState {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KomorebiNotificationStateContainerInformation {
|
pub struct KomorebiNotificationStateContainerInformation {
|
||||||
pub titles: Vec<String>,
|
pub titles: Vec<String>,
|
||||||
pub icons: Vec<Option<RgbaImage>>,
|
pub icons: Vec<Option<ImageIcon>>,
|
||||||
pub focused_window_idx: usize,
|
pub focused_window_idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -895,34 +884,17 @@ impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
|
|||||||
impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||||
fn from(value: &Container) -> Self {
|
fn from(value: &Container) -> Self {
|
||||||
let windows = value.windows().iter().collect::<Vec<_>>();
|
let windows = value.windows().iter().collect::<Vec<_>>();
|
||||||
let mut icons = vec![];
|
|
||||||
|
|
||||||
for window in windows {
|
let icons = windows
|
||||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
.iter()
|
||||||
let mut update_cache = false;
|
.map(|window| {
|
||||||
let hwnd = window.hwnd;
|
ImageIcon::try_load(window.hwnd, || {
|
||||||
|
windows_icons::get_icon_by_hwnd(window.hwnd).or_else(|| {
|
||||||
match icon_cache.get(&hwnd) {
|
windows_icons_fallback::get_icon_by_process_id(window.process_id())
|
||||||
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),
|
.collect::<Vec<_>>();
|
||||||
};
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
titles: value
|
titles: value
|
||||||
@@ -938,35 +910,14 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
|||||||
|
|
||||||
impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||||
fn from(value: &Window) -> Self {
|
fn from(value: &Window) -> Self {
|
||||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
let icons = ImageIcon::try_load(value.hwnd, || {
|
||||||
let mut update_cache = false;
|
windows_icons::get_icon_by_hwnd(value.hwnd)
|
||||||
let mut icons = vec![];
|
.or_else(|| windows_icons_fallback::get_icon_by_process_id(value.process_id()))
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
titles: vec![value.title().unwrap_or_default()],
|
titles: vec![value.title().unwrap_or_default()],
|
||||||
icons,
|
icons: vec![icons],
|
||||||
focused_window_idx: 0,
|
focused_window_idx: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 applications;
|
||||||
pub mod battery;
|
pub mod battery;
|
||||||
pub mod cpu;
|
pub mod cpu;
|
||||||
@@ -12,3 +23,151 @@ pub mod storage;
|
|||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod widget;
|
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<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
|
||||||
|
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ColorImage>) -> 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<TextureHandle> {
|
||||||
|
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<Arc<ColorImage>> {
|
||||||
|
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<ColorImage>) {
|
||||||
|
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<ColorImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageIcon {
|
||||||
|
/// Creates a new [`ImageIcon`] from the given ID and image data.
|
||||||
|
#[inline]
|
||||||
|
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> 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<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Option<I>,
|
||||||
|
I: Into<RgbaImage>,
|
||||||
|
{
|
||||||
|
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<Path>),
|
||||||
|
/// Windows HWND handle.
|
||||||
|
Hwnd(isize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Path> for ImageIconId {
|
||||||
|
#[inline]
|
||||||
|
fn from(value: &Path) -> Self {
|
||||||
|
Self::Path(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<isize> for ImageIconId {
|
||||||
|
#[inline]
|
||||||
|
fn from(value: isize) -> Self {
|
||||||
|
Self::Hwnd(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user