use super::{
CTX, Handle, Hwnd, Lparam, Point, Rect, Wparam, create_icon, fill_menu, push_event, wide,
};
use crate::platform::PlatformError;
use crate::platform::event::Event;
use crate::platform::menu::NativeTray;
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
pub(super) const WM_APP_TRAY: u32 = 0x8000 + 2;
const TRAY_UID: u32 = 1;
const NOTE_UID: u32 = 2;
const NIM_ADD: u32 = 0;
const NIM_MODIFY: u32 = 1;
const NIM_DELETE: u32 = 2;
const NIF_MESSAGE: u32 = 0x1;
const NIF_ICON: u32 = 0x2;
const NIF_TIP: u32 = 0x4;
const NIF_INFO: u32 = 0x10;
const NIIF_INFO: u32 = 0x1;
const NIN_BALLOONHIDE: u32 = 0x403;
const NIN_BALLOONTIMEOUT: u32 = 0x404;
const NIN_BALLOONUSERCLICK: u32 = 0x405;
const WM_NULL: u32 = 0;
const WM_CONTEXTMENU: u32 = 0x007B;
const WM_LBUTTONUP: u32 = 0x0202;
const WM_RBUTTONUP: u32 = 0x0205;
const TPM_RIGHTBUTTON: u32 = 0x2;
const TPM_BOTTOMALIGN: u32 = 0x20;
const IDI_APPLICATION: usize = 32512;
#[repr(C)]
#[derive(Clone, Copy)]
struct Guid {
data1: u32,
data2: u16,
data3: u16,
data4: [u8; 8],
}
#[repr(C)]
struct NotifyIconData {
cb_size: u32,
hwnd: Hwnd,
uid: u32,
flags: u32,
callback_message: u32,
icon: Handle,
tip: [u16; 128],
state: u32,
state_mask: u32,
info: [u16; 256],
timeout_or_version: u32,
info_title: [u16; 64],
info_flags: u32,
guid_item: Guid,
balloon_icon: Handle,
}
#[repr(C)]
struct NotifyIconIdentifier {
cb_size: u32,
hwnd: Hwnd,
uid: u32,
guid_item: Guid,
}
#[link(name = "shell32")]
unsafe extern "system" {
fn Shell_NotifyIconW(message: u32, data: *const NotifyIconData) -> i32;
fn Shell_NotifyIconGetRect(id: *const NotifyIconIdentifier, rect: *mut Rect) -> i32;
}
#[link(name = "user32")]
unsafe extern "system" {
fn TrackPopupMenu(
menu: Handle,
flags: u32,
x: i32,
y: i32,
reserved: i32,
hwnd: Hwnd,
rect: *const c_void,
) -> i32;
fn SetForegroundWindow(hwnd: Hwnd) -> i32;
fn GetCursorPos(point: *mut Point) -> i32;
fn RegisterWindowMessageW(name: *const u16) -> u32;
fn LoadIconW(instance: Handle, name: usize) -> Handle;
}
static MAIN_HWND: AtomicUsize = AtomicUsize::new(0);
static APP_ICON: AtomicUsize = AtomicUsize::new(0);
static TRAY_UP: AtomicBool = AtomicBool::new(false);
static NOTE_UP: AtomicBool = AtomicBool::new(false);
static TASKBAR_CREATED: AtomicU32 = AtomicU32::new(0);
#[derive(Default)]
pub(super) struct TrayState {
tray: Option<NativeTray>,
menu: usize,
icon: usize,
}
pub(super) fn install(hwnd: Hwnd) {
MAIN_HWND.store(hwnd as usize, Ordering::Release);
let name = wide("TaskbarCreated");
TASKBAR_CREATED.store(
unsafe { RegisterWindowMessageW(name.as_ptr()) },
Ordering::Release,
);
}
pub(super) fn set_app_icon(icon: Handle) {
APP_ICON.store(icon as usize, Ordering::Release);
}
fn data(hwnd: Hwnd, uid: u32) -> NotifyIconData {
let mut d: NotifyIconData = unsafe { std::mem::zeroed() };
d.cb_size = std::mem::size_of::<NotifyIconData>() as u32;
d.hwnd = hwnd;
d.uid = uid;
d
}
fn copy_into(dst: &mut [u16], s: &str) {
let max = dst.len() - 1;
let mut n = 0;
for u in s.encode_utf16().take(max) {
dst[n] = u;
n += 1;
}
dst[n] = 0;
}
fn fallback_icon() -> Handle {
match APP_ICON.load(Ordering::Acquire) {
0 => unsafe { LoadIconW(std::ptr::null_mut(), IDI_APPLICATION) },
icon => icon as Handle,
}
}
pub(super) fn set_tray(hwnd: Hwnd, tray: Option<&NativeTray>) -> bool {
let Some(tray) = tray else {
if TRAY_UP.swap(false, Ordering::AcqRel) {
unsafe { Shell_NotifyIconW(NIM_DELETE, &data(hwnd, TRAY_UID)) };
}
replace_state(TrayState::default());
return false;
};
let own_icon = tray
.icon
.as_ref()
.map(create_icon)
.unwrap_or(std::ptr::null_mut());
let menu = unsafe { super::CreatePopupMenu() };
fill_menu(menu, &tray.menu);
let mut d = data(hwnd, TRAY_UID);
d.flags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
d.callback_message = WM_APP_TRAY;
d.icon = if own_icon.is_null() {
fallback_icon()
} else {
own_icon
};
copy_into(&mut d.tip, &tray.tooltip);
let mut ok = false;
if TRAY_UP.load(Ordering::Acquire) {
ok = unsafe { Shell_NotifyIconW(NIM_MODIFY, &d) } != 0;
}
if !ok {
ok = unsafe { Shell_NotifyIconW(NIM_ADD, &d) } != 0;
}
TRAY_UP.store(ok, Ordering::Release);
debug_rect(hwnd, ok);
replace_state(TrayState {
tray: Some(tray.clone()),
menu: menu as usize,
icon: own_icon as usize,
});
ok
}
fn debug_rect(hwnd: Hwnd, added: bool) {
if std::env::var_os("STIPPLE_DEBUG_SHELL").is_none() {
return;
}
let id = NotifyIconIdentifier {
cb_size: std::mem::size_of::<NotifyIconIdentifier>() as u32,
hwnd,
uid: TRAY_UID,
guid_item: Guid {
data1: 0,
data2: 0,
data3: 0,
data4: [0; 8],
},
};
let mut r = Rect {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
let hr = unsafe { Shell_NotifyIconGetRect(&id, &mut r) };
eprintln!(
"stipple shell: Shell_NotifyIcon added={added}; GetRect hr={hr:#x} rect=({}, {}, {}, {})",
r.left, r.top, r.right, r.bottom
);
}
fn replace_state(new: TrayState) {
let old = CTX.with(|c| std::mem::replace(&mut c.borrow_mut().tray, new));
unsafe {
if old.menu != 0 {
super::DestroyMenu(old.menu as Handle);
}
if old.icon != 0 {
super::DestroyIcon(old.icon as Handle);
}
}
}
pub(super) fn callback(hwnd: Hwnd, wp: Wparam, lp: Lparam) {
let event = (lp & 0xffff) as u32;
match (wp as u32, event) {
(TRAY_UID, WM_LBUTTONUP) => push_event(Event::TrayActivated),
(TRAY_UID, WM_RBUTTONUP | WM_CONTEXTMENU) => show_menu(hwnd),
(NOTE_UID, NIN_BALLOONHIDE | NIN_BALLOONTIMEOUT | NIN_BALLOONUSERCLICK)
if NOTE_UP.swap(false, Ordering::AcqRel) =>
unsafe {
Shell_NotifyIconW(NIM_DELETE, &data(hwnd, NOTE_UID));
},
_ => {}
}
}
fn show_menu(hwnd: Hwnd) {
let menu = CTX.with(|c| c.borrow().tray.menu);
if menu == 0 {
return;
}
let mut pt = Point { x: 0, y: 0 };
unsafe {
GetCursorPos(&mut pt);
SetForegroundWindow(hwnd);
TrackPopupMenu(
menu as Handle,
TPM_RIGHTBUTTON | TPM_BOTTOMALIGN,
pt.x,
pt.y,
0,
hwnd,
std::ptr::null(),
);
super::PostMessageW(hwnd, WM_NULL, 0, 0);
}
}
pub(super) fn taskbar_created(hwnd: Hwnd, msg: u32) -> bool {
let id = TASKBAR_CREATED.load(Ordering::Acquire);
if id == 0 || msg != id {
return false;
}
TRAY_UP.store(false, Ordering::Release);
NOTE_UP.store(false, Ordering::Release);
let tray = CTX.with(|c| c.borrow().tray.tray.clone());
if let Some(tray) = tray {
set_tray(hwnd, Some(&tray));
}
true
}
pub(super) fn remove_all(hwnd: Hwnd) {
if TRAY_UP.swap(false, Ordering::AcqRel) {
unsafe { Shell_NotifyIconW(NIM_DELETE, &data(hwnd, TRAY_UID)) };
}
if NOTE_UP.swap(false, Ordering::AcqRel) {
unsafe { Shell_NotifyIconW(NIM_DELETE, &data(hwnd, NOTE_UID)) };
}
MAIN_HWND.store(0, Ordering::Release);
}
pub(crate) fn notify(title: &str, body: &str) -> Result<(), PlatformError> {
let hwnd = MAIN_HWND.load(Ordering::Acquire) as Hwnd;
if hwnd.is_null() {
return Err(PlatformError::Os(
"notifications need the app's window to exist".into(),
));
}
let on_tray = TRAY_UP.load(Ordering::Acquire);
let mut d = data(hwnd, if on_tray { TRAY_UID } else { NOTE_UID });
d.flags = NIF_INFO;
copy_into(&mut d.info_title, title);
copy_into(&mut d.info, if body.is_empty() { " " } else { body });
d.info_flags = NIIF_INFO;
let ok = if on_tray {
unsafe { Shell_NotifyIconW(NIM_MODIFY, &d) != 0 }
} else {
d.flags |= NIF_MESSAGE | NIF_ICON | NIF_TIP;
d.callback_message = WM_APP_TRAY;
d.icon = fallback_icon();
copy_into(&mut d.tip, title);
let mut ok =
NOTE_UP.load(Ordering::Acquire) && unsafe { Shell_NotifyIconW(NIM_MODIFY, &d) } != 0;
if !ok {
ok = unsafe { Shell_NotifyIconW(NIM_ADD, &d) } != 0;
}
NOTE_UP.store(ok, Ordering::Release);
ok
};
if std::env::var_os("STIPPLE_DEBUG_SHELL").is_some() {
eprintln!(
"stipple shell: notification balloon on uID {} shown={ok}",
d.uid
);
}
if ok {
Ok(())
} else {
Err(PlatformError::Os(
"Shell_NotifyIconW refused the balloon".into(),
))
}
}