use core::ffi::c_void;
use core::ptr::{self, NonNull};
use std::rc::Rc;
use super::ffi::{self, Handle};
pub(super) fn class_icon(source: Option<&IconSource>, metric: i32) -> Handle {
let side = unsafe { ffi::GetSystemMetrics(metric) };
source
.and_then(|source| source.load(side))
.map_or(ptr::null_mut(), OwnedIcon::leak)
}
#[derive(Debug)]
pub(super) struct IconSource {
module: Handle,
group: ResourceName,
}
impl IconSource {
pub(super) unsafe fn of(module: Handle) -> Option<Self> {
first_icon_group(module).map(|group| Self { module, group })
}
fn load(&self, side: i32) -> Option<OwnedIcon> {
let icon = unsafe {
ffi::LoadImageW(
self.module,
self.group.as_ptr(),
ffi::value::IMAGE_ICON,
side,
side,
0,
)
};
let icon = NonNull::new(icon).map(OwnedIcon);
if icon.is_none() {
let error = unsafe { ffi::GetLastError() };
crcbl_core::log::debug!(
"the executable's icon would not load at {side}px (Win32 error {error}); its \
windows keep the icon they have"
);
}
icon
}
}
#[derive(Debug)]
struct OwnedIcon(NonNull<c_void>);
impl OwnedIcon {
fn raw(&self) -> Handle {
self.0.as_ptr()
}
fn leak(self) -> Handle {
let icon = self.raw();
core::mem::forget(self);
icon
}
}
impl Drop for OwnedIcon {
fn drop(&mut self) {
if unsafe { ffi::DestroyIcon(self.raw()) } == 0 {
let error = unsafe { ffi::GetLastError() };
crcbl_core::log::warn!("DestroyIcon failed with Win32 error {error}");
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct IconSides {
big: i32,
small: i32,
}
impl IconSides {
fn at(dpi: u32) -> Self {
let side = |metric| unsafe { ffi::GetSystemMetricsForDpi(metric, dpi) };
Self {
big: side(ffi::value::SM_CX_ICON),
small: side(ffi::value::SM_CX_SM_ICON),
}
}
}
#[derive(Debug)]
pub(super) struct Icons {
source: Rc<IconSource>,
sides: Option<IconSides>,
big: Option<OwnedIcon>,
small: Option<OwnedIcon>,
}
impl Icons {
pub(super) fn new(hwnd: Handle, source: Rc<IconSource>, dpi: u32) -> Self {
let mut icons = Self {
source,
sides: None,
big: None,
small: None,
};
icons.set_dpi(hwnd, dpi);
icons
}
pub(super) fn set_dpi(&mut self, hwnd: Handle, dpi: u32) {
let sides = IconSides::at(dpi);
if !needs_load(self.sides, sides) {
return;
}
self.sides = Some(sides);
for (slot, side, held) in [
(ffi::value::ICON_BIG, sides.big, &mut self.big),
(ffi::value::ICON_SMALL, sides.small, &mut self.small),
] {
let Some(icon) = self.source.load(side) else {
continue;
};
unsafe {
ffi::SendMessageW(hwnd, ffi::msg::SET_ICON, slot, icon.raw() as ffi::Lparam);
}
*held = Some(icon);
}
}
}
fn needs_load(loaded: Option<IconSides>, wanted: IconSides) -> bool {
loaded != Some(wanted)
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ResourceName {
Id(u16),
Named(Vec<u16>),
}
impl ResourceName {
fn as_ptr(&self) -> *const u16 {
match self {
Self::Id(id) => usize::from(*id) as *const u16,
Self::Named(name) => name.as_ptr(),
}
}
}
fn first_icon_group(module: Handle) -> Option<ResourceName> {
unsafe extern "system" fn take_first(
_module: Handle,
_kind: *const u16,
name: *mut u16,
param: ffi::Lparam,
) -> ffi::Bool32 {
let raw = name as usize;
let name = if raw >> 16 == 0 {
ResourceName::Id(raw as u16)
} else {
let mut len = 0;
while unsafe { *name.add(len) } != 0 {
len += 1;
}
ResourceName::Named(unsafe { core::slice::from_raw_parts(name, len + 1) }.to_vec())
};
unsafe { *(param as *mut Option<ResourceName>) = Some(name) };
0
}
let mut found: Option<ResourceName> = None;
unsafe {
ffi::EnumResourceNamesW(
module,
ffi::value::RT_GROUP_ICON as *const u16,
take_first,
core::ptr::from_mut(&mut found) as ffi::Lparam,
);
}
found
}
#[cfg(test)]
pub(super) mod tests {
use super::*;
use windows::Win32::Graphics::Gdi::{BITMAP, DeleteObject, GetObjectW};
use windows::Win32::UI::WindowsAndMessaging::{GetIconInfo, HICON, ICONINFO};
pub(in super::super) fn shell32() -> Handle {
let name = ffi::wide("shell32.dll").expect("a literal");
let module = unsafe {
ffi::LoadLibraryExW(
name.as_ptr(),
ptr::null_mut(),
ffi::value::LOAD_AS_RESOURCES,
)
};
assert!(!module.is_null(), "shell32.dll maps as a resource file");
module
}
pub(in super::super) fn pixel_size(icon: Handle) -> Option<(i32, i32)> {
let mut info = ICONINFO::default();
unsafe { GetIconInfo(HICON(icon), &raw mut info) }.ok()?;
let mut bitmap = BITMAP::default();
let read = unsafe {
let read = GetObjectW(
info.hbmColor.into(),
size_of::<BITMAP>() as i32,
Some((&raw mut bitmap).cast()),
);
assert!(DeleteObject(info.hbmColor.into()).as_bool());
assert!(DeleteObject(info.hbmMask.into()).as_bool());
read
};
assert_ne!(read, 0, "a colour icon's bitmap reads back");
Some((bitmap.bmWidth, bitmap.bmHeight))
}
#[test]
fn an_executable_without_an_icon_leaves_the_class_icon_null() {
let exe = unsafe { ffi::GetModuleHandleW(ptr::null()) };
assert_eq!(first_icon_group(exe), None);
assert!(unsafe { IconSource::of(exe) }.is_none());
assert!(class_icon(None, ffi::value::SM_CX_ICON).is_null());
}
#[test]
fn a_module_with_icons_gives_its_first_group_at_both_sizes() {
let module = shell32();
assert_eq!(first_icon_group(module), Some(ResourceName::Id(1)));
let source = unsafe { IconSource::of(module) }.expect("shell32 has icons");
for metric in [ffi::value::SM_CX_ICON, ffi::value::SM_CX_SM_ICON] {
let icon = class_icon(Some(&source), metric);
assert!(!icon.is_null(), "no icon at metric {metric}");
unsafe { ffi::DestroyIcon(icon) };
}
drop(source);
unsafe { ffi::FreeLibrary(module) };
}
#[test]
fn icon_sides_scale_with_the_window_dpi() {
for (dpi, big, small) in [(96, 32, 16), (144, 48, 24), (192, 64, 32)] {
assert_eq!(IconSides::at(dpi), IconSides { big, small }, "at {dpi} DPI");
}
}
#[test]
fn icons_load_at_the_pixel_size_of_each_dpi() {
let module = shell32();
let source = unsafe { IconSource::of(module) }.expect("shell32 has icons");
for dpi in [96, 144, 192] {
let sides = IconSides::at(dpi);
for side in [sides.big, sides.small] {
let icon = source.load(side).expect("shell32's icon loads");
assert_eq!(pixel_size(icon.raw()), Some((side, side)), "at {dpi} DPI");
}
}
drop(source);
unsafe { ffi::FreeLibrary(module) };
}
#[test]
fn an_owned_icon_is_destroyed_when_dropped() {
let module = shell32();
let source = unsafe { IconSource::of(module) }.expect("shell32 has icons");
let icon = source.load(32).expect("shell32's icon loads");
let raw = icon.raw();
assert!(pixel_size(raw).is_some(), "live before the drop");
drop(icon);
assert_eq!(pixel_size(raw), None, "destroyed by the drop");
drop(source);
unsafe { ffi::FreeLibrary(module) };
}
#[test]
fn icons_reload_only_when_their_sides_change() {
let at_96 = IconSides { big: 32, small: 16 };
let at_144 = IconSides { big: 48, small: 24 };
assert!(needs_load(None, at_96));
assert!(!needs_load(Some(at_96), at_96));
assert!(needs_load(Some(at_96), at_144));
assert!(needs_load(Some(at_96), IconSides { big: 32, small: 20 }));
}
}