From 09fe01052843d58ea9865ad4689a934c2ea2501f Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 25 Oct 2024 08:56:38 +0800 Subject: [PATCH] refactor: cache app icons --- src/app.rs | 23 +---- src/painter.rs | 9 +- src/utils/app_icon.rs | 195 ++++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 4 +- src/utils/window.rs | 123 +----------------------- src/utils/windows_icon.rs | 58 ------------ 6 files changed, 210 insertions(+), 202 deletions(-) create mode 100644 src/utils/app_icon.rs delete mode 100644 src/utils/windows_icon.rs diff --git a/src/app.rs b/src/app.rs index 0a7f677..d003100 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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, }; @@ -52,7 +51,7 @@ pub struct App { config: Config, switch_windows_state: SwitchWindowsState, switch_apps_state: Option, - uwp_icons: HashMap>, + cached_icons: HashMap, painter: GdiAAPainter, } @@ -84,7 +83,7 @@ impl App { modifier_released: true, }, switch_apps_state: None, - uwp_icons: Default::default(), + cached_icons: Default::default(), painter, }; @@ -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 { diff --git a/src/painter.rs b/src/painter.rs index bfef34b..26433ae 100644 --- a/src/painter.rs +++ b/src/painter.rs @@ -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}; @@ -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; } } diff --git a/src/utils/app_icon.rs b/src/utils/app_icon.rs new file mode 100644 index 0000000..59488db --- /dev/null +++ b/src/utils/app_icon.rs @@ -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, + 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 { + 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>(image_path: T) -> Option { + 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 { + 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 { + unsafe { + let r: ::windows::core::Result = 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 { + unsafe { + let mut p_path: Vec = 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 + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 23de7c7..5f8fa24 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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::*; diff --git a/src/utils/window.rs b/src/utils/window.rs index 3682cd0..1d24930 100644 --- a/src/utils/window.rs +++ b/src/utils/window.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use indexmap::IndexMap; use windows::core::PWSTR; -use windows::Win32::Foundation::{BOOL, HWND, LPARAM, MAX_PATH, POINT, RECT, TRUE, WPARAM}; +use windows::Win32::Foundation::{BOOL, HWND, LPARAM, MAX_PATH, POINT, RECT}; use windows::Win32::Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED}; use windows::Win32::Graphics::Gdi::{ GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITOR_DEFAULTTONEAREST, @@ -13,19 +13,14 @@ use windows::Win32::System::Threading::{ PROCESS_VM_READ, }; use windows::Win32::UI::WindowsAndMessaging::{ - CreateIconFromResourceEx, EnumWindows, GetCursorPos, GetForegroundWindow, GetWindow, - GetWindowLongPtrW, GetWindowPlacement, GetWindowTextW, GetWindowThreadProcessId, IsIconic, - IsWindowVisible, LoadIconW, SendMessageW, SetForegroundWindow, SetWindowPos, ShowWindow, - GCL_HICON, GWL_EXSTYLE, GWL_USERDATA, GW_OWNER, HICON, ICON_BIG, IDI_APPLICATION, - LR_DEFAULTCOLOR, SWP_NOZORDER, SW_RESTORE, WINDOWPLACEMENT, WM_GETICON, WS_EX_TOPMOST, + EnumWindows, GetCursorPos, GetForegroundWindow, GetWindow, GetWindowLongPtrW, + GetWindowPlacement, GetWindowTextW, GetWindowThreadProcessId, IsIconic, IsWindowVisible, + SetForegroundWindow, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_USERDATA, GW_OWNER, + SWP_NOZORDER, SW_RESTORE, WINDOWPLACEMENT, WS_EX_TOPMOST, }; -use std::fs::File; -use std::io::{BufReader, Read}; use std::path::PathBuf; use std::{ffi::c_void, mem::size_of}; -use xml::reader::XmlEvent; -use xml::EventReader; pub fn is_iconic_window(hwnd: HWND) -> bool { unsafe { IsIconic(hwnd) }.as_bool() @@ -172,19 +167,6 @@ pub fn get_owner_window(hwnd: HWND) -> HWND { unsafe { GetWindow(hwnd, GW_OWNER) }.unwrap_or_default() } -pub fn get_module_icon(hwnd: HWND) -> Option { - let ret = unsafe { SendMessageW(hwnd, WM_GETICON, WPARAM(ICON_BIG as _), None) }.0; - if ret != 0 { - return Some(HICON(ret as _)); - } - - let ret = get_class_icon(hwnd); - if ret != 0 { - return Some(HICON(ret as _)); - } - None -} - #[cfg(target_arch = "x86")] pub fn get_window_user_data(hwnd: HWND) -> i32 { unsafe { windows::Win32::UI::WindowsAndMessaging::GetWindowLongW(hwnd, GWL_USERDATA) } @@ -204,15 +186,6 @@ pub fn set_window_user_data(hwnd: HWND, ptr: isize) -> isize { unsafe { windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW(hwnd, GWL_USERDATA, ptr) } } -#[cfg(target_arch = "x86")] -pub fn get_class_icon(hwnd: HWND) -> u32 { - unsafe { windows::Win32::UI::WindowsAndMessaging::GetClassLongW(hwnd, GCL_HICON) } -} -#[cfg(not(target_arch = "x86"))] -pub fn get_class_icon(hwnd: HWND) -> usize { - unsafe { windows::Win32::UI::WindowsAndMessaging::GetClassLongPtrW(hwnd, GCL_HICON) } -} - /// Lists available windows /// /// Duo to the limitation of `OpenProcess`, this function will not list `Task Manager` @@ -277,89 +250,3 @@ extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL { windows.push(hwnd); BOOL(1) } - -pub fn get_uwp_icon_data(module_path: &str) -> Option> { - let logo_path = get_appx_logo_path(module_path)?; - let mut logo_file = File::open(logo_path).ok()?; - let mut buffer = vec![]; - logo_file.read_to_end(&mut buffer).ok()?; - Some(buffer) -} - -fn get_appx_logo_path(module_path: &str) -> Option { - 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 create_hicon_from_resource(data: &[u8]) -> Option { - unsafe { CreateIconFromResourceEx(data, TRUE, 0x30000, 100, 100, LR_DEFAULTCOLOR) } - .ok() - .or_else(|| unsafe { LoadIconW(None, IDI_APPLICATION) }.ok()) -} diff --git a/src/utils/windows_icon.rs b/src/utils/windows_icon.rs deleted file mode 100644 index df56458..0000000 --- a/src/utils/windows_icon.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::{mem, time}; - -use windows::{ - core::PCWSTR, - Win32::{ - Storage::FileSystem::FILE_ATTRIBUTE_NORMAL, - UI::{ - Controls::IImageList, - Shell::{SHGetFileInfoW, SHGetImageList, SHFILEINFOW, SHGFI_SYSICONINDEX}, - WindowsAndMessaging::HICON, - }, - }, -}; - -fn get_module_icon_ex0(ext: &str) -> Option { - unsafe { - let mut p_path: Vec = ext.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 - } -} - -pub fn get_module_icon_ex(ext: &str) -> Option { - unsafe { - let r: ::windows::core::Result = SHGetImageList(0x04); - match r { - ::windows::core::Result::Ok(list) => { - if let Some(icon) = get_module_icon_ex0(ext) { - let r = list.GetIcon(icon.iIcon, 1u32); - match r { - Ok(v) => Some(v), - Err(_) => None, - } - } else { - None - } - } - Err(_) => None, - } - } -}