use crate::event::{MouseButton, UiEvent};
#[cfg(target_os = "windows")]
use crate::backend::{BackendError, ServiceResult};
#[cfg(target_os = "windows")]
use crate::primitives::context_menu::{ContextMenu, ContextMenuItem};
#[cfg(target_os = "windows")]
use crate::primitives::image::ImageSource;
#[cfg(target_os = "windows")]
use crate::types::WidgetId;
#[cfg(target_os = "windows")]
use windows::core::{IUnknown, PCWSTR};
#[cfg(target_os = "windows")]
use windows::Win32::Foundation::{HWND, POINT, TRUE};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Gdi::{
CreateBitmap, CreateCompatibleDC, CreateDIBSection, DeleteDC, DeleteObject, BITMAPINFO,
BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS,
};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Imaging::{
CLSID_WICImagingFactory, GUID_WICPixelFormat32bppPBGRA, IWICImagingFactory,
WICBitmapDitherTypeNone, WICBitmapPaletteTypeCustom, WICDecodeMetadataCacheOnLoad,
};
#[cfg(target_os = "windows")]
use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER};
#[cfg(target_os = "windows")]
use windows::Win32::UI::Shell::{
SHCreateMemStream, Shell_NotifyIconW, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_MODIFY,
NOTIFYICONDATAW,
};
#[cfg(target_os = "windows")]
use windows::Win32::UI::WindowsAndMessaging::{
AppendMenuW, CreateIconIndirect, CreatePopupMenu, DestroyIcon, DestroyMenu, GetCursorPos,
SetForegroundWindow, TrackPopupMenuEx, HICON, HMENU, ICONINFO, MF_CHECKED, MF_GRAYED, MF_POPUP,
MF_SEPARATOR, MF_STRING, TPM_NONOTIFY, TPM_RETURNCMD, TPM_RIGHTBUTTON,
};
#[cfg(target_os = "windows")]
use super::backend::WM_QUADRAUI_TRAY_CALLBACK;
#[cfg(target_os = "windows")]
use super::services::{copy_wide_truncated, ensure_com_initialized};
#[cfg(target_os = "windows")]
const TRAY_ICON_UID: u32 = 0;
#[cfg(target_os = "windows")]
#[derive(Default)]
pub(crate) struct WinTrayState {
added: bool,
icon: Option<HICON>,
menu: Option<ContextMenu>,
}
#[cfg(target_os = "windows")]
pub(crate) fn set_icon(
state: &mut WinTrayState,
hwnd: HWND,
icon: ImageSource,
) -> ServiceResult<()> {
let hicon = decode_hicon(&icon).ok_or(BackendError::PlatformFailure {
context: "WIC/GDI failed to decode the tray icon source into a HICON".to_string(),
})?;
let data = NOTIFYICONDATAW {
cbSize: std::mem::size_of::<NOTIFYICONDATAW>() as u32,
hWnd: hwnd,
uID: TRAY_ICON_UID,
uFlags: NIF_ICON | NIF_MESSAGE,
uCallbackMessage: WM_QUADRAUI_TRAY_CALLBACK,
hIcon: hicon,
..Default::default()
};
let message = if state.added { NIM_MODIFY } else { NIM_ADD };
let ok = unsafe { Shell_NotifyIconW(message, &data) }.as_bool();
if !ok {
unsafe {
let _ = DestroyIcon(hicon);
}
return Err(BackendError::PlatformFailure {
context: "Shell_NotifyIconW(NIM_ADD/NIM_MODIFY, NIF_ICON) failed".to_string(),
});
}
state.added = true;
if let Some(old) = state.icon.replace(hicon) {
unsafe {
let _ = DestroyIcon(old);
}
}
Ok(())
}
#[cfg(target_os = "windows")]
pub(crate) fn set_tooltip(
state: &mut WinTrayState,
hwnd: HWND,
tooltip: &str,
) -> ServiceResult<()> {
let mut data = NOTIFYICONDATAW {
cbSize: std::mem::size_of::<NOTIFYICONDATAW>() as u32,
hWnd: hwnd,
uID: TRAY_ICON_UID,
uFlags: NIF_TIP | NIF_MESSAGE,
uCallbackMessage: WM_QUADRAUI_TRAY_CALLBACK,
..Default::default()
};
copy_wide_truncated(&mut data.szTip, tooltip);
let message = if state.added { NIM_MODIFY } else { NIM_ADD };
let ok = unsafe { Shell_NotifyIconW(message, &data) }.as_bool();
if !ok {
return Err(BackendError::PlatformFailure {
context: "Shell_NotifyIconW(NIM_ADD/NIM_MODIFY, NIF_TIP) failed".to_string(),
});
}
state.added = true;
Ok(())
}
#[cfg(target_os = "windows")]
pub(crate) fn set_menu(
state: &mut WinTrayState,
_hwnd: HWND,
menu: &ContextMenu,
) -> ServiceResult<()> {
state.menu = if menu.items.is_empty() {
None
} else {
Some(menu.clone())
};
Ok(())
}
#[cfg(target_os = "windows")]
pub(crate) fn attached_menu(state: &WinTrayState) -> Option<ContextMenu> {
state.menu.clone()
}
#[cfg(target_os = "windows")]
pub(crate) fn track_menu(hwnd: HWND, menu: &ContextMenu) -> Option<WidgetId> {
let mut ids: Vec<WidgetId> = Vec::new();
let hmenu = unsafe { CreatePopupMenu() }.ok()?;
for item in &menu.items {
append_hmenu_item(hmenu, item, &mut ids);
}
unsafe {
let _ = SetForegroundWindow(hwnd);
}
let mut cursor = POINT::default();
let got_cursor = unsafe { GetCursorPos(&mut cursor) }.is_ok();
let flags = (TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_NONOTIFY).0;
let cmd = if got_cursor {
unsafe { TrackPopupMenuEx(hmenu, flags, cursor.x, cursor.y, hwnd, None) }.0
} else {
0
};
unsafe {
let _ = DestroyMenu(hmenu);
}
if cmd <= 0 {
return None;
}
ids.into_iter().nth(cmd as usize - 1)
}
#[cfg(target_os = "windows")]
fn append_hmenu_item(hmenu: HMENU, item: &ContextMenuItem, ids: &mut Vec<WidgetId>) {
if item.is_separator() {
unsafe {
let _ = AppendMenuW(hmenu, MF_SEPARATOR, 0, PCWSTR::null());
}
return;
}
let title: String = item.label.spans.iter().map(|s| s.text.as_str()).collect();
let wide = win_wide_nul_terminated(&title);
if let Some(nested) = item.submenu.as_ref() {
let submenu = unsafe { CreatePopupMenu() };
let Ok(submenu) = submenu else { return };
for child in nested {
append_hmenu_item(submenu, child, ids);
}
unsafe {
let _ = AppendMenuW(
hmenu,
MF_POPUP | MF_STRING,
submenu.0 as usize,
PCWSTR::from_raw(wide.as_ptr()),
);
}
return;
}
if let Some(id) = item.id {
ids.push(id);
let cmd_id = ids.len();
let mut flags = MF_STRING;
if item.disabled {
flags |= MF_GRAYED;
}
if let Some(true) = item.checked {
flags |= MF_CHECKED;
}
unsafe {
let _ = AppendMenuW(hmenu, flags, cmd_id, PCWSTR::from_raw(wide.as_ptr()));
}
}
}
#[cfg(target_os = "windows")]
use crate::desktop::wide_nul_terminated as win_wide_nul_terminated;
#[cfg(target_os = "windows")]
pub(crate) fn decode_hicon(source: &ImageSource) -> Option<HICON> {
let bytes: Vec<u8> = match source {
ImageSource::Path(path) => std::fs::read(path).ok()?,
ImageSource::Bytes(bytes) => bytes.clone(),
};
ensure_com_initialized();
let factory: IWICImagingFactory = unsafe {
CoCreateInstance(
&CLSID_WICImagingFactory,
None::<&IUnknown>,
CLSCTX_INPROC_SERVER,
)
}
.ok()?;
let stream = unsafe { SHCreateMemStream(Some(&bytes)) }?;
let decoder = unsafe {
factory.CreateDecoderFromStream(&stream, std::ptr::null(), WICDecodeMetadataCacheOnLoad)
}
.ok()?;
let frame = unsafe { decoder.GetFrame(0) }.ok()?;
let converter = unsafe { factory.CreateFormatConverter() }.ok()?;
unsafe {
converter.Initialize(
&frame,
&GUID_WICPixelFormat32bppPBGRA,
WICBitmapDitherTypeNone,
None,
0.0,
WICBitmapPaletteTypeCustom,
)
}
.ok()?;
let mut width = 0u32;
let mut height = 0u32;
unsafe { converter.GetSize(&mut width, &mut height) }.ok()?;
if width == 0 || height == 0 {
return None;
}
let stride = (width as usize) * 4;
let mut pixels = vec![0u8; stride * height as usize];
unsafe { converter.CopyPixels(std::ptr::null(), stride as u32, &mut pixels) }.ok()?;
build_hicon_from_bgra(&pixels, width, height)
}
#[cfg(target_os = "windows")]
fn build_hicon_from_bgra(pixels: &[u8], width: u32, height: u32) -> Option<HICON> {
let bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: width as i32,
biHeight: -(height as i32),
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let hdc = unsafe { CreateCompatibleDC(None) };
let mut bits: *mut core::ffi::c_void = std::ptr::null_mut();
let hbm_color =
match unsafe { CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0) } {
Ok(bitmap) => bitmap,
Err(_) => {
unsafe {
let _ = DeleteDC(hdc);
}
return None;
}
};
unsafe {
let _ = DeleteDC(hdc);
}
if bits.is_null() {
unsafe {
let _ = DeleteObject(hbm_color.into());
}
return None;
}
unsafe {
std::ptr::copy_nonoverlapping(pixels.as_ptr(), bits as *mut u8, pixels.len());
}
let mask_stride = width.div_ceil(16) * 2;
let mask_bits = vec![0u8; (mask_stride * height) as usize];
let hbm_mask = unsafe {
CreateBitmap(
width as i32,
height as i32,
1,
1,
Some(mask_bits.as_ptr().cast()),
)
};
let icon_info = ICONINFO {
fIcon: TRUE,
xHotspot: 0,
yHotspot: 0,
hbmMask: hbm_mask,
hbmColor: hbm_color,
};
let hicon = unsafe { CreateIconIndirect(&icon_info) }.ok();
unsafe {
let _ = DeleteObject(hbm_color.into());
let _ = DeleteObject(hbm_mask.into());
}
hicon
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn tray_click_button(win32_message: u32) -> Option<MouseButton> {
const WM_LBUTTONUP: u32 = 0x0202;
const WM_RBUTTONUP: u32 = 0x0205;
const WM_MBUTTONUP: u32 = 0x0208;
match win32_message {
WM_LBUTTONUP => Some(MouseButton::Left),
WM_RBUTTONUP => Some(MouseButton::Right),
WM_MBUTTONUP => Some(MouseButton::Middle),
_ => None,
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn plain_click_event(button: MouseButton) -> UiEvent {
UiEvent::TrayClicked { button }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tray_click_button_maps_the_three_up_messages() {
assert_eq!(tray_click_button(0x0202), Some(MouseButton::Left));
assert_eq!(tray_click_button(0x0205), Some(MouseButton::Right));
assert_eq!(tray_click_button(0x0208), Some(MouseButton::Middle));
}
#[test]
fn tray_click_button_ignores_down_and_move_messages() {
assert_eq!(tray_click_button(0x0201), None);
assert_eq!(tray_click_button(0x0200), None);
assert_eq!(tray_click_button(0x0203), None);
}
#[test]
fn plain_click_event_carries_the_button_through() {
assert_eq!(
plain_click_event(MouseButton::Right),
UiEvent::TrayClicked {
button: MouseButton::Right
}
);
}
}