Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: cache app icons #144

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 5 additions & 18 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ use crate::painter::{find_clicked_app_index, GdiAAPainter};
use crate::startup::Startup;
use crate::trayicon::TrayIcon;
use crate::utils::{
check_error, create_hicon_from_resource, get_foreground_window, get_module_icon,
get_module_icon_ex, get_uwp_icon_data, get_window_user_data, is_iconic_window,
check_error, get_app_icon, get_foreground_window, get_window_user_data, is_iconic_window,
is_running_as_admin, list_windows, set_foreground_window, set_window_user_data,
};

Expand Down Expand Up @@ -52,7 +51,7 @@ pub struct App {
config: Config,
switch_windows_state: SwitchWindowsState,
switch_apps_state: Option<SwitchAppsState>,
uwp_icons: HashMap<String, Vec<u8>>,
cached_icons: HashMap<String, HICON>,
painter: GdiAAPainter,
}

Expand Down Expand Up @@ -84,7 +83,7 @@ impl App {
modifier_released: true,
},
switch_apps_state: None,
uwp_icons: Default::default(),
cached_icons: Default::default(),
painter,
};

Expand Down Expand Up @@ -401,20 +400,8 @@ impl App {
} else {
hwnds[0].0
};
let mut module_hicon = None;
if module_path.starts_with("C:\\Program Files\\WindowsApps") {
if let Some(data) = self.uwp_icons.get(module_path) {
module_hicon = create_hicon_from_resource(data)
} else if let Some(data) = get_uwp_icon_data(module_path) {
module_hicon = create_hicon_from_resource(&data);
self.uwp_icons.insert(module_path.clone(), data);
}
}
if let Some(hicon) = module_hicon.or_else(|| get_module_icon_ex(module_path)) {
apps.push((hicon, module_hwnd));
} else if let Some(hicon) = get_module_icon(module_hwnd) {
apps.push((hicon, module_hwnd));
}
let module_hicon = get_app_icon(&mut self.cached_icons, module_path, module_hwnd);
apps.push((module_hicon, module_hwnd));
}
let num_apps = apps.len() as i32;
if num_apps == 0 {
Expand Down
9 changes: 3 additions & 6 deletions src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use windows::Win32::Graphics::GdiPlus::{
};
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
use windows::Win32::UI::WindowsAndMessaging::{
DestroyIcon, DrawIconEx, GetCursorPos, ShowWindow, UpdateLayeredWindow, DI_NORMAL, SW_HIDE,
SW_SHOW, ULW_ALPHA,
DrawIconEx, GetCursorPos, ShowWindow, UpdateLayeredWindow, DI_NORMAL, SW_HIDE, SW_SHOW,
ULW_ALPHA,
};
use windows::Win32::{Foundation::HWND, Graphics::Gdi::GetDC};

Expand Down Expand Up @@ -196,13 +196,10 @@ impl GdiAAPainter {
self.show = true;
}

pub fn unpaint(&mut self, state: SwitchAppsState) {
pub fn unpaint(&mut self, _state: SwitchAppsState) {
unsafe {
let _ = ShowWindow(self.hwnd, SW_HIDE);
}
for (hicon, _) in state.apps {
let _ = unsafe { DestroyIcon(hicon) };
}
self.show = false;
}
}
Expand Down
195 changes: 195 additions & 0 deletions src/utils/app_icon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use std::{
collections::HashMap,
fs::File,
io::{BufReader, Read},
mem,
path::{Path, PathBuf},
time,
};

use windows::{
core::PCWSTR,
Win32::{
Foundation::{HWND, TRUE, WPARAM},
Storage::FileSystem::FILE_ATTRIBUTE_NORMAL,
UI::{
Controls::IImageList,
Shell::{SHGetFileInfoW, SHGetImageList, SHFILEINFOW, SHGFI_SYSICONINDEX},
WindowsAndMessaging::{
CopyIcon, CreateIconFromResourceEx, LoadIconW, SendMessageW, GCL_HICON, HICON,
ICON_BIG, IDI_APPLICATION, LR_DEFAULTCOLOR, WM_GETICON,
},
},
},
};
use xml::reader::XmlEvent;
use xml::EventReader;

pub fn get_app_icon(
cached_icons: &mut HashMap<String, HICON>,
module_path: &str,
hwnd: HWND,
) -> HICON {
if let Some(icon) = cached_icons.get(module_path) {
return *icon;
}

if module_path.starts_with("C:\\Program Files\\WindowsApps") {
let icon = get_appx_logo_path(module_path)
.and_then(|image_path| load_image_as_hicon(&image_path))
.unwrap_or_else(fallback_icon);
cached_icons.insert(module_path.to_string(), icon);
return icon;
}
let icon = get_exe_icon(module_path)
.or_else(|| get_window_icon(hwnd))
.unwrap_or_else(fallback_icon);
cached_icons.insert(module_path.to_string(), icon);
icon
}

fn get_appx_logo_path(module_path: &str) -> Option<PathBuf> {
let module_path = PathBuf::from(module_path);
let executable = module_path.file_name()?.to_string_lossy();
let module_dir = module_path.parent()?;
let manifest_path = module_dir.join("AppxManifest.xml");
let manifest_file = File::open(manifest_path).ok()?;
let manifest_file = BufReader::new(manifest_file); // Buffering is important for performance
let reader = EventReader::new(manifest_file);
let mut logo_value = None;
let mut matched = false;
let mut paths = vec![];
let mut depth = 0;
for e in reader {
match e {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
if paths.len() == depth {
paths.push(name.local_name.clone())
}
let xpath = paths.join("/");
if xpath == "Package/Applications/Application" {
matched = attributes
.iter()
.any(|v| v.name.local_name == "Executable" && v.value == executable);
} else if xpath == "Package/Applications/Application/VisualElements" && matched {
if let Some(value) = attributes
.iter()
.find(|v| {
["Square44x44Logo", "Square30x30Logo", "SmallLogo"]
.contains(&v.name.local_name.as_str())
})
.map(|v| v.value.clone())
{
logo_value = Some(value);
break;
}
}
depth += 1;
}
Ok(XmlEvent::EndElement { .. }) => {
if paths.len() == depth {
paths.pop();
}
depth -= 1;
}
Err(_) => {
break;
}
_ => {}
}
}
let logo_path = module_dir.join(logo_value?);
let extension = format!(".{}", logo_path.extension()?.to_string_lossy());
let logo_path = logo_path.display().to_string();
let prefix = &logo_path[0..(logo_path.len() - extension.len())];
for size in [
"targetsize-256",
"targetsize-128",
"targetsize-72",
"targetsize-36",
"scale-200",
"scale-100",
] {
let logo_path = PathBuf::from(format!("{prefix}.{size}{extension}"));
if logo_path.exists() {
return Some(logo_path);
}
}
None
}

