1use windows::Win32::Foundation::{
2 ERROR_CLASS_ALREADY_EXISTS, GetLastError, HWND, LPARAM, LRESULT, WPARAM,
3};
4use windows::Win32::System::LibraryLoader::GetModuleHandleW;
5use windows::Win32::UI::Shell::{
6 NIF_ICON, NIF_INFO, NIF_TIP, NIIF_ERROR, NIIF_INFO, NIIF_NONE, NIIF_WARNING, NIM_ADD,
7 NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, Shell_NotifyIconW,
8};
9use windows::Win32::UI::WindowsAndMessaging::{
10 CreateWindowExW, DefWindowProcW, DestroyWindow, HICON, HWND_MESSAGE, IDI_APPLICATION,
11 LoadIconW, RegisterClassW, WINDOW_EX_STYLE, WINDOW_STYLE, WNDCLASSW,
12};
13use windows::core::PCWSTR;
14
15use crate::error::{DesktopError, DesktopOperationError, Error, InvalidParameterError, Result};
16use crate::utils::to_utf16_nul;
17
18use super::types::{BalloonIcon, TrayIconId, WindowHandle};
19
20const DEFAULT_TRAY_WINDOW_CLASS_NAME: &str = "windows_erg_tray_window";
21const DEFAULT_TRAY_WINDOW_NAME: &str = "windows_erg_tray_window";
22
23#[derive(Debug, Clone)]
25pub struct TrayNotification {
26 pub title: String,
28 pub body: String,
30 pub icon: BalloonIcon,
32}
33
34impl TrayNotification {
35 pub fn new(title: impl Into<String>, body: impl Into<String>, icon: BalloonIcon) -> Self {
37 Self {
38 title: title.into(),
39 body: body.into(),
40 icon,
41 }
42 }
43}
44
45#[derive(Debug)]
47pub struct TrayIcon {
48 hwnd: HWND,
49 id: TrayIconId,
50 icon_added: bool,
51 owns_window: bool,
52}
53
54impl TrayIcon {
55 pub fn new(id: TrayIconId, tooltip: &str) -> Result<Self> {
60 TrayIconBuilder::new(id, tooltip).create()
61 }
62
63 pub fn builder(id: TrayIconId, tooltip: impl Into<String>) -> TrayIconBuilder {
82 TrayIconBuilder::new(id, tooltip)
83 }
84
85 pub fn from_window(owner: WindowHandle, id: TrayIconId, tooltip: &str) -> Result<Self> {
87 let mut icon = TrayIcon {
88 hwnd: owner.into(),
89 id,
90 icon_added: false,
91 owns_window: false,
92 };
93
94 icon.add_icon(tooltip)?;
95 Ok(icon)
96 }
97
98 pub fn show_notification(&self, notification: &TrayNotification) -> Result<()> {
100 let mut data = self.base_notify_data();
101 data.uFlags = NIF_INFO;
102 copy_text_to_fixed(¬ification.body, &mut data.szInfo);
103 copy_text_to_fixed(¬ification.title, &mut data.szInfoTitle);
104 data.dwInfoFlags = match notification.icon {
105 BalloonIcon::None => NIIF_NONE,
106 BalloonIcon::Info => NIIF_INFO,
107 BalloonIcon::Warning => NIIF_WARNING,
108 BalloonIcon::Error => NIIF_ERROR,
109 };
110
111 let ok = unsafe { Shell_NotifyIconW(NIM_MODIFY, &data) }.as_bool();
112 if ok {
113 return Ok(());
114 }
115
116 let code = unsafe { GetLastError().0 as i32 };
117 Err(Error::Desktop(DesktopError::OperationFailed(
118 DesktopOperationError::with_code("Shell_NotifyIconW", "NIM_MODIFY notification", code),
119 )))
120 }
121
122 pub fn update_tooltip(&self, tooltip: &str) -> Result<()> {
124 let mut data = self.base_notify_data();
125 data.uFlags = NIF_TIP;
126 copy_text_to_fixed(tooltip, &mut data.szTip);
127
128 let ok = unsafe { Shell_NotifyIconW(NIM_MODIFY, &data) }.as_bool();
129 if ok {
130 return Ok(());
131 }
132
133 let code = unsafe { GetLastError().0 as i32 };
134 Err(Error::Desktop(DesktopError::OperationFailed(
135 DesktopOperationError::with_code("Shell_NotifyIconW", "NIM_MODIFY tooltip", code),
136 )))
137 }
138
139 pub fn remove(&mut self) -> Result<()> {
141 if !self.icon_added {
142 return Ok(());
143 }
144
145 let data = self.base_notify_data();
146 let ok = unsafe { Shell_NotifyIconW(NIM_DELETE, &data) }.as_bool();
147 if !ok {
148 let code = unsafe { GetLastError().0 as i32 };
149 return Err(Error::Desktop(DesktopError::OperationFailed(
150 DesktopOperationError::with_code("Shell_NotifyIconW", "NIM_DELETE", code),
151 )));
152 }
153
154 self.icon_added = false;
155 Ok(())
156 }
157
158 fn add_icon(&mut self, tooltip: &str) -> Result<()> {
159 let mut data = self.base_notify_data();
160 data.uFlags = NIF_ICON | NIF_TIP;
161 data.hIcon = load_default_icon();
162 copy_text_to_fixed(tooltip, &mut data.szTip);
163
164 let ok = unsafe { Shell_NotifyIconW(NIM_ADD, &data) }.as_bool();
165 if !ok {
166 let code = unsafe { GetLastError().0 as i32 };
167 return Err(Error::Desktop(DesktopError::OperationFailed(
168 DesktopOperationError::with_code("Shell_NotifyIconW", "NIM_ADD", code),
169 )));
170 }
171
172 self.icon_added = true;
173 Ok(())
174 }
175
176 fn base_notify_data(&self) -> NOTIFYICONDATAW {
177 NOTIFYICONDATAW {
178 cbSize: std::mem::size_of::<NOTIFYICONDATAW>() as u32,
179 hWnd: self.hwnd,
180 uID: self.id.as_u32(),
181 ..Default::default()
182 }
183 }
184}
185
186#[derive(Debug, Clone)]
188pub struct TrayIconBuilder {
189 id: TrayIconId,
190 tooltip: String,
191 owner: Option<WindowHandle>,
192 window_class_name: String,
193 window_name: String,
194}
195
196impl TrayIconBuilder {
197 pub fn new(id: TrayIconId, tooltip: impl Into<String>) -> Self {
199 Self {
200 id,
201 tooltip: tooltip.into(),
202 owner: None,
203 window_class_name: DEFAULT_TRAY_WINDOW_CLASS_NAME.to_string(),
204 window_name: DEFAULT_TRAY_WINDOW_NAME.to_string(),
205 }
206 }
207
208 pub fn owner_window(mut self, owner: WindowHandle) -> Self {
213 self.owner = Some(owner);
214 self
215 }
216
217 pub fn window_class_name(mut self, name: impl Into<String>) -> Self {
219 self.window_class_name = name.into();
220 self
221 }
222
223 pub fn window_name(mut self, name: impl Into<String>) -> Self {
225 self.window_name = name.into();
226 self
227 }
228
229 pub fn create(self) -> Result<TrayIcon> {
231 validate_window_name("window_class_name", &self.window_class_name)?;
232 validate_window_name("window_name", &self.window_name)?;
233
234 let hwnd = match self.owner {
235 Some(owner) => owner.into(),
236 None => create_message_window(&self.window_class_name, &self.window_name)?,
237 };
238
239 let mut icon = TrayIcon {
240 hwnd,
241 id: self.id,
242 icon_added: false,
243 owns_window: self.owner.is_none(),
244 };
245
246 icon.add_icon(&self.tooltip)?;
247 Ok(icon)
248 }
249}
250
251impl Drop for TrayIcon {
252 fn drop(&mut self) {
253 let _ = self.remove();
254
255 if self.owns_window && !self.hwnd.0.is_null() {
256 unsafe {
257 let _ = DestroyWindow(self.hwnd);
258 }
259 }
260 }
261}
262
263fn create_message_window(class_name: &str, window_name: &str) -> Result<HWND> {
264 let instance = unsafe { GetModuleHandleW(None) }.map_err(|e| {
265 Error::Desktop(DesktopError::OperationFailed(
266 DesktopOperationError::with_code("GetModuleHandleW", "tray window class", e.code().0),
267 ))
268 })?;
269
270 let class_name_wide = to_utf16_nul(class_name);
271 let window_name_wide = to_utf16_nul(window_name);
272
273 let wnd_class = WNDCLASSW {
274 lpfnWndProc: Some(tray_window_proc),
275 hInstance: instance.into(),
276 lpszClassName: PCWSTR(class_name_wide.as_ptr()),
277 ..Default::default()
278 };
279
280 let class_atom = unsafe { RegisterClassW(&wnd_class) };
281 if class_atom == 0 {
282 let code = unsafe { GetLastError() };
283 if code != ERROR_CLASS_ALREADY_EXISTS {
284 return Err(Error::Desktop(DesktopError::OperationFailed(
285 DesktopOperationError::with_code(
286 "RegisterClassW",
287 class_name.to_string(),
288 code.0 as i32,
289 ),
290 )));
291 }
292 }
293
294 let hwnd = unsafe {
295 CreateWindowExW(
296 WINDOW_EX_STYLE(0),
297 PCWSTR(class_name_wide.as_ptr()),
298 PCWSTR(window_name_wide.as_ptr()),
299 WINDOW_STYLE(0),
300 0,
301 0,
302 0,
303 0,
304 HWND_MESSAGE,
305 None,
306 instance,
307 None,
308 )
309 }
310 .map_err(|e| {
311 Error::Desktop(DesktopError::OperationFailed(
312 DesktopOperationError::with_code("CreateWindowExW", "tray message window", e.code().0),
313 ))
314 })?;
315
316 Ok(hwnd)
317}
318
319fn load_default_icon() -> HICON {
320 unsafe { LoadIconW(None, IDI_APPLICATION).unwrap_or_default() }
321}
322
323unsafe extern "system" fn tray_window_proc(
324 hwnd: HWND,
325 msg: u32,
326 wparam: WPARAM,
327 lparam: LPARAM,
328) -> LRESULT {
329 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
330}
331
332fn copy_text_to_fixed<const N: usize>(text: &str, destination: &mut [u16; N]) {
333 destination.fill(0);
334 let mut encoded = text.encode_utf16();
335
336 for slot in destination.iter_mut().take(N.saturating_sub(1)) {
337 if let Some(ch) = encoded.next() {
338 *slot = ch;
339 } else {
340 break;
341 }
342 }
343}
344
345fn validate_window_name(field: &'static str, value: &str) -> Result<()> {
346 if value.is_empty() {
347 return Err(Error::InvalidParameter(InvalidParameterError::new(
348 field,
349 "value cannot be empty",
350 )));
351 }
352
353 if value.contains('\0') {
354 return Err(Error::InvalidParameter(InvalidParameterError::new(
355 field,
356 "value cannot contain NUL characters",
357 )));
358 }
359
360 Ok(())
361}