use winit::window::{Icon, WindowAttributes};
#[derive(Clone, Default)]
pub(crate) struct AppIcon {
window: Option<Icon>,
#[cfg(target_os = "windows")]
taskbar: Option<Icon>,
#[cfg(target_os = "windows")]
resource: Option<ResourceIcons>,
}
#[cfg(target_os = "windows")]
#[derive(Clone, Copy)]
struct ResourceIcons {
small: windows_sys::Win32::UI::WindowsAndMessaging::HICON,
big: windows_sys::Win32::UI::WindowsAndMessaging::HICON,
}
pub(crate) fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Icon, String> {
if width == 0 || height == 0 {
return Err(format!(
"an icon is at least one pixel, not {width}x{height}"
));
}
Icon::from_rgba(rgba, width, height).map_err(|e| format!("icon: {e}"))
}
impl AppIcon {
pub(crate) fn new(rgba: Option<Icon>, resource: Option<u16>) -> Self {
#[cfg(target_os = "windows")]
if let Some(id) = resource {
match from_resource(id) {
Ok(icons) => {
return AppIcon {
window: None,
taskbar: None,
resource: Some(icons),
};
}
Err(why) => eprintln!(
"kui: icon resource {id} is not in the executable ({why}); {}",
if rgba.is_some() {
"the icon's pixels are used"
} else {
"the windows keep the default icon"
}
),
}
}
#[cfg(not(target_os = "windows"))]
let _ = resource;
AppIcon {
#[cfg(target_os = "windows")]
taskbar: rgba.clone(),
#[cfg(target_os = "windows")]
resource: None,
window: rgba,
}
}
pub(crate) fn set_on(&self, window: &winit::window::Window) {
#[cfg(target_os = "windows")]
if let Some(icons) = self.resource {
use windows_sys::Win32::Foundation::HWND;
use windows_sys::Win32::UI::WindowsAndMessaging::{
ICON_BIG, ICON_SMALL, SendMessageW, WM_SETICON,
};
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
let Some(RawWindowHandle::Win32(h)) = window.window_handle().ok().map(|h| h.as_raw())
else {
return;
};
let hwnd = h.hwnd.get() as HWND;
unsafe {
SendMessageW(hwnd, WM_SETICON, ICON_SMALL as usize, icons.small);
SendMessageW(hwnd, WM_SETICON, ICON_BIG as usize, icons.big);
}
}
#[cfg(not(target_os = "windows"))]
let _ = window;
}
pub(crate) fn apply(&self, attrs: WindowAttributes) -> WindowAttributes {
#[allow(unused_mut)]
let mut attrs = attrs.with_window_icon(self.window.clone());
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowAttributesExtWindows;
attrs = attrs.with_taskbar_icon(self.taskbar.clone());
}
attrs
}
}
#[cfg(target_os = "windows")]
fn from_resource(id: u16) -> Result<ResourceIcons, std::io::Error> {
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetSystemMetrics, IMAGE_ICON, LR_DEFAULTCOLOR, LoadImageW, SM_CXICON, SM_CXSMICON,
SM_CYICON, SM_CYSMICON,
};
unsafe {
let module = GetModuleHandleW(std::ptr::null());
let load = |w: i32, h: i32| {
let icon = LoadImageW(
module,
id as usize as *const u16,
IMAGE_ICON,
w,
h,
LR_DEFAULTCOLOR,
);
if icon == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(icon)
}
};
Ok(ResourceIcons {
small: load(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON))?,
big: load(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON))?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pixels_that_are_not_the_size_are_refused_with_the_reason() {
assert!(from_rgba(vec![0; 2 * 2 * 4], 2, 2).is_ok());
let short = from_rgba(vec![0; 2 * 2 * 4 - 4], 2, 2).unwrap_err();
assert!(
short.contains("2x2") || short.contains("dimensions"),
"{short}"
);
let ragged = from_rgba(vec![0; 7], 1, 1).unwrap_err();
assert!(ragged.contains("divisible by 4"), "{ragged}");
let empty = from_rgba(Vec::new(), 0, 0).unwrap_err();
assert!(empty.contains("at least one pixel"), "{empty}");
}
#[test]
fn every_window_is_created_with_the_launcher_s_icon_and_none_without() {
let attrs = || winit::window::Window::default_attributes();
assert!(AppIcon::default().apply(attrs()).window_icon.is_none());
let icon = from_rgba(vec![255; 4 * 4 * 4], 4, 4).unwrap();
#[cfg(not(target_os = "windows"))]
{
let set = AppIcon::new(Some(icon), Some(1));
assert!(set.apply(attrs()).window_icon.is_some());
assert!(
set.apply(attrs()).window_icon.is_some(),
"and again for the next window"
);
assert!(
AppIcon::new(None, Some(1))
.apply(attrs())
.window_icon
.is_none()
);
}
#[cfg(target_os = "windows")]
{
let set = AppIcon::new(Some(icon), Some(1));
assert!(set.apply(attrs()).window_icon.is_some());
assert!(set.resource.is_none());
let why = from_resource(1)
.err()
.expect("no resource in a test binary");
assert!(why.raw_os_error().is_some(), "{why}");
}
}
}