pub fn load_image_as_hicon<T: AsRef<Path>>(image_path: T) -> Option<HICON> {
let mut logo_file = File::open(image_path.as_ref()).ok()?;
let mut buffer = vec![];
logo_file.read_to_end(&mut buffer).ok()?;
unsafe { CreateIconFromResourceEx(&buffer, TRUE, 0x30000, 100, 100, LR_DEFAULTCOLOR) }.ok()
}

fn fallback_icon() -> HICON {
unsafe { LoadIconW(None, IDI_APPLICATION) }.unwrap_or_default()
}

pub fn get_window_icon(hwnd: HWND) -> Option<HICON> {
let ret = unsafe { SendMessageW(hwnd, WM_GETICON, WPARAM(ICON_BIG as _), None) };
if ret.0 != 0 {
return Some(HICON(ret.0 as _));
}
#[cfg(target_arch = "x86")]
let ret = unsafe { windows::Win32::UI::WindowsAndMessaging::GetClassLongW(hwnd, GCL_HICON) };
#[cfg(not(target_arch = "x86"))]
let ret = unsafe { windows::Win32::UI::WindowsAndMessaging::GetClassLongPtrW(hwnd, GCL_HICON) };
if ret != 0 {
return unsafe { CopyIcon(HICON(ret as _)) }.ok();
}
None
}

fn get_exe_icon(module_path: &str) -> Option<HICON> {
unsafe {
let r: ::windows::core::Result<IImageList> = SHGetImageList(0x04);
match r {
::windows::core::Result::Ok(list) => {
if let Some(icon) = get_shfileinfo(module_path) {
let r = list.GetIcon(icon.iIcon, 1u32);
match r {
Ok(v) => Some(v),
Err(_) => None,
}
} else {
None
}
}
Err(_) => None,
}
}
}

fn get_shfileinfo(module_path: &str) -> Option<SHFILEINFOW> {
unsafe {
let mut p_path: Vec<u16> = module_path
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let mut file_info = SHFILEINFOW::default();
let file_info_size = mem::size_of_val(&file_info) as u32;
for _ in 0..3 {
// sporadically this method returns 0
let fff: usize = SHGetFileInfoW(
PCWSTR::from_raw(p_path.as_mut_ptr()),
FILE_ATTRIBUTE_NORMAL,
Some(&mut file_info),
file_info_size,
SHGFI_SYSICONINDEX,
);
if fff != 0 {
return Some(file_info);
} else {
let millis = time::Duration::from_millis(30);
std::thread::sleep(millis);
}
}
None
}
}
4 changes: 2 additions & 2 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
mod admin;
mod app_icon;
mod check_error;
mod handle_wrapper;
mod regedit;
mod scheduled_task;
mod single_instance;
mod window;
mod windows_icon;
mod windows_theme;
mod windows_version;

pub use admin::*;
pub use app_icon::*;
pub use check_error::*;
pub use handle_wrapper::*;
pub use regedit::*;
pub use scheduled_task::*;
pub use single_instance::*;
pub use window::*;
pub use windows_icon::get_module_icon_ex;
pub use windows_theme::*;
pub use windows_version::*;

Expand Down
Loading