winapi_easy/ui/
window.rs

1//! UI components related to windows.
2
3use std::any::Any;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::error::Error;
7use std::ffi::c_void;
8use std::fmt::{
9    Display,
10    Formatter,
11};
12use std::marker::PhantomData;
13use std::ops::{
14    BitOr,
15    Deref,
16};
17use std::ptr::NonNull;
18use std::rc::Rc;
19use std::{
20    io,
21    mem,
22    ptr,
23    vec,
24};
25
26use num_enum::{
27    IntoPrimitive,
28    TryFromPrimitive,
29};
30use windows::Win32::Foundation::{
31    ERROR_SUCCESS,
32    GetLastError,
33    HWND,
34    LPARAM,
35    NO_ERROR,
36    SetLastError,
37    WPARAM,
38};
39use windows::Win32::Graphics::Dwm::{
40    DWMWA_CLOAKED,
41    DwmGetWindowAttribute,
42};
43use windows::Win32::Graphics::Gdi::{
44    GetWindowRgn,
45    InvalidateRect,
46    MapWindowPoints,
47    RGN_ERROR,
48    SetWindowRgn,
49};
50use windows::Win32::System::Console::GetConsoleWindow;
51use windows::Win32::UI::Input::KeyboardAndMouse::SetActiveWindow;
52use windows::Win32::UI::Magnification::{
53    MAGTRANSFORM,
54    MS_SHOWMAGNIFIEDCURSOR,
55    MagSetWindowSource,
56    MagSetWindowTransform,
57    WC_MAGNIFIER,
58};
59use windows::Win32::UI::Shell::{
60    NIF_GUID,
61    NIF_ICON,
62    NIF_INFO,
63    NIF_MESSAGE,
64    NIF_SHOWTIP,
65    NIF_STATE,
66    NIF_TIP,
67    NIIF_ERROR,
68    NIIF_INFO,
69    NIIF_NONE,
70    NIIF_WARNING,
71    NIM_ADD,
72    NIM_DELETE,
73    NIM_MODIFY,
74    NIM_SETVERSION,
75    NIS_HIDDEN,
76    NOTIFY_ICON_INFOTIP_FLAGS,
77    NOTIFY_ICON_STATE,
78    NOTIFYICON_VERSION_4,
79    NOTIFYICONDATAW,
80    NOTIFYICONIDENTIFIER,
81    Shell_NotifyIconGetRect,
82    Shell_NotifyIconW,
83};
84use windows::Win32::UI::WindowsAndMessaging::{
85    CW_USEDEFAULT,
86    CreateWindowExW,
87    DestroyWindow,
88    EnumWindows,
89    FLASHW_ALL,
90    FLASHW_CAPTION,
91    FLASHW_STOP,
92    FLASHW_TIMER,
93    FLASHW_TIMERNOFG,
94    FLASHW_TRAY,
95    FLASHWINFO,
96    FLASHWINFO_FLAGS,
97    FlashWindowEx,
98    GWLP_USERDATA,
99    GetClassNameW,
100    GetClientRect,
101    GetDesktopWindow,
102    GetForegroundWindow,
103    GetWindowLongPtrW,
104    GetWindowPlacement,
105    GetWindowTextLengthW,
106    GetWindowTextW,
107    HICON,
108    HWND_BOTTOM,
109    HWND_NOTOPMOST,
110    HWND_TOP,
111    HWND_TOPMOST,
112    IsWindow,
113    IsWindowVisible,
114    KillTimer,
115    LWA_ALPHA,
116    RegisterClassExW,
117    SC_CLOSE,
118    SC_MAXIMIZE,
119    SC_MINIMIZE,
120    SC_MONITORPOWER,
121    SC_RESTORE,
122    SHOW_WINDOW_CMD,
123    SW_HIDE,
124    SW_MAXIMIZE,
125    SW_MINIMIZE,
126    SW_RESTORE,
127    SW_SHOW,
128    SW_SHOWMINIMIZED,
129    SW_SHOWMINNOACTIVE,
130    SW_SHOWNA,
131    SW_SHOWNOACTIVATE,
132    SW_SHOWNORMAL,
133    SWP_NOSIZE,
134    SendMessageW,
135    SetForegroundWindow,
136    SetLayeredWindowAttributes,
137    SetMenu,
138    SetTimer,
139    SetWindowLongPtrW,
140    SetWindowPlacement,
141    SetWindowPos,
142    SetWindowTextW,
143    ShowWindow,
144    UnregisterClassW,
145    WINDOW_EX_STYLE,
146    WINDOW_STYLE,
147    WINDOWPLACEMENT,
148    WM_SYSCOMMAND,
149    WNDCLASSEXW,
150    WPF_SETMINPOSITION,
151    WS_CHILD,
152    WS_CLIPCHILDREN,
153    WS_EX_COMPOSITED,
154    WS_EX_LAYERED,
155    WS_EX_LEFT,
156    WS_EX_NOACTIVATE,
157    WS_EX_TOPMOST,
158    WS_EX_TRANSPARENT,
159    WS_OVERLAPPED,
160    WS_OVERLAPPEDWINDOW,
161    WS_POPUP,
162    WS_VISIBLE,
163};
164use windows::core::{
165    BOOL,
166    GUID,
167    PCWSTR,
168};
169
170use super::{
171    Point,
172    RectTransform,
173    Rectangle,
174    Region,
175    init_magnifier,
176};
177use crate::internal::{
178    RawBox,
179    ResultExt,
180    ReturnValue,
181    custom_err_with_code,
182    with_sync_closure_to_callback2,
183};
184#[cfg(feature = "process")]
185use crate::process::{
186    ProcessId,
187    ThreadId,
188};
189use crate::string::{
190    FromWideString,
191    ZeroTerminatedWideString,
192    to_wide_chars_iter,
193};
194use crate::ui::menu::MenuBar;
195use crate::ui::messaging::{
196    CustomUserMessage,
197    ListenerAnswer,
198    ListenerMessage,
199    RawMessage,
200    WmlOpaqueClosure,
201    generic_window_proc,
202};
203use crate::ui::resource::{
204    Brush,
205    Cursor,
206    Icon,
207    ImageKindInternal,
208};
209
210/// A (non-null) handle to a window.
211#[derive(Clone, Copy, Eq, PartialEq, Debug)]
212pub struct WindowHandle {
213    raw_handle: HWND,
214}
215
216// See reasoning: https://docs.rs/hwnd0/0.0.0-2024-01-10/hwnd0/struct.HWND.html
217unsafe impl Send for WindowHandle {}
218unsafe impl Sync for WindowHandle {}
219
220impl WindowHandle {
221    /// Returns the console window associated with the current process, if there is one.
222    pub fn get_console_window() -> Option<Self> {
223        let handle = unsafe { GetConsoleWindow() };
224        Self::from_maybe_null(handle)
225    }
226
227    /// Returns the current foreground window, if any.
228    pub fn get_foreground_window() -> Option<Self> {
229        let handle = unsafe { GetForegroundWindow() };
230        Self::from_maybe_null(handle)
231    }
232
233    /// Returns the 'desktop' window.
234    pub fn get_desktop_window() -> io::Result<Self> {
235        let handle = unsafe { GetDesktopWindow() };
236        handle
237            .if_null_to_error(|| io::ErrorKind::Other.into())
238            .map(Self::from_non_null)
239    }
240
241    /// Returns all top-level windows of desktop apps.
242    pub fn get_toplevel_windows() -> io::Result<Vec<Self>> {
243        let mut result: Vec<WindowHandle> = Vec::new();
244        let callback = |handle: HWND, _app_value: LPARAM| -> BOOL {
245            let window_handle = Self::from_maybe_null(handle).unwrap_or_else(|| {
246                unreachable!("Window handle passed to callback should never be null")
247            });
248            result.push(window_handle);
249            true.into()
250        };
251        let acceptor = |raw_callback| unsafe { EnumWindows(Some(raw_callback), LPARAM::default()) };
252        with_sync_closure_to_callback2(callback, acceptor)?;
253        Ok(result)
254    }
255
256    pub(crate) fn from_non_null(handle: HWND) -> Self {
257        Self { raw_handle: handle }
258    }
259
260    pub(crate) fn from_maybe_null(handle: HWND) -> Option<Self> {
261        if handle.is_null() {
262            None
263        } else {
264            Some(Self { raw_handle: handle })
265        }
266    }
267
268    /// Checks if the handle points to an existing window.
269    pub fn is_window(self) -> bool {
270        let result = unsafe { IsWindow(Some(self.raw_handle)) };
271        result.as_bool()
272    }
273
274    pub fn is_visible(self) -> bool {
275        let result = unsafe { IsWindowVisible(self.raw_handle) };
276        result.as_bool()
277    }
278
279    /// Checks if the window is cloaked.
280    ///
281    /// See also: <https://devblogs.microsoft.com/oldnewthing/20200302-00/?p=103507>
282    pub fn is_cloaked(self) -> io::Result<bool> {
283        let mut is_cloaked = BOOL::default();
284        unsafe {
285            DwmGetWindowAttribute(
286                self.raw_handle,
287                DWMWA_CLOAKED,
288                (&raw mut is_cloaked).cast::<c_void>(),
289                mem::size_of::<BOOL>()
290                    .try_into()
291                    .unwrap_or_else(|_| unreachable!()),
292            )
293        }?;
294        Ok(is_cloaked.as_bool())
295    }
296
297    /// Returns the window caption text, converted to UTF-8 in a potentially lossy way.
298    pub fn get_caption_text(self) -> String {
299        let required_length: usize = unsafe { GetWindowTextLengthW(self.raw_handle) }
300            .try_into()
301            .unwrap_or_else(|_| unreachable!());
302        let required_length = if required_length == 0 {
303            return String::new();
304        } else {
305            1 + required_length
306        };
307
308        let mut buffer: Vec<u16> = vec![0; required_length as usize];
309        let copied_chars = unsafe { GetWindowTextW(self.raw_handle, buffer.as_mut()) }
310            .try_into()
311            .unwrap_or_else(|_| unreachable!());
312        if copied_chars == 0 {
313            String::new()
314        } else {
315            // Normally unnecessary, but the text length can theoretically change between the 2 API calls
316            buffer.truncate(copied_chars);
317            buffer.to_string_lossy()
318        }
319    }
320
321    /// Sets the window caption text.
322    pub fn set_caption_text(self, text: &str) -> io::Result<()> {
323        let ret_val = unsafe {
324            SetWindowTextW(
325                self.raw_handle,
326                ZeroTerminatedWideString::from_os_str(text).as_raw_pcwstr(),
327            )
328        };
329        ret_val?;
330        Ok(())
331    }
332
333    pub(crate) fn set_menu(self, menu: Option<&MenuBar>) -> io::Result<()> {
334        let maybe_raw_handle = menu.map(|x| x.as_handle().as_raw_handle());
335        unsafe { SetMenu(self.raw_handle, maybe_raw_handle) }?;
336        Ok(())
337    }
338
339    /// Brings the window to the foreground.
340    ///
341    /// May interfere with the Z-position of other windows created by this process.
342    pub fn set_as_foreground(self) -> io::Result<()> {
343        unsafe {
344            SetForegroundWindow(self.raw_handle).if_null_to_error_else_drop(|| {
345                io::Error::other("Cannot bring window to foreground")
346            })?;
347        }
348        Ok(())
349    }
350
351    /// Sets the window as the currently active (selected) window.
352    pub fn set_as_active(self) -> io::Result<()> {
353        unsafe {
354            SetActiveWindow(self.raw_handle)?;
355        }
356        Ok(())
357    }
358
359    /// Changes the window show state.
360    pub fn set_show_state(self, state: WindowShowState) -> io::Result<()> {
361        if self.is_window() {
362            unsafe {
363                let _ = ShowWindow(self.raw_handle, state.into());
364            }
365            Ok(())
366        } else {
367            Err(io::Error::new(
368                io::ErrorKind::NotFound,
369                "Cannot set show state because window does not exist",
370            ))
371        }
372    }
373
374    /// Returns the window's show state and positions.
375    pub fn get_placement(self) -> io::Result<WindowPlacement> {
376        let mut raw_placement: WINDOWPLACEMENT = WINDOWPLACEMENT {
377            length: mem::size_of::<WINDOWPLACEMENT>()
378                .try_into()
379                .unwrap_or_else(|_| unreachable!()),
380            ..Default::default()
381        };
382        unsafe { GetWindowPlacement(self.raw_handle, &raw mut raw_placement)? };
383        Ok(WindowPlacement { raw_placement })
384    }
385
386    /// Sets the window's show state and positions.
387    pub fn set_placement(self, placement: &WindowPlacement) -> io::Result<()> {
388        unsafe { SetWindowPlacement(self.raw_handle, &raw const placement.raw_placement)? };
389        Ok(())
390    }
391
392    pub fn modify_placement_with<F>(self, f: F) -> io::Result<()>
393    where
394        F: FnOnce(&mut WindowPlacement) -> io::Result<()>,
395    {
396        let mut placement = self.get_placement()?;
397        f(&mut placement)?;
398        self.set_placement(&placement)?;
399        Ok(())
400    }
401
402    pub fn set_z_position(self, z_position: WindowZPosition) -> io::Result<()> {
403        unsafe {
404            SetWindowPos(
405                self.raw_handle,
406                Some(z_position.to_raw_hwnd()),
407                0,
408                0,
409                0,
410                0,
411                SWP_NOSIZE,
412            )?;
413        }
414        Ok(())
415    }
416
417    /// Returns the window's client area rectangle relative to the screen.
418    pub fn get_client_area_coords(self) -> io::Result<Rectangle> {
419        let mut result_rect: Rectangle = Default::default();
420        unsafe { GetClientRect(self.raw_handle, &raw mut result_rect) }?;
421        self.map_points(None, result_rect.as_point_array_mut())?;
422        Ok(result_rect)
423    }
424
425    pub(crate) fn map_points(
426        self,
427        other_window: Option<Self>,
428        points: &mut [Point],
429    ) -> io::Result<()> {
430        unsafe { SetLastError(ERROR_SUCCESS) };
431        let map_result = unsafe {
432            MapWindowPoints(
433                Some(self.raw_handle),
434                other_window.map(|x| x.raw_handle),
435                points,
436            )
437        };
438        if map_result == 0 {
439            let last_error = unsafe { GetLastError() };
440            if last_error != ERROR_SUCCESS {
441                return Err(io::Error::last_os_error());
442            }
443        }
444        Ok(())
445    }
446
447    pub fn get_region(self) -> io::Result<Option<Region>> {
448        let region = Region::from_rect(Default::default())?;
449        let result = unsafe { GetWindowRgn(self.raw_handle, region.raw_handle) };
450        if result == RGN_ERROR {
451            Ok(None)
452        } else {
453            Ok(Some(region))
454        }
455    }
456
457    /// Sets the window's interaction region.
458    ///
459    /// Will potentially remove visual styles from the window.
460    pub fn set_region(self, region: Region) -> io::Result<()> {
461        unsafe {
462            SetWindowRgn(self.raw_handle, Some(region.into()), true)
463                .if_null_to_error_else_drop(|| io::ErrorKind::Other.into())
464        }
465    }
466
467    pub fn redraw(self) -> io::Result<()> {
468        unsafe {
469            InvalidateRect(Some(self.raw_handle), None, true).if_null_get_last_error_else_drop()
470        }
471    }
472
473    /// Returns the class name of the window's associated [`WindowClass`].
474    pub fn get_class_name(self) -> io::Result<String> {
475        const BUFFER_SIZE: usize = WindowClass::MAX_WINDOW_CLASS_NAME_CHARS + 1;
476        let mut buffer: Vec<u16> = vec![0; BUFFER_SIZE];
477        let chars_copied: usize = unsafe { GetClassNameW(self.raw_handle, buffer.as_mut()) }
478            .if_null_get_last_error()?
479            .try_into()
480            .unwrap_or_else(|_| unreachable!());
481        buffer.truncate(chars_copied);
482        Ok(buffer.to_string_lossy())
483    }
484
485    /// Sends a command to the window, same as if one of the symbols in its top right were clicked.
486    pub fn send_command(self, action: WindowCommand) -> io::Result<()> {
487        let result = unsafe {
488            SendMessageW(
489                self.raw_handle,
490                WM_SYSCOMMAND,
491                Some(WPARAM(action.to_usize())),
492                None,
493            )
494        };
495        result
496            .if_non_null_to_error(|| custom_err_with_code("Cannot perform window action", result.0))
497    }
498
499    /// Flashes the window using default flash settings.
500    ///
501    /// Same as [`Self::flash_custom`] using [`Default::default`] for all parameters.
502    pub fn flash(self) {
503        self.flash_custom(Default::default(), Default::default(), Default::default());
504    }
505
506    /// Flashes the window, allowing various customization parameters.
507    pub fn flash_custom(
508        self,
509        element: FlashElement,
510        duration: FlashDuration,
511        frequency: FlashInterval,
512    ) {
513        let (count, flags) = match duration {
514            FlashDuration::Count(count) => (count, Default::default()),
515            FlashDuration::CountUntilForeground(count) => (count, FLASHW_TIMERNOFG),
516            FlashDuration::ContinuousUntilForeground => (0, FLASHW_TIMERNOFG),
517            FlashDuration::Continuous => (0, FLASHW_TIMER),
518        };
519        let flags = flags | element.to_flashwinfo_flags();
520        let raw_config = FLASHWINFO {
521            cbSize: mem::size_of::<FLASHWINFO>()
522                .try_into()
523                .unwrap_or_else(|_| unreachable!()),
524            hwnd: self.into(),
525            dwFlags: flags,
526            uCount: count,
527            dwTimeout: match frequency {
528                FlashInterval::DefaultCursorBlinkInterval => 0,
529                FlashInterval::Milliseconds(ms) => ms,
530            },
531        };
532        unsafe {
533            let _ = FlashWindowEx(&raw const raw_config);
534        };
535    }
536
537    /// Stops the window from flashing.
538    pub fn flash_stop(self) {
539        let raw_config = FLASHWINFO {
540            cbSize: mem::size_of::<FLASHWINFO>()
541                .try_into()
542                .unwrap_or_else(|_| unreachable!()),
543            hwnd: self.into(),
544            dwFlags: FLASHW_STOP,
545            ..Default::default()
546        };
547        unsafe {
548            let _ = FlashWindowEx(&raw const raw_config);
549        };
550    }
551
552    fn internal_set_layered_opacity_alpha(self, alpha: u8) -> io::Result<()> {
553        unsafe {
554            SetLayeredWindowAttributes(self.raw_handle, Default::default(), alpha, LWA_ALPHA)?;
555        }
556        Ok(())
557    }
558
559    pub fn set_timer(self, timer_id: usize, interval_ms: u32) -> io::Result<()> {
560        unsafe {
561            SetTimer(Some(self.raw_handle), timer_id, interval_ms, None)
562                .if_null_get_last_error_else_drop()
563        }
564    }
565
566    pub fn kill_timer(self, timer_id: usize) -> io::Result<()> {
567        unsafe { KillTimer(Some(self.raw_handle), timer_id)? }
568        Ok(())
569    }
570
571    pub fn send_user_message(self, message: CustomUserMessage) -> io::Result<()> {
572        RawMessage::from(message).post_to_queue(Some(self))
573    }
574
575    /// Returns the thread ID that created this window.
576    #[cfg(feature = "process")]
577    pub fn get_creator_thread_id(self) -> ThreadId {
578        self.get_creator_thread_process_ids().0
579    }
580
581    /// Returns the process ID that created this window.
582    #[cfg(feature = "process")]
583    pub fn get_creator_process_id(self) -> ProcessId {
584        self.get_creator_thread_process_ids().1
585    }
586
587    #[cfg(feature = "process")]
588    fn get_creator_thread_process_ids(self) -> (ThreadId, ProcessId) {
589        use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
590        let mut process_id: u32 = 0;
591        let thread_id =
592            unsafe { GetWindowThreadProcessId(self.raw_handle, Some(&raw mut process_id)) };
593        (ThreadId(thread_id), ProcessId(process_id))
594    }
595
596    /// Returns all top-level (non-child) windows created by the thread.
597    #[cfg(feature = "process")]
598    pub fn get_nonchild_windows(thread_id: ThreadId) -> Vec<Self> {
599        use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
600        let mut result: Vec<WindowHandle> = Vec::new();
601        let callback = |handle: HWND, _app_value: LPARAM| -> BOOL {
602            let window_handle = WindowHandle::from_maybe_null(handle).unwrap_or_else(|| {
603                unreachable!("Window handle passed to callback should never be null")
604            });
605            result.push(window_handle);
606            true.into()
607        };
608        let acceptor = |raw_callback| {
609            let _ =
610                unsafe { EnumThreadWindows(thread_id.0, Some(raw_callback), LPARAM::default()) };
611        };
612        with_sync_closure_to_callback2(callback, acceptor);
613        result
614    }
615
616    /// Turns the monitor on or off.
617    ///
618    /// Windows requires this command to be sent through a window, e.g. using
619    /// [`WindowHandle::get_foreground_window`].
620    pub fn set_monitor_power(self, level: MonitorPower) -> io::Result<()> {
621        let result = unsafe {
622            SendMessageW(
623                self.raw_handle,
624                WM_SYSCOMMAND,
625                Some(WPARAM(
626                    SC_MONITORPOWER
627                        .try_into()
628                        .unwrap_or_else(|_| unreachable!()),
629                )),
630                Some(LPARAM(level.into())),
631            )
632        };
633        result.if_non_null_to_error(|| {
634            custom_err_with_code("Cannot set monitor power using window", result.0)
635        })
636    }
637
638    pub(crate) unsafe fn get_user_data_ptr<T>(self) -> Option<NonNull<T>> {
639        let ptr_value = unsafe { GetWindowLongPtrW(self.raw_handle, GWLP_USERDATA) };
640        NonNull::new(ptr::with_exposed_provenance_mut(ptr_value.cast_unsigned()))
641    }
642
643    pub(crate) unsafe fn set_user_data_ptr<T>(self, ptr: *const T) -> io::Result<()> {
644        unsafe { SetLastError(NO_ERROR) };
645        let ret_val = unsafe {
646            SetWindowLongPtrW(
647                self.raw_handle,
648                GWLP_USERDATA,
649                ptr.expose_provenance().cast_signed(),
650            )
651        };
652        if ret_val == 0 {
653            let err_val = unsafe { GetLastError() };
654            if err_val != NO_ERROR {
655                return Err(custom_err_with_code(
656                    "Cannot set window procedure",
657                    err_val.0,
658                ));
659            }
660        }
661        Ok(())
662    }
663}
664
665impl From<WindowHandle> for HWND {
666    /// Returns the underlying raw window handle used by [`windows`].
667    fn from(value: WindowHandle) -> Self {
668        value.raw_handle
669    }
670}
671
672impl TryFrom<HWND> for WindowHandle {
673    type Error = TryFromHWNDError;
674
675    /// Returns a new window handle from a raw handle if it is non-null.
676    fn try_from(value: HWND) -> Result<Self, Self::Error> {
677        WindowHandle::from_maybe_null(value).ok_or(TryFromHWNDError(()))
678    }
679}
680
681#[derive(Copy, Clone, PartialEq, Eq, Debug)]
682pub struct TryFromHWNDError(pub(crate) ());
683
684impl Display for TryFromHWNDError {
685    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
686        write!(f, "HWND value must not be null")
687    }
688}
689
690impl Error for TryFromHWNDError {}
691
692#[derive(Debug)]
693enum WindowClassVariant {
694    Builtin(PCWSTR),
695    Custom(Rc<WindowClass>),
696}
697
698impl WindowClassVariant {
699    fn raw_class_identifier(&self) -> PCWSTR {
700        match self {
701            WindowClassVariant::Builtin(pcwstr) => *pcwstr,
702            WindowClassVariant::Custom(window_class) => window_class.raw_class_identifier(),
703        }
704    }
705}
706
707/// Window class serving as a base for [`Window`].
708#[derive(Debug)]
709pub struct WindowClass {
710    atom: u16,
711    #[expect(dead_code)]
712    appearance: WindowClassAppearance,
713}
714
715impl WindowClass {
716    const MAX_WINDOW_CLASS_NAME_CHARS: usize = 256;
717
718    fn raw_class_identifier(&self) -> PCWSTR {
719        PCWSTR(self.atom as *const u16)
720    }
721}
722
723impl WindowClass {
724    /// Registers a new class.
725    ///
726    /// This class can then be used to create instances of [`Window`].
727    ///
728    /// The class name will be generated from the given prefix by adding a random base64 encoded UUID
729    /// to ensure uniqueness. This means that the maximum length of the class name prefix is a little less
730    /// than the standard 256 characters for class names.
731    pub fn register_new(
732        class_name_prefix: &str,
733        appearance: WindowClassAppearance,
734    ) -> io::Result<Self> {
735        use base64::Engine;
736        use base64::engine::general_purpose::URL_SAFE_NO_PAD;
737
738        let base64_uuid = URL_SAFE_NO_PAD.encode(uuid::Uuid::new_v4().as_bytes());
739        let class_name = class_name_prefix.to_string() + "_" + &base64_uuid;
740
741        let icon_handle = appearance
742            .icon
743            .as_deref()
744            .map_or_else(Default::default, Icon::as_handle);
745        // No need to reserve extra window memory if we only need a single pointer
746        let class_def = WNDCLASSEXW {
747            cbSize: mem::size_of::<WNDCLASSEXW>()
748                .try_into()
749                .unwrap_or_else(|_| unreachable!()),
750            lpfnWndProc: Some(generic_window_proc),
751            hIcon: icon_handle,
752            hCursor: appearance
753                .cursor
754                .as_deref()
755                .map_or_else(Default::default, Cursor::as_handle),
756            hbrBackground: appearance
757                .background_brush
758                .as_deref()
759                .map_or_else(Default::default, Brush::as_handle),
760            lpszClassName: ZeroTerminatedWideString::from_os_str(class_name).as_raw_pcwstr(),
761            ..Default::default()
762        };
763        let atom = unsafe { RegisterClassExW(&raw const class_def).if_null_get_last_error()? };
764        Ok(WindowClass { atom, appearance })
765    }
766}
767
768impl Drop for WindowClass {
769    /// Unregisters the class on drop.
770    fn drop(&mut self) {
771        unsafe { UnregisterClassW(self.raw_class_identifier(), None) }
772            .unwrap_or_default_and_print_error();
773    }
774}
775
776#[derive(Clone, Debug)]
777pub struct WindowClassAppearance {
778    pub background_brush: Option<Rc<Brush>>,
779    pub icon: Option<Rc<Icon>>,
780    pub cursor: Option<Rc<Cursor>>,
781}
782
783impl WindowClassAppearance {
784    pub fn empty() -> Self {
785        Self {
786            background_brush: None,
787            icon: None,
788            cursor: None,
789        }
790    }
791}
792
793impl Default for WindowClassAppearance {
794    fn default() -> Self {
795        Self {
796            background_brush: Some(Default::default()),
797            icon: Some(Default::default()),
798            cursor: Some(Default::default()),
799        }
800    }
801}
802
803pub type DefaultWmlType = fn(&ListenerMessage) -> ListenerAnswer;
804
805pub trait WindowSubtype: 'static {}
806
807impl WindowSubtype for () {}
808
809pub enum Layered {}
810
811impl WindowSubtype for Layered {}
812
813pub enum Magnifier {}
814
815impl WindowSubtype for Magnifier {}
816
817/// A window based on a [`WindowClass`].
818///
819/// # Multithreading
820///
821/// `Window` is not [`Send`] because the window procedure and window destruction calls
822/// must only be called from the creating thread.
823pub struct Window<WST = ()> {
824    handle: WindowHandle,
825    #[expect(dead_code)]
826    class: WindowClassVariant,
827    #[expect(dead_code)]
828    opaque_listener: Option<RawBox<WmlOpaqueClosure<'static>>>,
829    #[expect(dead_code)]
830    parent: Option<Rc<dyn Any>>,
831    notification_icons: HashMap<NotificationIconId, NotificationIcon>,
832    phantom: PhantomData<WST>,
833}
834
835#[cfg(test)]
836static_assertions::assert_not_impl_any!(Window: Send);
837
838impl<WST: WindowSubtype> Window<WST> {
839    fn internal_new<WML, PST>(
840        class: WindowClassVariant,
841        listener: Option<WML>,
842        caption_text: &str,
843        appearance: WindowAppearance,
844        parent: Option<Rc<RefCell<Window<PST>>>>,
845    ) -> io::Result<Self>
846    where
847        WML: FnMut(&ListenerMessage) -> ListenerAnswer + 'static,
848        PST: WindowSubtype,
849    {
850        let h_wnd: HWND = unsafe {
851            CreateWindowExW(
852                appearance.extended_style.into(),
853                class.raw_class_identifier(),
854                ZeroTerminatedWideString::from_os_str(caption_text).as_raw_pcwstr(),
855                appearance.style.into(),
856                CW_USEDEFAULT,
857                0,
858                CW_USEDEFAULT,
859                0,
860                parent.as_deref().map(|x| x.borrow().raw_handle),
861                None,
862                None,
863                None,
864            )?
865        };
866        let handle = WindowHandle::from_non_null(h_wnd);
867
868        let opaque_listener = if let Some(listener) = listener {
869            let opaque_listener = unsafe { Self::set_listener_internal(handle, listener) }?;
870            Some(opaque_listener)
871        } else {
872            None
873        };
874        Ok(Window {
875            handle,
876            class,
877            opaque_listener,
878            parent: parent.map(|x| x as Rc<dyn Any>),
879            notification_icons: HashMap::new(),
880            phantom: PhantomData,
881        })
882    }
883
884    pub fn as_handle(&self) -> WindowHandle {
885        self.handle
886    }
887
888    /// Changes the user window message listener.
889    pub fn set_listener<WML>(&mut self, listener: WML) -> io::Result<()>
890    where
891        WML: FnMut(&ListenerMessage) -> ListenerAnswer + 'static,
892    {
893        unsafe { Self::set_listener_internal(self.handle, listener) }?;
894        Ok(())
895    }
896
897    /// Internally sets the listener
898    ///
899    /// # Safety
900    ///
901    /// The returned value must not be dropped while the window callback may still be active.
902    unsafe fn set_listener_internal<WML>(
903        window_handle: WindowHandle,
904        listener: WML,
905    ) -> io::Result<RawBox<WmlOpaqueClosure<'static>>>
906    where
907        WML: FnMut(&ListenerMessage) -> ListenerAnswer + 'static,
908    {
909        let mut opaque_listener = RawBox::new(Box::new(listener) as WmlOpaqueClosure);
910        unsafe {
911            window_handle.set_user_data_ptr::<WmlOpaqueClosure>(opaque_listener.as_mut_ptr())?;
912        }
913        Ok(opaque_listener)
914    }
915
916    /// Sets or removes the top menu bar.
917    pub fn set_menu(&mut self, menu: Option<&MenuBar>) -> io::Result<()> {
918        self.handle.set_menu(menu)
919    }
920
921    /// Adds a new notification icon.
922    ///
923    /// # Panics
924    ///
925    /// Will panic if the notification icon ID already exists.
926    pub fn add_notification_icon(
927        &mut self,
928        options: NotificationIconOptions,
929    ) -> io::Result<&mut NotificationIcon> {
930        let id = options.icon_id;
931        assert!(
932            !self.notification_icons.contains_key(&id),
933            "Notification icon ID already exists"
934        );
935        self.notification_icons
936            .insert(id, NotificationIcon::new(self.handle, options)?);
937        Ok(self.get_notification_icon_mut(id))
938    }
939
940    /// Returns a reference to a previously added notification icon.
941    ///
942    /// # Panics
943    ///
944    /// Will panic if the ID doesn't exist.
945    pub fn get_notification_icon(&self, id: NotificationIconId) -> &NotificationIcon {
946        self.notification_icons
947            .get(&id)
948            .expect("Notification icon ID doesn't exist")
949    }
950
951    /// Returns a mutable reference to a previously added notification icon.
952    ///
953    /// # Panics
954    ///
955    /// Will panic if the ID doesn't exist.
956    pub fn get_notification_icon_mut(&mut self, id: NotificationIconId) -> &mut NotificationIcon {
957        self.notification_icons
958            .get_mut(&id)
959            .expect("Notification icon ID doesn't exist")
960    }
961
962    /// Removes a notification icon.
963    ///
964    /// # Panics
965    ///
966    /// Will panic if the ID doesn't exist.
967    pub fn remove_notification_icon(&mut self, id: NotificationIconId) {
968        let _ = self
969            .notification_icons
970            .remove(&id)
971            .expect("Notification icon ID doesn't exist");
972    }
973}
974
975impl Window<()> {
976    /// Creates a new window.
977    ///
978    /// User interaction with the window will result in messages sent to the window message listener provided here.
979    ///
980    /// # Generics
981    ///
982    /// Note that you can use [`DefaultWmlType`] for the `WML` type parameter when not providing a listener.
983    pub fn new<WML, PST>(
984        class: Rc<WindowClass>,
985        listener: Option<WML>,
986        caption_text: &str,
987        appearance: WindowAppearance,
988        parent: Option<Rc<RefCell<Window<PST>>>>,
989    ) -> io::Result<Self>
990    where
991        WML: FnMut(&ListenerMessage) -> ListenerAnswer + 'static,
992        PST: WindowSubtype,
993    {
994        let class = WindowClassVariant::Custom(class);
995        Self::internal_new(class, listener, caption_text, appearance, parent)
996    }
997}
998
999impl Window<Layered> {
1000    /// Creates a new layered window.
1001    ///
1002    /// This is analogous to [`Window::new`].
1003    pub fn new_layered<WML, PST>(
1004        class: Rc<WindowClass>,
1005        listener: Option<WML>,
1006        caption_text: &str,
1007        mut appearance: WindowAppearance,
1008        parent: Option<Rc<RefCell<Window<PST>>>>,
1009    ) -> io::Result<Self>
1010    where
1011        WML: FnMut(&ListenerMessage) -> ListenerAnswer + 'static,
1012        PST: WindowSubtype,
1013    {
1014        appearance.extended_style =
1015            appearance.extended_style | WindowExtendedStyle::Other(WS_EX_LAYERED.0);
1016        let class = WindowClassVariant::Custom(class);
1017        Self::internal_new(class, listener, caption_text, appearance, parent)
1018    }
1019
1020    /// Sets the opacity value for a layered window.
1021    pub fn set_layered_opacity_alpha(&self, alpha: u8) -> io::Result<()> {
1022        self.handle.internal_set_layered_opacity_alpha(alpha)
1023    }
1024}
1025
1026impl Window<Magnifier> {
1027    pub fn new_magnifier(
1028        caption_text: &str,
1029        mut appearance: WindowAppearance,
1030        parent: Rc<RefCell<Window<Layered>>>,
1031    ) -> io::Result<Self> {
1032        init_magnifier()?;
1033        appearance.style =
1034            appearance.style | WindowStyle::Other(MS_SHOWMAGNIFIEDCURSOR.cast_unsigned());
1035        let class = WindowClassVariant::Builtin(WC_MAGNIFIER);
1036        Self::internal_new(
1037            class,
1038            None::<DefaultWmlType>,
1039            caption_text,
1040            appearance,
1041            Some(parent),
1042        )
1043    }
1044
1045    pub fn set_magnification_factor(&self, mag_factor: f32) -> io::Result<()> {
1046        const NUM_COLS: usize = 3;
1047        fn multi_index(matrix: &mut [f32], row: usize, col: usize) -> &mut f32 {
1048            &mut matrix[row * NUM_COLS + col]
1049        }
1050        let mut matrix: MAGTRANSFORM = Default::default();
1051        *multi_index(&mut matrix.v, 0, 0) = mag_factor;
1052        *multi_index(&mut matrix.v, 1, 1) = mag_factor;
1053        *multi_index(&mut matrix.v, 2, 2) = 1.0;
1054        unsafe {
1055            MagSetWindowTransform(self.raw_handle, &raw mut matrix)
1056                .if_null_get_last_error_else_drop()
1057        }
1058    }
1059
1060    pub fn set_magnification_source(&self, source: Rectangle) -> io::Result<()> {
1061        let _ = unsafe { MagSetWindowSource(self.raw_handle, source).if_null_get_last_error()? };
1062        Ok(())
1063    }
1064
1065    pub fn set_lens_use_bitmap_smoothing(&self, use_smoothing: bool) -> io::Result<()> {
1066        #[link(
1067            name = "magnification.dll",
1068            kind = "raw-dylib",
1069            modifiers = "+verbatim"
1070        )]
1071        unsafe extern "system" {
1072            fn MagSetLensUseBitmapSmoothing(h_wnd: HWND, use_smoothing: BOOL) -> BOOL;
1073        }
1074        unsafe {
1075            MagSetLensUseBitmapSmoothing(self.raw_handle, use_smoothing.into())
1076                .if_null_get_last_error_else_drop()
1077        }
1078    }
1079}
1080
1081impl<WST> Deref for Window<WST> {
1082    type Target = WindowHandle;
1083
1084    fn deref(&self) -> &Self::Target {
1085        &self.handle
1086    }
1087}
1088
1089impl<WST> Drop for Window<WST> {
1090    fn drop(&mut self) {
1091        if self.handle.is_window() {
1092            unsafe { DestroyWindow(self.handle.raw_handle) }.unwrap_or_default_and_print_error();
1093        }
1094    }
1095}
1096
1097/// Window style.
1098///
1099/// Using combinations is possible with [`std::ops::BitOr`].
1100///
1101/// See also: [Microsoft docs](https://learn.microsoft.com/en-us/windows/win32/winmsg/window-styles)
1102#[derive(IntoPrimitive, TryFromPrimitive, Copy, Clone, Eq, PartialEq, Debug)]
1103#[non_exhaustive]
1104#[repr(u32)]
1105pub enum WindowStyle {
1106    Child = WS_CHILD.0,
1107    ClipChildren = WS_CLIPCHILDREN.0,
1108    Overlapped = WS_OVERLAPPED.0,
1109    OverlappedWindow = WS_OVERLAPPEDWINDOW.0,
1110    Popup = WS_POPUP.0,
1111    Visible = WS_VISIBLE.0,
1112    #[num_enum(catch_all)]
1113    Other(u32),
1114}
1115
1116#[expect(clippy::derivable_impls)]
1117impl Default for WindowStyle {
1118    fn default() -> Self {
1119        Self::Overlapped
1120    }
1121}
1122
1123impl BitOr for WindowStyle {
1124    type Output = WindowStyle;
1125
1126    fn bitor(self, rhs: Self) -> Self::Output {
1127        Self::Other(u32::from(self) | u32::from(rhs))
1128    }
1129}
1130
1131impl From<WindowStyle> for WINDOW_STYLE {
1132    fn from(value: WindowStyle) -> Self {
1133        WINDOW_STYLE(value.into())
1134    }
1135}
1136
1137/// Extended window style.
1138///
1139/// Using combinations is possible with [`std::ops::BitOr`].
1140///
1141/// See also: [Microsoft docs](https://learn.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles)
1142#[derive(IntoPrimitive, TryFromPrimitive, Copy, Clone, Eq, PartialEq, Debug)]
1143#[non_exhaustive]
1144#[repr(u32)]
1145pub enum WindowExtendedStyle {
1146    Composited = WS_EX_COMPOSITED.0,
1147    Left = WS_EX_LEFT.0,
1148    NoActivate = WS_EX_NOACTIVATE.0,
1149    Topmost = WS_EX_TOPMOST.0,
1150    Transparent = WS_EX_TRANSPARENT.0,
1151    #[num_enum(catch_all)]
1152    Other(u32),
1153}
1154
1155#[expect(clippy::derivable_impls)]
1156impl Default for WindowExtendedStyle {
1157    fn default() -> Self {
1158        Self::Left
1159    }
1160}
1161
1162impl BitOr for WindowExtendedStyle {
1163    type Output = WindowExtendedStyle;
1164
1165    fn bitor(self, rhs: Self) -> Self::Output {
1166        Self::Other(u32::from(self) | u32::from(rhs))
1167    }
1168}
1169
1170impl From<WindowExtendedStyle> for WINDOW_EX_STYLE {
1171    fn from(value: WindowExtendedStyle) -> Self {
1172        WINDOW_EX_STYLE(value.into())
1173    }
1174}
1175
1176#[derive(Copy, Clone, Eq, PartialEq, Default, Debug)]
1177pub struct WindowAppearance {
1178    pub style: WindowStyle,
1179    pub extended_style: WindowExtendedStyle,
1180}
1181
1182/// Window show state such as 'minimized' or 'hidden'.
1183///
1184/// Changing this state for a window can be done with [`WindowHandle::set_show_state`].
1185///
1186/// [`WindowHandle::get_placement`] and [`WindowPlacement::get_show_state`] can be used to read the state.
1187#[derive(IntoPrimitive, TryFromPrimitive, Copy, Clone, Eq, PartialEq, Debug)]
1188#[repr(i32)]
1189pub enum WindowShowState {
1190    Hide = SW_HIDE.0,
1191    Maximize = SW_MAXIMIZE.0,
1192    Minimize = SW_MINIMIZE.0,
1193    Restore = SW_RESTORE.0,
1194    Show = SW_SHOW.0,
1195    ShowMinimized = SW_SHOWMINIMIZED.0,
1196    ShowMinNoActivate = SW_SHOWMINNOACTIVE.0,
1197    ShowNoActivate = SW_SHOWNA.0,
1198    ShowNormalNoActivate = SW_SHOWNOACTIVATE.0,
1199    ShowNormal = SW_SHOWNORMAL.0,
1200}
1201
1202impl From<WindowShowState> for SHOW_WINDOW_CMD {
1203    fn from(value: WindowShowState) -> Self {
1204        SHOW_WINDOW_CMD(value.into())
1205    }
1206}
1207
1208/// Window show state plus positions.
1209#[derive(Copy, Clone, Debug)]
1210pub struct WindowPlacement {
1211    raw_placement: WINDOWPLACEMENT,
1212}
1213
1214impl WindowPlacement {
1215    pub fn get_show_state(&self) -> Option<WindowShowState> {
1216        i32::try_from(self.raw_placement.showCmd)
1217            .ok()?
1218            .try_into()
1219            .ok()
1220    }
1221
1222    pub fn set_show_state(&mut self, state: WindowShowState) {
1223        self.raw_placement.showCmd = i32::from(state)
1224            .try_into()
1225            .unwrap_or_else(|_| unreachable!());
1226    }
1227
1228    pub fn get_minimized_position(&self) -> Point {
1229        self.raw_placement.ptMinPosition
1230    }
1231
1232    pub fn set_minimized_position(&mut self, coords: Point) {
1233        self.raw_placement.ptMinPosition = coords;
1234        self.raw_placement.flags |= WPF_SETMINPOSITION;
1235    }
1236
1237    pub fn get_maximized_position(&self) -> Point {
1238        self.raw_placement.ptMaxPosition
1239    }
1240
1241    pub fn set_maximized_position(&mut self, coords: Point) {
1242        self.raw_placement.ptMaxPosition = coords;
1243    }
1244
1245    pub fn get_normal_position(&self) -> Rectangle {
1246        self.raw_placement.rcNormalPosition
1247    }
1248
1249    pub fn set_normal_position(&mut self, rectangle: Rectangle) {
1250        self.raw_placement.rcNormalPosition = rectangle;
1251    }
1252}
1253
1254#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1255pub enum WindowZPosition {
1256    Bottom,
1257    NoTopMost,
1258    Top,
1259    TopMost,
1260}
1261
1262impl WindowZPosition {
1263    fn to_raw_hwnd(self) -> HWND {
1264        match self {
1265            WindowZPosition::Bottom => HWND_BOTTOM,
1266            WindowZPosition::NoTopMost => HWND_NOTOPMOST,
1267            WindowZPosition::Top => HWND_TOP,
1268            WindowZPosition::TopMost => HWND_TOPMOST,
1269        }
1270    }
1271}
1272
1273/// Window command corresponding to its buttons in the top right corner.
1274#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Debug)]
1275#[non_exhaustive]
1276#[repr(u32)]
1277pub enum WindowCommand {
1278    Close = SC_CLOSE,
1279    Maximize = SC_MAXIMIZE,
1280    Minimize = SC_MINIMIZE,
1281    Restore = SC_RESTORE,
1282}
1283
1284impl WindowCommand {
1285    fn to_usize(self) -> usize {
1286        usize::try_from(u32::from(self)).unwrap()
1287    }
1288}
1289
1290/// The target of the flash animation.
1291#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Default, Debug)]
1292#[repr(u32)]
1293pub enum FlashElement {
1294    Caption = FLASHW_CAPTION.0,
1295    Taskbar = FLASHW_TRAY.0,
1296    #[default]
1297    CaptionPlusTaskbar = FLASHW_ALL.0,
1298}
1299
1300impl FlashElement {
1301    fn to_flashwinfo_flags(self) -> FLASHWINFO_FLAGS {
1302        FLASHWINFO_FLAGS(u32::from(self))
1303    }
1304}
1305
1306/// The amount of times the window should be flashed.
1307#[derive(Copy, Clone, Eq, PartialEq, Debug)]
1308pub enum FlashDuration {
1309    Count(u32),
1310    CountUntilForeground(u32),
1311    ContinuousUntilForeground,
1312    Continuous,
1313}
1314
1315impl Default for FlashDuration {
1316    fn default() -> Self {
1317        FlashDuration::CountUntilForeground(5)
1318    }
1319}
1320
1321/// The interval between flashes.
1322#[derive(Copy, Clone, Eq, PartialEq, Default, Debug)]
1323pub enum FlashInterval {
1324    #[default]
1325    DefaultCursorBlinkInterval,
1326    Milliseconds(u32),
1327}
1328
1329/// Monitor power state.
1330///
1331/// Can be set using [`WindowHandle::set_monitor_power`].
1332#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Default, Debug)]
1333#[repr(isize)]
1334pub enum MonitorPower {
1335    #[default]
1336    On = -1,
1337    Low = 1,
1338    Off = 2,
1339}
1340
1341/// An icon in the Windows notification area.
1342///
1343/// This icon is always associated with a window and can be used in conjunction with [`crate::ui::menu::SubMenu`].
1344#[derive(Debug)]
1345pub struct NotificationIcon {
1346    id: NotificationIconId,
1347    window: WindowHandle,
1348    icon: Rc<Icon>,
1349}
1350
1351impl NotificationIcon {
1352    /// Adds a notification icon.
1353    ///
1354    /// The window's [`WindowMessageListener`] will receive messages when user interactions with the icon occur.
1355    fn new(window: WindowHandle, options: NotificationIconOptions) -> io::Result<Self> {
1356        // For GUID handling maybe look at generating it from the executable path:
1357        // https://stackoverflow.com/questions/7432319/notifyicondata-guid-problem
1358        let call_data = Self::get_notification_call_data(
1359            window,
1360            options.icon_id,
1361            true,
1362            Some(options.icon.as_handle()),
1363            options.tooltip_text.as_deref(),
1364            Some(!options.visible),
1365            None,
1366        );
1367        unsafe {
1368            Shell_NotifyIconW(NIM_ADD, &raw const call_data)
1369                .if_null_to_error_else_drop(|| io::Error::other("Cannot add notification icon"))?;
1370            Shell_NotifyIconW(NIM_SETVERSION, &raw const call_data).if_null_to_error_else_drop(
1371                || io::Error::other("Cannot set notification version"),
1372            )?;
1373        };
1374        Ok(NotificationIcon {
1375            id: options.icon_id,
1376            window,
1377            icon: options.icon,
1378        })
1379    }
1380
1381    pub fn get_bounding_rectangle(&self) -> io::Result<Rectangle> {
1382        let identifier = self.get_raw_identifier();
1383        unsafe { Shell_NotifyIconGetRect(&raw const identifier).map_err(Into::into) }
1384    }
1385
1386    /// Sets the icon graphics.
1387    pub fn set_icon(&mut self, icon: Rc<Icon>) -> io::Result<()> {
1388        let call_data = Self::get_notification_call_data(
1389            self.window,
1390            self.id,
1391            false,
1392            Some(icon.as_handle()),
1393            None,
1394            None,
1395            None,
1396        );
1397        unsafe {
1398            Shell_NotifyIconW(NIM_MODIFY, &raw const call_data)
1399                .if_null_to_error_else_drop(|| io::Error::other("Cannot set notification icon"))?;
1400        };
1401        self.icon = icon;
1402        Ok(())
1403    }
1404
1405    /// Allows showing or hiding the icon in the notification area.
1406    pub fn set_icon_hidden_state(&mut self, hidden: bool) -> io::Result<()> {
1407        let call_data = Self::get_notification_call_data(
1408            self.window,
1409            self.id,
1410            false,
1411            None,
1412            None,
1413            Some(hidden),
1414            None,
1415        );
1416        unsafe {
1417            Shell_NotifyIconW(NIM_MODIFY, &raw const call_data).if_null_to_error_else_drop(
1418                || io::Error::other("Cannot set notification icon hidden state"),
1419            )?;
1420        };
1421        Ok(())
1422    }
1423
1424    /// Sets the tooltip text when hovering over the icon with the mouse.
1425    pub fn set_tooltip_text(&mut self, text: &str) -> io::Result<()> {
1426        let call_data = Self::get_notification_call_data(
1427            self.window,
1428            self.id,
1429            false,
1430            None,
1431            Some(text),
1432            None,
1433            None,
1434        );
1435        unsafe {
1436            Shell_NotifyIconW(NIM_MODIFY, &raw const call_data).if_null_to_error_else_drop(
1437                || io::Error::other("Cannot set notification icon tooltip text"),
1438            )?;
1439        };
1440        Ok(())
1441    }
1442
1443    /// Triggers a balloon notification above the notification icon.
1444    pub fn set_balloon_notification(
1445        &mut self,
1446        notification: Option<BalloonNotification>,
1447    ) -> io::Result<()> {
1448        let call_data = Self::get_notification_call_data(
1449            self.window,
1450            self.id,
1451            false,
1452            None,
1453            None,
1454            None,
1455            Some(notification),
1456        );
1457        unsafe {
1458            Shell_NotifyIconW(NIM_MODIFY, &raw const call_data).if_null_to_error_else_drop(
1459                || io::Error::other("Cannot set notification icon balloon text"),
1460            )?;
1461        };
1462        Ok(())
1463    }
1464
1465    fn get_raw_identifier(&self) -> NOTIFYICONIDENTIFIER {
1466        let (uid, guid_item) = match self.id {
1467            NotificationIconId::Simple(uid) => (uid, GUID::zeroed()),
1468            NotificationIconId::GUID(guid) => (0, guid),
1469        };
1470        NOTIFYICONIDENTIFIER {
1471            cbSize: std::mem::size_of::<NOTIFYICONIDENTIFIER>()
1472                .try_into()
1473                .unwrap_or_else(|_| unreachable!()),
1474            hWnd: self.window.into(),
1475            uID: uid.into(),
1476            guidItem: guid_item,
1477        }
1478    }
1479
1480    #[expect(clippy::option_option)]
1481    fn get_notification_call_data(
1482        window_handle: WindowHandle,
1483        icon_id: NotificationIconId,
1484        set_callback_message: bool,
1485        maybe_icon: Option<HICON>,
1486        maybe_tooltip_str: Option<&str>,
1487        icon_hidden_state: Option<bool>,
1488        maybe_balloon_text: Option<Option<BalloonNotification>>,
1489    ) -> NOTIFYICONDATAW {
1490        let mut icon_data = NOTIFYICONDATAW {
1491            cbSize: mem::size_of::<NOTIFYICONDATAW>()
1492                .try_into()
1493                .expect("NOTIFYICONDATAW size conversion failed"),
1494            hWnd: window_handle.into(),
1495            ..Default::default()
1496        };
1497        icon_data.Anonymous.uVersion = NOTIFYICON_VERSION_4;
1498        match icon_id {
1499            NotificationIconId::GUID(id) => {
1500                icon_data.guidItem = id;
1501                icon_data.uFlags |= NIF_GUID;
1502            }
1503            NotificationIconId::Simple(simple_id) => icon_data.uID = simple_id.into(),
1504        }
1505        if set_callback_message {
1506            icon_data.uCallbackMessage = super::messaging::RawMessage::ID_NOTIFICATION_ICON_MSG;
1507            icon_data.uFlags |= NIF_MESSAGE;
1508        }
1509        if let Some(icon) = maybe_icon {
1510            icon_data.hIcon = icon;
1511            icon_data.uFlags |= NIF_ICON;
1512        }
1513        if let Some(tooltip_str) = maybe_tooltip_str {
1514            let chars = to_wide_chars_iter(tooltip_str)
1515                .take(icon_data.szTip.len() - 1)
1516                .chain(std::iter::once(0))
1517                .enumerate();
1518            for (i, w_char) in chars {
1519                icon_data.szTip[i] = w_char;
1520            }
1521            icon_data.uFlags |= NIF_TIP;
1522            // Standard tooltip is normally suppressed on NOTIFYICON_VERSION_4
1523            icon_data.uFlags |= NIF_SHOWTIP;
1524        }
1525        if let Some(hidden_state) = icon_hidden_state {
1526            if hidden_state {
1527                icon_data.dwState = NOTIFY_ICON_STATE(icon_data.dwState.0 | NIS_HIDDEN.0);
1528                icon_data.dwStateMask |= NIS_HIDDEN;
1529            }
1530            icon_data.uFlags |= NIF_STATE;
1531        }
1532        if let Some(set_balloon_notification) = maybe_balloon_text {
1533            if let Some(balloon) = set_balloon_notification {
1534                let body_chars = to_wide_chars_iter(balloon.body)
1535                    .take(icon_data.szInfo.len() - 1)
1536                    .chain(std::iter::once(0))
1537                    .enumerate();
1538                for (i, w_char) in body_chars {
1539                    icon_data.szInfo[i] = w_char;
1540                }
1541                let title_chars = to_wide_chars_iter(balloon.title)
1542                    .take(icon_data.szInfoTitle.len() - 1)
1543                    .chain(std::iter::once(0))
1544                    .enumerate();
1545                for (i, w_char) in title_chars {
1546                    icon_data.szInfoTitle[i] = w_char;
1547                }
1548                icon_data.dwInfoFlags =
1549                    NOTIFY_ICON_INFOTIP_FLAGS(icon_data.dwInfoFlags.0 | u32::from(balloon.icon));
1550            }
1551            icon_data.uFlags |= NIF_INFO;
1552        }
1553        icon_data
1554    }
1555}
1556
1557impl Drop for NotificationIcon {
1558    fn drop(&mut self) {
1559        let call_data =
1560            Self::get_notification_call_data(self.window, self.id, false, None, None, None, None);
1561        unsafe { Shell_NotifyIconW(NIM_DELETE, &raw const call_data) }
1562            .if_null_get_last_error_else_drop()
1563            .unwrap_or_default_and_print_error();
1564    }
1565}
1566
1567/// Notification icon ID given to the Windows API.
1568#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
1569pub enum NotificationIconId {
1570    /// A simple ID.
1571    Simple(u16),
1572    /// A GUID that allows Windows to track the icon between applidation restarts.
1573    ///
1574    /// This way the user can set preferences for icon visibility and position.
1575    GUID(GUID),
1576}
1577
1578impl Default for NotificationIconId {
1579    fn default() -> Self {
1580        NotificationIconId::Simple(0)
1581    }
1582}
1583
1584/// Options for a new notification icon used by [`Window::add_notification_icon`].
1585#[derive(Eq, PartialEq, Default, Debug)]
1586pub struct NotificationIconOptions {
1587    pub icon_id: NotificationIconId,
1588    pub icon: Rc<Icon>,
1589    pub tooltip_text: Option<String>,
1590    pub visible: bool,
1591}
1592
1593/// A Balloon notification above the Windows notification area.
1594///
1595/// Used with [`NotificationIcon::set_balloon_notification`].
1596#[derive(Copy, Clone, Default, Debug)]
1597pub struct BalloonNotification<'a> {
1598    pub title: &'a str,
1599    pub body: &'a str,
1600    pub icon: BalloonNotificationStandardIcon,
1601}
1602
1603/// Built-in Windows standard icons for balloon notifications.
1604#[derive(IntoPrimitive, Copy, Clone, Default, Debug)]
1605#[repr(u32)]
1606pub enum BalloonNotificationStandardIcon {
1607    #[default]
1608    None = NIIF_NONE.0,
1609    Info = NIIF_INFO.0,
1610    Warning = NIIF_WARNING.0,
1611    Error = NIIF_ERROR.0,
1612}
1613
1614#[cfg(test)]
1615mod tests {
1616    use more_asserts::*;
1617
1618    use super::*;
1619
1620    #[test]
1621    fn run_window_tests_without_parallelism() -> io::Result<()> {
1622        check_toplevel_windows()?;
1623        new_window_with_class()?;
1624        Ok(())
1625    }
1626
1627    fn check_toplevel_windows() -> io::Result<()> {
1628        let all_windows = WindowHandle::get_toplevel_windows()?;
1629        assert_gt!(all_windows.len(), 0);
1630        for window in all_windows {
1631            assert!(window.is_window());
1632            assert!(window.get_placement().is_ok());
1633            assert!(window.get_class_name().is_ok());
1634            std::hint::black_box(&window.get_caption_text());
1635            #[cfg(feature = "process")]
1636            std::hint::black_box(&window.get_creator_thread_process_ids());
1637        }
1638        Ok(())
1639    }
1640
1641    fn new_window_with_class() -> io::Result<()> {
1642        const CLASS_NAME_PREFIX: &str = "myclass1";
1643        const WINDOW_NAME: &str = "mywindow1";
1644        const CAPTION_TEXT: &str = "Testwindow";
1645
1646        let icon: Rc<Icon> = Default::default();
1647        let class: WindowClass = WindowClass::register_new(
1648            CLASS_NAME_PREFIX,
1649            WindowClassAppearance {
1650                icon: Some(Rc::clone(&icon)),
1651                ..Default::default()
1652            },
1653        )?;
1654        let mut window = Window::new::<DefaultWmlType, ()>(
1655            class.into(),
1656            None,
1657            WINDOW_NAME,
1658            WindowAppearance::default(),
1659            None,
1660        )?;
1661        let notification_icon_options = NotificationIconOptions {
1662            icon,
1663            tooltip_text: Some("A tooltip!".to_string()),
1664            visible: false,
1665            ..Default::default()
1666        };
1667        let notification_icon = window.add_notification_icon(notification_icon_options)?;
1668        let balloon_notification = BalloonNotification::default();
1669        notification_icon.set_balloon_notification(Some(balloon_notification))?;
1670
1671        let window_handle = window.as_handle();
1672        assert!(!window_handle.is_visible());
1673        assert!(!window_handle.is_cloaked()?);
1674        assert_eq!(window_handle.get_caption_text(), WINDOW_NAME);
1675        window_handle.set_caption_text(CAPTION_TEXT)?;
1676        assert_eq!(window_handle.get_caption_text(), CAPTION_TEXT);
1677        assert!(dbg!(window_handle.get_class_name()?).starts_with(CLASS_NAME_PREFIX));
1678        assert!(window_handle.get_client_area_coords()?.left >= 0);
1679
1680        Ok(())
1681    }
1682}