diff --git a/src/utils/app_icon.rs b/src/utils/app_icon.rs index 68ac226..eb709e4 100644 --- a/src/utils/app_icon.rs +++ b/src/utils/app_icon.rs @@ -12,10 +12,11 @@ use indexmap::IndexMap; use windows::{ core::PCWSTR, Win32::{ - Foundation::{COLORREF, HWND, WPARAM}, + Foundation::{HWND, LPARAM, WPARAM}, Graphics::Gdi::{ - CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetObjectW, GetPixel, ReleaseDC, - SelectObject, BITMAP, HGDIOBJ, + CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits, GetObjectW, ReleaseDC, + SelectObject, BITMAP, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, HBITMAP, HDC, + HGDIOBJ, RGBQUAD, }, Storage::FileSystem::FILE_ATTRIBUTE_NORMAL, UI::{ @@ -23,8 +24,9 @@ use windows::{ Shell::{SHGetFileInfoW, SHGetImageList, SHFILEINFOW, SHGFI_SYSICONINDEX}, WindowsAndMessaging::{ CopyIcon, CreateIconFromResourceEx, DestroyIcon, GetIconInfo, LoadIconW, - LoadImageW, SendMessageW, GCL_HICON, HICON, ICONINFO, ICON_BIG, IDI_APPLICATION, - IMAGE_ICON, LR_DEFAULTCOLOR, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_GETICON, + LoadImageW, SendMessageTimeoutW, GCLP_HICONSM, GCL_HICON, HICON, ICONINFO, + ICON_BIG, IDI_APPLICATION, IMAGE_ICON, LR_DEFAULTCOLOR, LR_DEFAULTSIZE, + LR_LOADFROMFILE, SMTO_ABORTIFHUNG, WM_GETICON, }, }, }, @@ -187,9 +189,20 @@ fn fallback_icon() -> HICON { } pub fn get_window_icon(hwnd: HWND) -> Option { - let ret = unsafe { SendMessageW(hwnd, WM_GETICON, Some(WPARAM(ICON_BIG as _)), None) }; - if ret.0 != 0 { - return unsafe { CopyIcon(HICON(ret.0 as _)) }.ok(); + let mut result: usize = 0; + let ret = unsafe { + SendMessageTimeoutW( + hwnd, + WM_GETICON, + WPARAM(ICON_BIG as _), + LPARAM(0), + SMTO_ABORTIFHUNG, + 250, + Some(&mut result), + ) + }; + if ret.0 != 0 && result != 0 { + return unsafe { CopyIcon(HICON(result as _)) }.ok(); } #[cfg(target_arch = "x86")] let ret = unsafe { windows::Win32::UI::WindowsAndMessaging::GetClassLongW(hwnd, GCL_HICON) }; @@ -198,6 +211,14 @@ pub fn get_window_icon(hwnd: HWND) -> Option { if ret != 0 { return unsafe { CopyIcon(HICON(ret as _)) }.ok(); } + #[cfg(target_arch = "x86")] + let ret = unsafe { windows::Win32::UI::WindowsAndMessaging::GetClassLongW(hwnd, GCLP_HICONSM) }; + #[cfg(not(target_arch = "x86"))] + let ret = + unsafe { windows::Win32::UI::WindowsAndMessaging::GetClassLongPtrW(hwnd, GCLP_HICONSM) }; + if ret != 0 { + return unsafe { CopyIcon(HICON(ret as _)) }.ok(); + } None } @@ -249,33 +270,24 @@ fn get_pwa_icon_from_lnk(module_path: &str) -> Option { } } +const SHIL_JUMBO: i32 = 0x04; +const SHIL_EXTRALARGE: i32 = 0x02; +const SHIL_LARGE: i32 = 0x00; + 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(info) = get_shfileinfo(module_path) { - let r = list.GetIcon(info.iIcon, 1u32); - match r { - Ok(hicon) => { - let size = get_icon_size(hicon); - match size { - Some((x, y)) if x >= 64 && y >= 64 => Some(hicon), - _ => { - let _ = DestroyIcon(hicon); - None - } - } - } - Err(_) => None, - } - } else { - None - } + let info = get_shfileinfo(module_path)?; + for shil in [SHIL_JUMBO, SHIL_EXTRALARGE, SHIL_LARGE] { + unsafe { + let list = SHGetImageList::(shil).ok()?; + let hicon = list.GetIcon(info.iIcon, 1u32).ok()?; + if is_valid_icon(hicon) { + return Some(hicon); + } else { + let _ = DestroyIcon(hicon); } - Err(_) => None, } } + None } fn get_shfileinfo(module_path: &str) -> Option { @@ -285,8 +297,10 @@ fn get_shfileinfo(module_path: &str) -> Option { .chain(std::iter::once(0)) .collect(); let mut file_info = SHFILEINFOW::default(); + // Retry up to 3 times because SHGetFileInfoW can transiently fail + // (e.g. shell not fully initialized, file system contention). A simple + // short sleep + retry handles these spurious failures robustly. for _ in 0..3 { - // sporadically this method returns 0 let fff: usize = SHGetFileInfoW( PCWSTR::from_raw(p_path.as_mut_ptr()), FILE_ATTRIBUTE_NORMAL, @@ -305,57 +319,293 @@ fn get_shfileinfo(module_path: &str) -> Option { } } -fn get_icon_size(hicon: HICON) -> Option<(i32, i32)> { +/// Returns `false` for icons whose content is squeezed into the top-left corner +/// while the rest is stretched/padded garbage — e.g. the icon of `hh.exe` +/// (Windows's help viewer, reused by AutoHotKey, etc. to display their *.chm help). +/// These look ugly in the switcher and are better replaced with a fallback icon. +pub fn is_valid_icon(hicon: HICON) -> bool { + let Some(bounds) = get_icon_bounds(hicon) else { + return false; + }; + !is_topleft_icon(&bounds) +} + +struct IconBounds { + pub canvas_width: i32, + pub canvas_height: i32, + pub min_x: i32, + pub min_y: i32, + pub max_x: i32, + pub max_y: i32, +} + +fn is_topleft_icon(bounds: &IconBounds) -> bool { + let bbox_width = bounds.max_x - bounds.min_x + 1; + let bbox_height = bounds.max_y - bounds.min_y + 1; + + let bbox_area = bbox_width * bbox_height; + let canvas_area = bounds.canvas_width * bounds.canvas_height; + + let bbox_ratio = bbox_area as f32 / canvas_area as f32; + + let center_x = (bounds.min_x + bounds.max_x) as f32 / 2.0 / bounds.canvas_width as f32; + let center_y = (bounds.min_y + bounds.max_y) as f32 / 2.0 / bounds.canvas_height as f32; + + let small_content = bbox_ratio < 0.25; + let top_left = center_x < 0.30 && center_y < 0.30; + + small_content && top_left +} + +struct HdcGuard(HDC); +impl Drop for HdcGuard { + fn drop(&mut self) { + unsafe { + let _ = DeleteDC(self.0); + } + } +} + +struct ScreenDcGuard(HDC); +impl Drop for ScreenDcGuard { + fn drop(&mut self) { + unsafe { + ReleaseDC(None, self.0); + } + } +} + +struct BitmapGuard(HBITMAP); +impl Drop for BitmapGuard { + fn drop(&mut self) { + unsafe { + let _ = DeleteObject(HGDIOBJ(self.0 .0 as _)); + } + } +} + +fn get_icon_bounds(hicon: HICON) -> Option { unsafe { let mut icon_info: ICONINFO = std::mem::zeroed(); if GetIconInfo(hicon, &mut icon_info).is_err() { return None; } + let _color_guard = BitmapGuard(icon_info.hbmColor); + let _mask_guard = BitmapGuard(icon_info.hbmMask); let mut bmp = BITMAP::default(); - if 0 == GetObjectW( + if GetObjectW( icon_info.hbmColor.into(), std::mem::size_of::() as i32, Some(&mut bmp as *mut _ as *mut _), + ) == 0 + { + return None; + } + + let width = bmp.bmWidth; + let height = bmp.bmHeight; + if width <= 0 || height <= 0 { + return None; + } + + let screen_dc = GetDC(None); + let _screen_guard = ScreenDcGuard(screen_dc); + + let mem_dc = CreateCompatibleDC(Some(screen_dc)); + let _dc_guard = HdcGuard(mem_dc); + + let old_bmp = SelectObject(mem_dc, HGDIOBJ(icon_info.hbmColor.0 as _)); + + let mut bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: std::mem::size_of::() as u32, + biWidth: width, + biHeight: -height, + biPlanes: 1, + biBitCount: 32, + biCompression: 0, + biSizeImage: 0, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [RGBQUAD { + rgbBlue: 0, + rgbGreen: 0, + rgbRed: 0, + rgbReserved: 0, + }; 1], + }; + + let buf_size = (width * height * 4) as usize; + let mut pixels: Vec = vec![0; buf_size]; + + if 0 == GetDIBits( + mem_dc, + icon_info.hbmColor, + 0, + height as u32, + Some(pixels.as_mut_ptr() as *mut _), + &mut bmi, + DIB_RGB_COLORS, ) { - let _ = DeleteObject(icon_info.hbmColor.into()); - let _ = DeleteObject(icon_info.hbmMask.into()); + SelectObject(mem_dc, old_bmp); return None; } - let (width, height) = (bmp.bmWidth, bmp.bmHeight); - let hdc = GetDC(None); - let hmemdc = CreateCompatibleDC(Some(hdc)); - let old_bitmap = SelectObject(hmemdc, HGDIOBJ(icon_info.hbmColor.0 as _)); + let mut min_x = width; + let mut min_y = height; + let mut max_x: i32 = -1; + let mut max_y: i32 = -1; + let mut has_semi_transparent = false; - let (mut min_x, mut min_y, mut max_x, mut max_y) = (width, height, 0, 0); + // Pass 1: top/bottom bounds + alpha detection in one scan + let rows = pixels.chunks_exact(width as usize * 4); - for y in 0..height { - for x in 0..width { - let pixel = GetPixel(hmemdc, x, y); - if pixel != COLORREF(0) { - if x < min_x { - min_x = x; + for (y, row) in rows.clone().enumerate() { + let mut row_has_visible = false; + for c in row.chunks_exact(4) { + let a = c[3]; + if a != 0 { + row_has_visible = true; + if a < 255 { + has_semi_transparent = true; } - if y < min_y { - min_y = y; + } + } + if row_has_visible { + min_y = y as i32; + break; + } + } + + if min_y < height { + for (y, row) in rows.clone().rev().enumerate() { + let actual_y = (height as usize - 1) - y; + let mut row_has_visible = false; + for c in row.chunks_exact(4) { + let a = c[3]; + if a != 0 { + row_has_visible = true; + if a < 255 { + has_semi_transparent = true; + } } - if x > max_x { + } + if row_has_visible { + max_y = actual_y as i32; + break; + } + } + } + + // Pass 2: left/right bounds within [min_y, max_y] + if max_y >= 0 { + let stride = width as usize * 4; + + 'left: for x in 0..width { + for y in min_y..=max_y { + if pixels[(y as usize * stride + x as usize * 4) + 3] != 0 { + min_x = x; + break 'left; + } + } + } + + 'right: for x in (0..width).rev() { + for y in min_y..=max_y { + if pixels[(y as usize * stride + x as usize * 4) + 3] != 0 { max_x = x; + break 'right; + } + } + } + } + + // Fallback to mask when no alpha channel and no visible pixels found + if !has_semi_transparent && max_x < 0 { + SelectObject(mem_dc, HGDIOBJ(icon_info.hbmMask.0 as _)); + + if 0 != GetDIBits( + mem_dc, + icon_info.hbmMask, + 0, + height as u32, + Some(pixels.as_mut_ptr() as *mut _), + &mut bmi, + DIB_RGB_COLORS, + ) { + let mask_rows = pixels.chunks_exact(width as usize * 4); + + min_y = height; + max_y = -1i32; + + for (y, row) in mask_rows.clone().enumerate() { + if row + .chunks_exact(4) + .any(|c| c[0] == 0 && c[1] == 0 && c[2] == 0) + { + min_y = y as i32; + break; } - if y > max_y { - max_y = y; + } + + if min_y < height { + for (y, row) in mask_rows.clone().rev().enumerate() { + let actual_y = (height as usize - 1) - y; + if row + .chunks_exact(4) + .any(|c| c[0] == 0 && c[1] == 0 && c[2] == 0) + { + max_y = actual_y as i32; + break; + } + } + } + + if max_y >= 0 { + let stride = width as usize * 4; + + 'mleft: for x in 0..width { + for y in min_y..=max_y { + let base = y as usize * stride + x as usize * 4; + if pixels[base] == 0 && pixels[base + 1] == 0 && pixels[base + 2] == 0 { + min_x = x; + break 'mleft; + } + } + } + + 'mright: for x in (0..width).rev() { + for y in min_y..=max_y { + let base = y as usize * stride + x as usize * 4; + if pixels[base] == 0 && pixels[base + 1] == 0 && pixels[base + 2] == 0 { + max_x = x; + break 'mright; + } + } } } } + + SelectObject(mem_dc, old_bmp); + } else { + SelectObject(mem_dc, old_bmp); } - SelectObject(hmemdc, old_bitmap); - let _ = DeleteDC(hmemdc); - ReleaseDC(None, hdc); - let _ = DeleteObject(icon_info.hbmColor.into()); - let _ = DeleteObject(icon_info.hbmMask.into()); + if max_x < 0 { + return None; + } - Some((max_x - min_x + 1, max_y - min_y + 1)) + Some(IconBounds { + canvas_width: width, + canvas_height: height, + min_x, + min_y, + max_x, + max_y, + }) } }