rustydialogs 0.4.2

Provides a simple and cross-platform way to display native dialog boxes.
Documentation
use std::sync::OnceLock;

use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Shell::{
	Shell_NotifyIconW, ExtractIconExW, NIF_ICON, NIF_INFO, NIF_MESSAGE, NIF_TIP, NIIF_ERROR, NIIF_INFO,
	NIIF_WARNING, NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW,
	NOTIFY_ICON_INFOTIP_FLAGS,
};
use windows::Win32::UI::WindowsAndMessaging::{
	CreateWindowExW, DestroyIcon, DestroyWindow, HICON, IDI_APPLICATION, LoadIconW,
	WINDOW_EX_STYLE, WINDOW_STYLE,
};
use windows::core::{w, Error, PCWSTR};

use super::*;

const CLASS_NAME: PCWSTR = w!("Static");
const TRAY_ICON_ID: u32 = 1;

struct TrayState {
	hwnd: HWND,
	icon: Icon,
}

unsafe impl Send for TrayState {}
unsafe impl Sync for TrayState {}

fn init(app_id: &str) -> Option<&TrayState> {
	static TRAY_STATE: OnceLock<Option<TrayState>> = OnceLock::new();

	TRAY_STATE.get_or_init(|| {
		match TrayState::create(app_id) {
			Ok(state) => Some(state),
			Err(error) => {
				eprintln!("rustydialogs: failed to initialize tray notifications: {error}");
				None
			},
		}
	}).as_ref()
}

pub fn setup(app_id: &str) -> bool {
	init(app_id).is_some()
}

pub fn notify(p: &Notification<'_>) {
	let Some(state) = init(p.app_id) else {
		return;
	};

	let mut data = state.base_data();
	data.uFlags = NIF_INFO;
	copy_wide_trunc(&mut data.szInfoTitle, p.title);
	copy_wide_trunc(&mut data.szInfo, p.message);
	data.dwInfoFlags = icon_to_flags(p.icon);
	data.Anonymous.uTimeout = timeout_hint(p.timeout);

	unsafe {
		let _ = Shell_NotifyIconW(NIM_MODIFY, &data);
	}
}

impl TrayState {
	fn create(app_id: &str) -> windows::core::Result<Self> {
		if app_id.is_empty() {
			return Err(Error::new(windows::core::HRESULT(0x80070057u32 as i32), "Application identifier cannot be empty"));
		}

		let hwnd = unsafe {
			CreateWindowExW(
				WINDOW_EX_STYLE::default(),
				CLASS_NAME, w!(""),
				WINDOW_STYLE::default(),
				0, 0, 0, 0,
				None, None, None, None,
			)
		}?;

		let icon = Icon::load();

		let mut data = NOTIFYICONDATAW::default();
		data.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
		data.hWnd = hwnd;
		data.uID = TRAY_ICON_ID;
		data.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
		data.uCallbackMessage = 0;
		data.hIcon = icon.hicon;
		copy_wide_trunc(&mut data.szTip, app_id);

		unsafe {
			if !Shell_NotifyIconW(NIM_ADD, &data).as_bool() {
				let error = windows::core::Error::from_thread();
				let _ = DestroyWindow(hwnd);
				return Err(error);
			}
		}

		Ok(Self { hwnd, icon })
	}

	fn base_data(&self) -> NOTIFYICONDATAW {
		let mut data = NOTIFYICONDATAW::default();
		data.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
		data.hWnd = self.hwnd;
		data.uID = TRAY_ICON_ID;
		data.hIcon = self.icon.hicon;
		data
	}
}

impl Drop for TrayState {
	fn drop(&mut self) {
		let data = self.base_data();
		unsafe {
			let _ = Shell_NotifyIconW(NIM_DELETE, &data);
			if !self.hwnd.is_invalid() {
				let _ = DestroyWindow(self.hwnd);
			}
		}
	}
}

fn timeout_hint(timeout_ms: i32) -> u32 {
	if timeout_ms <= 0 {
		0
	}
	else {
		timeout_ms.clamp(1_000, 30_000) as u32
	}
}

fn icon_to_flags(icon: MessageIcon) -> NOTIFY_ICON_INFOTIP_FLAGS {
	match icon {
		MessageIcon::Info => NIIF_INFO,
		MessageIcon::Warning => NIIF_WARNING,
		MessageIcon::Error => NIIF_ERROR,
		MessageIcon::Question => NIIF_INFO,
	}
}

fn copy_wide_trunc(dest: &mut [u16], value: &str) {
	if dest.is_empty() {
		return;
	}

	let mut i = 0usize;
	for unit in value.encode_utf16() {
		if i + 1 >= dest.len() {
			break;
		}
		dest[i] = unit;
		i += 1;
	}
	dest[i] = 0;
}

struct Icon {
	hicon: HICON,
	owned: bool,
}
impl Default for Icon {
	fn default() -> Icon {
		let hicon = unsafe { LoadIconW(None, IDI_APPLICATION).unwrap_or_default() };
		Icon { hicon, owned: false }
	}
}
impl Icon {
	fn load() -> Icon {
		load_exe_icon()
			.map(|icon| Icon { hicon: icon, owned: true })
			.unwrap_or_default()
	}
}
impl Drop for Icon {
	fn drop(&mut self) {
		if self.owned && self.hicon != HICON::default() {
			let _ = unsafe { DestroyIcon(self.hicon) };
		}
	}
}

fn load_exe_icon() -> Option<HICON> {
	let exe = std::env::current_exe().ok()?;
	let exe_wide = utf16cs(&exe.to_string_lossy());
	let mut large = [HICON::default(); 1];
	let mut small = [HICON::default(); 1];

	let count = unsafe {
		ExtractIconExW(PCWSTR(exe_wide.as_ptr()), 0, Some(large.as_mut_ptr()), Some(small.as_mut_ptr()), 1)
	};

	if count == 0 {
		return None;
	}

	let [small_icon] = small;
	let [large_icon] = large;

	if small_icon != HICON::default() {
		if large_icon != HICON::default() {
			unsafe {
				let _ = DestroyIcon(large_icon);
			}
		}
		Some(small_icon)
	}
	else if large_icon != HICON::default() {
		Some(large_icon)
	}
	else {
		None
	}
}