virtual_desktop_manager/
nwg_ext.rs

1//! Extends the `nwg` crate with additional features.
2#![allow(dead_code)] // We consider this more of an external library.
3
4mod number_select;
5
6use std::{
7    any::Any,
8    borrow::Cow,
9    cell::{Cell, RefCell},
10    cmp::Ordering,
11    collections::BTreeMap,
12    mem,
13    ops::ControlFlow,
14    ptr::null_mut,
15    sync::{
16        atomic::AtomicBool,
17        mpsc::{self, RecvTimeoutError},
18        Arc, OnceLock,
19    },
20    time::{Duration, Instant},
21};
22
23use nwg::ControlHandle;
24use windows::Win32::Foundation::{HWND, RECT};
25
26pub use number_select::{NumberSelect2, NumberSelectBuilder};
27
28/// Copied from [`native_windows_gui::win32::base_helper::to_utf16`].
29pub fn to_utf16(s: &str) -> Vec<u16> {
30    use std::ffi::OsStr;
31    use std::os::windows::ffi::OsStrExt;
32
33    OsStr::new(s)
34        .encode_wide()
35        .chain(core::iter::once(0u16))
36        .collect()
37}
38/// Decode a raw utf16 string. Should be null terminated.
39///
40/// Adapted from [`native_windows_gui::win32::base_helper::from_utf16`].
41pub fn from_utf16(s: &[u16]) -> String {
42    use std::os::windows::ffi::OsStringExt;
43
44    let null_index = s.iter().position(|&i| i == 0).unwrap_or(s.len());
45    let os_string = std::ffi::OsString::from_wide(&s[0..null_index]);
46
47    os_string
48        .into_string()
49        .unwrap_or("Decoding error".to_string())
50}
51
52/// Utility for catching panics and resuming them. Useful to implement unsafe
53/// callbacks.
54pub struct PanicCatcher {
55    caught: Option<Box<dyn Any + Send + 'static>>,
56}
57impl PanicCatcher {
58    pub const fn new() -> Self {
59        Self { caught: None }
60    }
61    fn drop_without_unwind(value: Box<dyn Any + Send + 'static>) {
62        struct SafeDrop(Option<Box<dyn Any + Send + 'static>>);
63        impl Drop for SafeDrop {
64            fn drop(&mut self) {
65                while let Some(value) = self.0.take() {
66                    if let Err(payload) =
67                        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(value)))
68                    {
69                        self.0 = Some(payload);
70                    }
71                }
72            }
73        }
74        drop(SafeDrop(Some(value)));
75    }
76    pub fn has_caught_panic(&self) -> bool {
77        self.caught.is_some()
78    }
79    /// Catch panics that occur in the provided callback.
80    pub fn catch<R, F: FnOnce() -> R>(&mut self, f: F) -> Option<R> {
81        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
82            Ok(res) => Some(res),
83            Err(e) => {
84                if let Some(old) = std::mem::replace(&mut self.caught, Some(e)) {
85                    Self::drop_without_unwind(old);
86                }
87                None
88            }
89        }
90    }
91    /// Resume any panics that occurred in the callback.
92    pub fn resume_panic(&mut self) {
93        if let Some(e) = self.caught.take() {
94            std::panic::resume_unwind(e);
95        }
96    }
97}
98impl Default for PanicCatcher {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104/// Get handles to all open windows or to child windows of a specific window.
105///
106/// # References
107///
108/// - Rust library for getting titles of all open windows:
109///   <https://github.com/HiruNya/window_titles/blob/924feffac93c9ac7238d6fa5c39c1453815a0408/src/winapi.rs>
110/// - [Getting a list of all open windows in c++ and storing them - Stack
111///   Overflow](https://stackoverflow.com/questions/42589496/getting-a-list-of-all-open-windows-in-c-and-storing-them)
112/// - [java - Windows: how to get a list of all visible windows? - Stack
113///   Overflow](https://stackoverflow.com/questions/3188484/windows-how-to-get-a-list-of-all-visible-windows)
114/// - [EnumChildWindows function (winuser.h) - Win32 apps | Microsoft
115///   Learn](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumchildwindows)
116pub fn enum_child_windows<F: FnMut(HWND) -> ControlFlow<()>>(parent: Option<HWND>, f: F) {
117    use windows::Win32::{
118        Foundation::{BOOL, HWND, LPARAM},
119        UI::WindowsAndMessaging::EnumChildWindows,
120    };
121
122    struct State<F> {
123        f: F,
124        catcher: PanicCatcher,
125    }
126
127    unsafe extern "system" fn enumerate_windows<F: FnMut(HWND) -> ControlFlow<()>>(
128        window: HWND,
129        state: LPARAM,
130    ) -> BOOL {
131        let state = state.0 as *mut State<F>;
132        let state: &mut State<F> = unsafe { &mut *state };
133        let result = state
134            .catcher
135            .catch(|| (state.f)(window))
136            .unwrap_or(ControlFlow::Break(()));
137        BOOL::from(result.is_continue())
138    }
139
140    let mut state = State {
141        f,
142        catcher: PanicCatcher::new(),
143    };
144
145    unsafe {
146        let _ = EnumChildWindows(
147            parent.unwrap_or(HWND(null_mut())),
148            Some(enumerate_windows::<F>),
149            LPARAM(&mut state as *mut State<F> as isize),
150        );
151    }
152
153    state.catcher.resume_panic();
154}
155
156/// Return the index of a menu item in a parent menu.
157///
158/// Adapted from [`native_windows_gui::win32::menu::menu_index_in_parent`] (in
159/// the private `win32` module).
160pub fn menu_item_index_in_parent(item_handle: ControlHandle) -> Option<u32> {
161    if item_handle.blank() {
162        return None;
163    }
164    let (parent, id) = item_handle.hmenu_item()?;
165
166    use windows::Win32::UI::WindowsAndMessaging::{GetMenuItemCount, GetMenuItemID, HMENU};
167
168    let parent = HMENU(parent.cast());
169    let children_count = unsafe { GetMenuItemCount(parent) };
170
171    for i in 0..children_count {
172        let item_id = unsafe { GetMenuItemID(parent, i) };
173        if item_id == (-1_i32 as u32) {
174            continue;
175        } else if item_id == id {
176            return Some(i as u32);
177        }
178    }
179
180    None
181}
182
183/**
184    Return the index of a children menu/menuitem in a parent menu.
185
186    Adapted from [`native_windows_gui::win32::menu::menu_index_in_parent`] (in the private `win32` module).
187*/
188pub fn menu_index_in_parent(menu_handle: ControlHandle) -> Option<u32> {
189    if menu_handle.blank() {
190        return None;
191    }
192    let (parent, menu) = menu_handle.hmenu()?;
193
194    // Safety: we check the same preconditions as the nwg crate does when it
195    // calls this function on a menu.
196    use windows::Win32::UI::WindowsAndMessaging::{GetMenuItemCount, GetSubMenu, HMENU};
197
198    let parent = HMENU(parent.cast());
199    let children_count = unsafe { GetMenuItemCount(parent) };
200    let mut sub_menu;
201
202    for i in 0..children_count {
203        sub_menu = unsafe { GetSubMenu(parent, i) };
204        if sub_menu.0 == null_mut() {
205            continue;
206        } else if sub_menu.0 == (menu.cast()) {
207            return Some(i as u32);
208        }
209    }
210
211    None
212}
213
214/// Update the text of a submenu or menu item.
215pub fn menu_set_text(handle: ControlHandle, text: &str) {
216    if handle.blank() {
217        panic!("Unbound handle");
218    }
219    enum MenuItemInfo {
220        Position(u32),
221        Id(u32),
222    }
223    let (parent, item_info) = match handle {
224        ControlHandle::Menu(parent, _) => {
225            // Safety: the handles inside ControlHandle is valid, according to
226            // https://gabdube.github.io/native-windows-gui/native-windows-docs/extern_wrapping.html
227            // constructing new ControlHandle instances should be considered
228            // unsafe.
229            if let Some(index) = menu_index_in_parent(handle) {
230                (parent, MenuItemInfo::Position(index))
231            } else {
232                return;
233            }
234        }
235        ControlHandle::MenuItem(parent, id) => (parent, MenuItemInfo::Id(id)),
236        _ => return,
237    };
238
239    use windows::{
240        core::PWSTR,
241        Win32::UI::WindowsAndMessaging::{SetMenuItemInfoW, HMENU, MENUITEMINFOW, MIIM_STRING},
242    };
243
244    // The code below was inspired by `nwg::win32::menu::enable_menuitem`
245    // and: https://stackoverflow.com/questions/25139819/change-text-of-an-menu-item
246
247    let use_position = matches!(item_info, MenuItemInfo::Position(_));
248    let value = match item_info {
249        MenuItemInfo::Position(p) => p,
250        MenuItemInfo::Id(id) => id,
251    };
252
253    let text = to_utf16(text);
254
255    let mut info = MENUITEMINFOW::default();
256    info.cbSize = core::mem::size_of_val(&info) as u32;
257    info.fMask = MIIM_STRING;
258    info.dwTypeData = PWSTR(text.as_ptr().cast_mut());
259
260    let _ = unsafe { SetMenuItemInfoW(HMENU(parent as _), value, use_position, &info) };
261}
262
263/// Remove a submenu from its parent. Note that this is not done automatically
264/// when a menu is dropped.
265pub fn menu_remove(menu: &nwg::Menu) {
266    if menu.handle.blank() {
267        return;
268    }
269    let Some((parent, _)) = menu.handle.hmenu() else {
270        return;
271    };
272
273    let Some(index) = menu_index_in_parent(menu.handle) else {
274        return;
275    };
276
277    use windows::Win32::UI::WindowsAndMessaging::{RemoveMenu, HMENU, MF_BYPOSITION};
278
279    let _ = unsafe { RemoveMenu(HMENU(parent.cast()), index, MF_BYPOSITION) };
280}
281
282/// Finds the current context menu window using an undocumented trick.
283///
284/// Note that you can send the undocumented message `0x1e5` to the window in
285/// order to select an item, specify the item index as the `wparam`. (Leave
286/// `lparam` as 0.) Then you can activate that item (to for example open a
287/// submenu) by sending a [`WM_KEYDOWN`] message with the [`VK_RETURN`] key.
288///
289/// [`WM_KEYDOWN`]: windows::Win32::UI::WindowsAndMessaging::WM_KEYDOWN
290/// [`VK_RETURN`]: windows::Win32::UI::Input::KeyboardAndMouse::VK_RETURN
291///
292/// # References
293///
294/// - <https://microsoft.public.win32.programmer.ui.narkive.com/jQJBmxzp/open-submenu-programmatically#post6>
295///    - Which links to:
296///      <http://www.codeproject.com/menu/skinmenu.asp?df=100&forumid=14636&exp=0&select=2219867>
297pub fn find_context_menu_window() -> Option<HWND> {
298    use windows::{
299        core::PCWSTR,
300        Win32::{Foundation::HWND, UI::WindowsAndMessaging::FindWindowW},
301    };
302
303    static CONTEXT_MENU_CLASS_NAME: OnceLock<Vec<u16>> = OnceLock::new();
304    let class_name = CONTEXT_MENU_CLASS_NAME.get_or_init(|| {
305        let mut t = to_utf16("#32768");
306        t.shrink_to_fit();
307        t
308    });
309
310    let window = unsafe { FindWindowW(PCWSTR::from_raw(class_name.as_ptr()), None) }.ok()?;
311    if window == HWND::default() {
312        None
313    } else {
314        Some(window)
315    }
316}
317
318/// Check if a window is valid (not destroyed). A closed window might still be
319/// valid.
320///
321/// Adapted from [`native_windows_gui::win32::base_helper::check_hwnd`] used by
322/// many methods of [`nwg::Window`].
323pub fn window_is_valid(handle: nwg::ControlHandle) -> bool {
324    if handle.blank() {
325        return false;
326    }
327    let Some(hwnd) = handle.hwnd() else {
328        return false;
329    };
330    unsafe { windows::Win32::UI::WindowsAndMessaging::IsWindow(HWND(hwnd.cast())) }.as_bool()
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
334pub enum WindowPlacement {
335    Normal,
336    Maximized,
337    Minimized,
338}
339
340/// Retrieves the show state and the restored, minimized, and maximized positions of the specified window.
341///
342/// # References
343///
344/// - <https://learn.microsoft.com/sv-se/windows/win32/api/winuser/nf-winuser-getwindowplacement?redirectedfrom=MSDN>
345/// - <https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-windowplacement>
346pub fn window_placement(window: &nwg::Window) -> windows::core::Result<WindowPlacement> {
347    use windows::Win32::UI::WindowsAndMessaging::{
348        GetWindowPlacement, SW_SHOWMAXIMIZED, SW_SHOWMINIMIZED, SW_SHOWNORMAL, WINDOWPLACEMENT,
349    };
350
351    let handle = window.handle.hwnd().expect("Not a window handle");
352
353    let mut info = WINDOWPLACEMENT {
354        length: core::mem::size_of::<WINDOWPLACEMENT>() as u32,
355        ..WINDOWPLACEMENT::default()
356    };
357
358    unsafe { GetWindowPlacement(HWND(handle.cast()), &mut info) }.inspect_err(|e| {
359        tracing::error!(error = e.to_string(), "GetWindowPlacement failed");
360    })?;
361
362    Ok(if info.showCmd == SW_SHOWMAXIMIZED.0 as u32 {
363        WindowPlacement::Maximized
364    } else if info.showCmd == SW_SHOWMINIMIZED.0 as u32 {
365        WindowPlacement::Minimized
366    } else if info.showCmd == SW_SHOWNORMAL.0 as u32 {
367        WindowPlacement::Normal
368    } else {
369        tracing::error!(
370            showCmd = info.showCmd,
371            "Invalid return value from GetWindowPlacement"
372        );
373        WindowPlacement::Normal
374    })
375}
376
377/// Set a tray to use version 4. Shell_NotifyIcon mouse and keyboard events are
378/// handled differently than in earlier versions of Windows.
379///
380/// # References
381///
382/// - <https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw#remarks>
383/// - <https://stackoverflow.com/questions/41649303/difference-between-notifyicon-version-and-notifyicon-version-4-used-in-notifyico>
384/// - Note that the NIN_KEYSELECT event will be sent twice for the enter key:
385///   <https://github.com/openjdk/jdk/blob/master/src/java.desktop/windows/native/libawt/windows/awt_TrayIcon.cpp#L449>
386pub fn tray_set_version_4(tray: &nwg::TrayNotification) {
387    use windows::Win32::UI::Shell::{
388        Shell_NotifyIconW, NIM_SETVERSION, NOTIFYICONDATAW, NOTIFYICON_VERSION_4,
389    };
390
391    const NOT_BOUND: &str = "TrayNotification is not yet bound to a winapi object";
392    const BAD_HANDLE: &str = "INTERNAL ERROR: TrayNotification handle is not HWND!";
393
394    if tray.handle.blank() {
395        panic!("{}", NOT_BOUND);
396    }
397
398    let parent = tray.handle.tray().expect(BAD_HANDLE);
399    let mut data = NOTIFYICONDATAW {
400        hWnd: HWND(parent.cast()),
401        ..Default::default()
402    };
403    data.Anonymous.uVersion = NOTIFYICON_VERSION_4;
404    data.cbSize = mem::size_of_val(&data) as u32;
405
406    let success = unsafe { Shell_NotifyIconW(NIM_SETVERSION, &data) };
407    if !success.as_bool() {
408        tracing::error!("Failed to set tray version to 4");
409    }
410}
411
412#[inline]
413pub fn tray_get_rect(tray: &nwg::TrayNotification) -> windows::core::Result<RECT> {
414    use windows::Win32::UI::Shell::{Shell_NotifyIconGetRect, NOTIFYICONIDENTIFIER};
415
416    const NOT_BOUND: &str = "TrayNotification is not yet bound to a winapi object";
417    const BAD_HANDLE: &str = "INTERNAL ERROR: TrayNotification handle is not HWND!";
418
419    if tray.handle.blank() {
420        panic!("{}", NOT_BOUND);
421    }
422    let parent = tray.handle.tray().expect(BAD_HANDLE);
423
424    let nid = NOTIFYICONIDENTIFIER {
425        hWnd: HWND(parent.cast()),
426        cbSize: std::mem::size_of::<NOTIFYICONIDENTIFIER>() as _,
427        ..NOTIFYICONIDENTIFIER::default()
428    };
429
430    unsafe { Shell_NotifyIconGetRect(&nid) }
431}
432
433/// Sort the items in a list view. The callback is given the current indexes of
434/// list items that should be compared.
435///
436/// # [Note](https://learn.microsoft.com/en-us/windows/win32/controls/lvm-sortitemsex#remarks)
437///
438/// During the sorting process, the list-view contents are unstable. If the
439/// callback function sends any messages to the list-view control aside from
440/// LVM_GETITEM (ListView_GetItem), the results are unpredictable.
441///
442/// That message corresponds to the [`nwg::ListView::item`] function.
443///
444/// # References
445///
446/// - <https://learn.microsoft.com/en-us/windows/win32/controls/lvm-sortitemsex>
447pub fn list_view_sort_rows<F>(list_view: &nwg::ListView, f: F)
448where
449    F: FnMut(usize, usize) -> Ordering,
450{
451    use windows::Win32::{
452        Foundation::{LPARAM, LRESULT, WPARAM},
453        UI::{
454            Controls::LVM_SORTITEMSEX,
455            WindowsAndMessaging::{IsWindow, SendMessageW},
456        },
457    };
458
459    const NOT_BOUND: &str = "ListView is not yet bound to a winapi object";
460    const BAD_HANDLE: &str = "INTERNAL ERROR: ListView handle is not HWND!";
461
462    /// Adapted from [`native_windows_gui::win32::base_helper::check_hwnd`].
463    fn check_hwnd(handle: &ControlHandle, not_bound: &str, bad_handle: &str) -> HWND {
464        if handle.blank() {
465            panic!("{}", not_bound);
466        }
467        match handle.hwnd() {
468            Some(hwnd) => {
469                if unsafe { IsWindow(HWND(hwnd.cast())) }.as_bool() {
470                    HWND(hwnd.cast())
471                } else {
472                    panic!("The window handle is no longer valid. This usually means the control was freed by the OS");
473                }
474            }
475            None => {
476                panic!("{}", bad_handle);
477            }
478        }
479    }
480    let handle = check_hwnd(&list_view.handle, NOT_BOUND, BAD_HANDLE);
481
482    struct State<F> {
483        f: F,
484        catcher: PanicCatcher,
485    }
486
487    unsafe extern "system" fn compare_func<F: FnMut(usize, usize) -> Ordering>(
488        index1: LPARAM,
489        index2: LPARAM,
490        state: LPARAM,
491    ) -> LRESULT {
492        let state = state.0 as *mut State<F>;
493        let state: &mut State<F> = unsafe { &mut *state };
494        if state.catcher.has_caught_panic() {
495            return LRESULT(0);
496        }
497        let result = state
498            .catcher
499            .catch(|| (state.f)(index1.0 as usize, index2.0 as usize))
500            .unwrap_or(Ordering::Equal);
501        LRESULT(match result {
502            Ordering::Less => -1,
503            Ordering::Equal => 0,
504            Ordering::Greater => 1,
505        })
506    }
507
508    let mut state = State {
509        f,
510        catcher: PanicCatcher::new(),
511    };
512
513    unsafe {
514        SendMessageW(
515            handle,
516            LVM_SORTITEMSEX,
517            WPARAM(&mut state as *mut State<F> as usize),
518            LPARAM(compare_func::<F> as usize as isize),
519        )
520    };
521
522    state.catcher.resume_panic();
523}
524
525/// Enables or disables whether the items in a list-view control display as a
526/// group.
527///
528/// # References
529///
530/// - [ListView_EnableGroupView macro (commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-listview_enablegroupview)
531/// - [LVM_ENABLEGROUPVIEW message (Commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/controls/lvm-enablegroupview)
532pub fn list_view_enable_groups(list_view: &nwg::ListView, enable: bool) {
533    if !window_is_valid(list_view.handle) {
534        tracing::error!("Tried to toggle groups for invalid list view");
535        return;
536    }
537    let Some(handle) = list_view.handle.hwnd() else {
538        tracing::error!("Tried to toggle groups for invalid list view");
539        return;
540    };
541    let result = unsafe {
542        windows::Win32::UI::WindowsAndMessaging::SendMessageW(
543            HWND(handle.cast()),
544            windows::Win32::UI::Controls::LVM_ENABLEGROUPVIEW,
545            windows::Win32::Foundation::WPARAM(enable as usize),
546            windows::Win32::Foundation::LPARAM(0),
547        )
548    };
549    match result.0 {
550        0 => tracing::trace!(
551            "Groups in list view was already {}",
552            if enable { "enabled" } else { "disabled" }
553        ),
554        1 => tracing::trace!(
555            "Groups in list view was successfully {}",
556            if enable { "enabled" } else { "disabled" }
557        ),
558        -1 => tracing::error!("Failed to enable/disable groups in list view"),
559        _ => {
560            tracing::error!(result =? result.0, "Unexpected return value when toggling groups in list view")
561        }
562    }
563}
564
565#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
566pub enum ListViewGroupAlignment {
567    #[default]
568    Left,
569    Center,
570    Right,
571}
572
573#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
574pub struct ListViewGroupInfo<'a> {
575    pub group_id: i32,
576    /// Determines if a new group is created or if an existing group should be
577    /// updated.
578    pub create_new: bool,
579    pub header: Option<Cow<'a, str>>,
580    /// This element is drawn under the header text.
581    pub subtitle: Option<Cow<'a, str>>,
582    pub footer: Option<Cow<'a, str>>,
583    /// This item is drawn right-aligned opposite the header text. When clicked
584    /// by the user, the task link generates an `LVN_LINKCLICK` notification.
585    pub task: Option<Cow<'a, str>>,
586    /// This item is drawn opposite the title image when there is a title image,
587    /// no extended image, and header is centered aligned.
588    pub description_top: Option<Cow<'a, str>>,
589    /// This item is drawn under the top description text when there is a title
590    /// image, no extended image, and header is center aligned.
591    pub description_bottom: Option<Cow<'a, str>>,
592    pub header_alignment: Option<ListViewGroupAlignment>,
593    /// If this is specified then the `header_alignment` should also be specified.
594    pub footer_alignment: Option<ListViewGroupAlignment>,
595    /// If the group is collapsed/expanded.
596    pub collapsed: Option<bool>,
597    /// The group is hidden.
598    pub hidden: Option<bool>,
599    /// The group does not display a header.
600    pub no_header: Option<bool>,
601    /// The group can be collapsed.
602    pub collapsible: Option<bool>,
603    /// The group has keyboard focus.
604    pub focused: Option<bool>,
605    /// The group is selected.
606    pub selected: Option<bool>,
607    /// The group displays only a portion of its items.
608    pub subseted: Option<bool>,
609    /// The subset link of the group has keyboard focus.
610    pub subset_link_focused: Option<bool>,
611}
612
613/// Create or update a group inside a list view.
614///
615/// Adapted from [`native_windows_gui::ListView::update_item`].
616///
617/// # References
618///
619/// - [How to Use Groups in a List-View - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/controls/use-groups-in-a-list-view)
620/// - [LVM_SETGROUPINFO message (Commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/controls/lvm-setgroupinfo)
621/// - [LVM_INSERTGROUP message (Commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/controls/lvm-insertgroup)
622/// - [LVGROUP (commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-lvgroup)
623/// - [Discussion/Proposal: more ListViewGroup functionality · Issue #2623 · dotnet/winforms · GitHub](https://github.com/dotnet/winforms/issues/2623)
624///    - This also links to some source code that sends the above messages.
625/// - [Grouping in List View Controls - CodeProject](https://www.codeproject.com/Questions/415005/Grouping-in-List-View-Controls)
626pub fn list_view_set_group_info(list_view: &nwg::ListView, info: ListViewGroupInfo) {
627    use windows::{
628        core::PWSTR,
629        Win32::{
630            Foundation::{LPARAM, WPARAM},
631            UI::{
632                Controls::{
633                    LVGA_FOOTER_CENTER, LVGA_FOOTER_LEFT, LVGA_FOOTER_RIGHT, LVGA_HEADER_CENTER,
634                    LVGA_HEADER_LEFT, LVGA_HEADER_RIGHT, LVGF_ALIGN, LVGF_DESCRIPTIONBOTTOM,
635                    LVGF_DESCRIPTIONTOP, LVGF_FOOTER, LVGF_GROUPID, LVGF_HEADER, LVGF_NONE,
636                    LVGF_STATE, LVGF_SUBTITLE, LVGF_TASK, LVGROUP, LVGS_COLLAPSED,
637                    LVGS_COLLAPSIBLE, LVGS_FOCUSED, LVGS_HIDDEN, LVGS_NOHEADER, LVGS_SELECTED,
638                    LVGS_SUBSETED, LVGS_SUBSETLINKFOCUSED, LVM_INSERTGROUP, LVM_SETGROUPINFO,
639                },
640                WindowsAndMessaging::SendMessageW,
641            },
642        },
643    };
644
645    if !window_is_valid(list_view.handle) {
646        tracing::error!("Tried to create/update group for invalid list view");
647        return;
648    }
649    let Some(handle) = list_view.handle.hwnd() else {
650        tracing::error!("Tried to create/update group for invalid list view");
651        return;
652    };
653    let mut item = LVGROUP {
654        cbSize: core::mem::size_of::<LVGROUP>() as u32,
655        mask: if info.create_new {
656            LVGF_GROUPID
657        } else {
658            LVGF_NONE
659        },
660        iGroupId: info.group_id,
661        ..Default::default()
662    };
663
664    /// # Safety
665    ///
666    /// Can't be used inside a nested block or the generated buffer will free the
667    /// string early.
668    macro_rules! set_str {
669        (Options {
670            mask: $mask:expr,
671            input: $input:expr,
672            str_field: $str:ident,
673        }) => {
674            let mut __temp_buffer;
675            if let Some(text_utf8) = $input {
676                item.mask |= $mask;
677                __temp_buffer = to_utf16(text_utf8);
678                item.$str = PWSTR::from_raw(__temp_buffer.as_mut_ptr());
679            }
680        };
681    }
682
683    set_str!(Options {
684        mask: LVGF_HEADER,
685        input: &info.header,
686        str_field: pszHeader,
687    });
688    set_str!(Options {
689        mask: LVGF_FOOTER,
690        input: &info.footer,
691        str_field: pszFooter,
692    });
693    set_str!(Options {
694        mask: LVGF_SUBTITLE,
695        input: &info.subtitle,
696        str_field: pszSubtitle,
697    });
698    set_str!(Options {
699        mask: LVGF_TASK,
700        input: &info.task,
701        str_field: pszTask,
702    });
703    set_str!(Options {
704        mask: LVGF_DESCRIPTIONTOP,
705        input: &info.description_top,
706        str_field: pszDescriptionTop,
707    });
708    set_str!(Options {
709        mask: LVGF_DESCRIPTIONBOTTOM,
710        input: &info.description_bottom,
711        str_field: pszDescriptionBottom,
712    });
713
714    if info.header_alignment.is_some() || info.footer_alignment.is_some() {
715        item.mask |= LVGF_ALIGN;
716    }
717    if let Some(header_align) = info.header_alignment {
718        item.uAlign = match header_align {
719            ListViewGroupAlignment::Left => LVGA_HEADER_LEFT,
720            ListViewGroupAlignment::Center => LVGA_HEADER_CENTER,
721            ListViewGroupAlignment::Right => LVGA_HEADER_RIGHT,
722        };
723    } else if info.footer_alignment.is_some() {
724        item.uAlign = LVGA_HEADER_LEFT;
725    }
726    if let Some(footer_align) = info.footer_alignment {
727        item.uAlign |= match footer_align {
728            ListViewGroupAlignment::Left => LVGA_FOOTER_LEFT,
729            ListViewGroupAlignment::Center => LVGA_FOOTER_CENTER,
730            ListViewGroupAlignment::Right => LVGA_FOOTER_RIGHT,
731        };
732    }
733
734    let state_flags = [
735        (info.collapsed, LVGS_COLLAPSED),
736        (info.hidden, LVGS_HIDDEN),
737        (info.no_header, LVGS_NOHEADER),
738        (info.collapsible, LVGS_COLLAPSIBLE),
739        (info.focused, LVGS_FOCUSED),
740        (info.selected, LVGS_SELECTED),
741        (info.subseted, LVGS_SUBSETED),
742        (info.subset_link_focused, LVGS_SUBSETLINKFOCUSED),
743    ];
744    if state_flags.iter().any(|(v, _)| v.is_some()) {
745        item.mask |= LVGF_STATE;
746        for (value, flag) in state_flags {
747            if let Some(value) = value {
748                item.stateMask |= flag;
749                if value {
750                    item.state |= flag;
751                }
752            }
753        }
754    }
755
756    let res = unsafe {
757        if info.create_new {
758            SendMessageW(
759                HWND(handle.cast()),
760                LVM_INSERTGROUP,
761                // Index where the group is to be added. If this is -1, the group is added at the end of the list.
762                WPARAM((-1_isize) as usize),
763                LPARAM(&mut item as *mut LVGROUP as _),
764            )
765        } else {
766            SendMessageW(
767                HWND(handle.cast()),
768                LVM_SETGROUPINFO,
769                WPARAM(info.group_id as _),
770                LPARAM(&mut item as *mut LVGROUP as _),
771            )
772        }
773    };
774    if res.0 == -1 {
775        tracing::error!(
776            create_new = info.create_new,
777            "Failed to create or update group for list view"
778        );
779    }
780    // Note: res contains the id of the group.
781}
782
783/// Set the group that a list view item belongs to.
784///
785/// Adapted from [`native_windows_gui::ListView::update_item`].
786///
787/// # References
788///
789/// - [How to Use Groups in a List-View - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/controls/use-groups-in-a-list-view)
790/// - [LVITEMW (commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-lvitemw)
791/// - [LVM_SETITEM message (Commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/controls/lvm-setitem)
792pub fn list_view_item_set_group_id(
793    list_view: &nwg::ListView,
794    row_index: usize,
795    group_id: Option<i32>,
796) {
797    use windows::Win32::{
798        Foundation::{LPARAM, WPARAM},
799        UI::{
800            Controls::{I_GROUPIDNONE, LVIF_GROUPID, LVITEMW, LVM_SETITEMW},
801            WindowsAndMessaging::SendMessageW,
802        },
803    };
804
805    if !list_view.has_item(row_index, 0) {
806        tracing::error!(
807            row_index,
808            "Tried to set group id for row that didn't exist."
809        );
810        return;
811    }
812    if !window_is_valid(list_view.handle) {
813        tracing::error!("Tried to set group id for item inside invalid list view");
814        return;
815    }
816    let Some(handle) = list_view.handle.hwnd() else {
817        tracing::error!("Tried to set group id for item inside invalid list view");
818        return;
819    };
820    let mut item = LVITEMW {
821        mask: LVIF_GROUPID,
822        iGroupId: group_id.unwrap_or(I_GROUPIDNONE.0),
823        iItem: row_index as _,
824        iSubItem: 0,
825        ..Default::default()
826    };
827
828    let res = unsafe {
829        SendMessageW(
830            HWND(handle.cast()),
831            LVM_SETITEMW,
832            WPARAM(0),
833            LPARAM(&mut item as *mut LVITEMW as _),
834        )
835    };
836    if res.0 == 0 {
837        tracing::error!("Failed to set group id for list view item");
838    }
839}
840
841/// Get the group that a list view item belongs to.
842///
843/// Adapted from [`native_windows_gui::ListView::item`].
844///
845/// # References
846///
847/// - [How to Use Groups in a List-View - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/controls/use-groups-in-a-list-view)
848/// - [LVITEMW (commctrl.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-lvitemw)
849/// - <https://learn.microsoft.com/en-us/windows/win32/controls/lvm-getitem>
850pub fn list_view_item_get_group_id(list_view: &nwg::ListView, row_index: usize) -> i32 {
851    use windows::Win32::{
852        Foundation::{LPARAM, WPARAM},
853        UI::{
854            Controls::{I_GROUPIDNONE, LVIF_GROUPID, LVITEMW, LVM_GETITEMW},
855            WindowsAndMessaging::SendMessageW,
856        },
857    };
858
859    if !window_is_valid(list_view.handle) {
860        tracing::error!("Tried to set group id for item inside invalid list view");
861        return I_GROUPIDNONE.0;
862    }
863    let Some(handle) = list_view.handle.hwnd() else {
864        tracing::error!("Tried to set group id for item inside invalid list view");
865        return I_GROUPIDNONE.0;
866    };
867    let mut item = LVITEMW {
868        mask: LVIF_GROUPID,
869        iItem: row_index as _,
870        iSubItem: 0,
871        ..Default::default()
872    };
873
874    let res = unsafe {
875        SendMessageW(
876            HWND(handle.cast()),
877            LVM_GETITEMW,
878            WPARAM(0),
879            LPARAM(&mut item as *mut LVITEMW as _),
880        )
881    };
882    if res.0 == 0 {
883        // Item not found
884        I_GROUPIDNONE.0
885    } else {
886        item.iGroupId
887    }
888}
889
890/// When the taskbar is created, it registers a message with the
891/// "TaskbarCreated" string and then broadcasts this message to all top-level
892/// windows When the application receives this message, it should assume that
893/// any taskbar icons it added have been removed and add them again.
894///
895/// # Reference
896///
897/// - Code copied from: [tray-icon/src/platform_impl/windows/mod.rs at
898///   3c75d9031a915c108cc1886121b9b84cb9c8c312 ·
899///   tauri-apps/tray-icon](https://github.com/tauri-apps/tray-icon/blob/3c75d9031a915c108cc1886121b9b84cb9c8c312/src/platform_impl/windows/mod.rs#L45-L48)
900pub fn windows_msg_for_explorer_restart() -> u32 {
901    static TASKBAR_RESTART_MSG: OnceLock<u32> = OnceLock::new();
902    *TASKBAR_RESTART_MSG.get_or_init(|| {
903        let msg = unsafe {
904            windows::Win32::UI::WindowsAndMessaging::RegisterWindowMessageA(windows::core::s!(
905                "TaskbarCreated"
906            ))
907        };
908        if msg == 0 {
909            tracing::error!(
910                error = ?windows::core::Error::from_win32(),
911                "Called \"RegisterWindowMessageA\" with \"TaskbarCreated\" and failed!"
912            );
913        } else {
914            tracing::debug!(
915                msg = ?msg,
916                "Called \"RegisterWindowMessageA\" with \"TaskbarCreated\" and succeeded"
917            );
918        }
919        msg
920    })
921}
922
923/// A modified version of [`nwg::MessageWindow`] that allows detecting if
924/// `explorer.exe` has restarted.
925///
926/// This requires creating the window with certain flags, see:
927/// [tray-icon/src/platform_impl/windows/mod.rs at
928/// 3c75d9031a915c108cc1886121b9b84cb9c8c312 ·
929/// tauri-apps/tray-icon](https://github.com/tauri-apps/tray-icon/blob/3c75d9031a915c108cc1886121b9b84cb9c8c312/src/platform_impl/windows/mod.rs#L91-L112)
930#[derive(Default, PartialEq, Eq)]
931pub struct TrayWindow {
932    pub handle: ControlHandle,
933}
934impl TrayWindow {
935    pub fn builder() -> TrayWindowBuilder {
936        TrayWindowBuilder {}
937    }
938}
939impl Drop for TrayWindow {
940    fn drop(&mut self) {
941        self.handle.destroy();
942    }
943}
944impl<'a> From<&'a TrayWindow> for nwg::ControlHandle {
945    fn from(value: &'a TrayWindow) -> Self {
946        value.handle
947    }
948}
949/// Can use this component as a partial GUI as a workaround for the
950/// [`nwd::NwgPartial`] derive macro's requirement that unknown controls must
951/// have a parent.
952impl nwg::PartialUi for TrayWindow {
953    fn build_partial<W: Into<ControlHandle>>(
954        data: &mut Self,
955        parent: Option<W>,
956    ) -> Result<(), nwg::NwgError> {
957        let mut b = Self::builder();
958        if let Some(p) = parent {
959            b = b.parent(p.into());
960        }
961        b.build(data)
962    }
963
964    fn handles(&self) -> Vec<&ControlHandle> {
965        vec![&self.handle]
966    }
967}
968
969#[non_exhaustive]
970pub struct TrayWindowBuilder {}
971
972impl TrayWindowBuilder {
973    pub fn parent<C: Into<ControlHandle>>(self, _p: C) -> Self {
974        // self.parent = Some(p.into());
975        self
976    }
977    pub fn build(self, out: &mut TrayWindow) -> Result<(), nwg::NwgError> {
978        use windows::Win32::UI::WindowsAndMessaging::*;
979
980        *out = Default::default();
981        out.handle = nwg::ControlBase::build_hwnd()
982            .class_name(nwg::Window::class_name(&Default::default()))
983            .ex_flags(
984                // Same styles as the tray-icon crate, see:
985                // https://github.com/tauri-apps/tray-icon/blob/9231438b895055dddaf817dc44f680988c3d3c90/src/platform_impl/windows/mod.rs#L99-L106
986                WS_EX_NOACTIVATE.0 | WS_EX_TRANSPARENT.0 | WS_EX_LAYERED.0 |
987                // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which
988                // we want to avoid. If you remove this style, this window won't show up in the
989                // taskbar *initially*, but it can show up at some later point. This can sometimes
990                // happen on its own after several hours have passed, although this has proven
991                // difficult to reproduce. Alternatively, it can be manually triggered by killing
992                // `explorer.exe` and then starting the process back up.
993                // It is unclear why the bug is triggered by waiting for several hours.
994                WS_EX_TOOLWINDOW.0,
995            )
996            .flags(WS_OVERLAPPED.0)
997            .size((CW_USEDEFAULT, 0))
998            .position((CW_USEDEFAULT, 0))
999            .text("")
1000            .build()?;
1001
1002        Ok(())
1003    }
1004}
1005
1006pub trait LazyUiHooks {
1007    fn set_parent(&mut self, _parent: Option<ControlHandle>) {}
1008    /// Build this type when the `nwg::PartialUi::build_partial` method is
1009    /// called on `LazyUi`. (Defaults to `false`.)
1010    fn eager_build(&mut self) -> bool {
1011        false
1012    }
1013}
1014/// Implements `PartialUi` and delegates to a `PartialUi` inside a `RefCell`.
1015///
1016/// Note: if this is located inside a `PartialUI` then any parent passed to that
1017/// partial UI won't be passed down one step more to this type, and so the
1018/// initial parent will be set to `None`.
1019#[derive(Default)]
1020pub struct LazyUi<T> {
1021    pub ui: RefCell<T>,
1022    pub latest_parent: Cell<Option<ControlHandle>>,
1023    pub is_built: Cell<bool>,
1024}
1025impl<T> LazyUi<T> {
1026    /// Reset the wrapped UI to its default state.
1027    pub fn clear(&self)
1028    where
1029        T: Default,
1030    {
1031        *self.ui.borrow_mut() = T::default();
1032        self.is_built.set(false);
1033    }
1034    /// Build the UI with the most recently used parent. Make sure the UI isn't
1035    /// already constructed when calling this method, perhaps by calling `clear`
1036    /// first.
1037    pub fn build_with_latest_parent(&self) -> Result<(), nwg::NwgError>
1038    where
1039        T: LazyUiHooks + nwg::PartialUi,
1040    {
1041        self.build(self.latest_parent.get())
1042    }
1043    /// Build the UI with the given parent. Make sure the UI isn't already
1044    /// constructed when calling this method, perhaps by calling `clear` first.
1045    pub fn build(&self, parent: Option<ControlHandle>) -> Result<(), nwg::NwgError>
1046    where
1047        T: LazyUiHooks + nwg::PartialUi,
1048    {
1049        let mut this = self.ui.borrow_mut();
1050        self.latest_parent.set(parent);
1051        this.set_parent(parent);
1052        <T as nwg::PartialUi>::build_partial(&mut *this, parent)?;
1053        self.is_built.set(true);
1054        Ok(())
1055    }
1056}
1057impl<T> nwg::PartialUi for LazyUi<T>
1058where
1059    T: LazyUiHooks + nwg::PartialUi,
1060{
1061    fn build_partial<W: Into<ControlHandle>>(
1062        data: &mut Self,
1063        parent: Option<W>,
1064    ) -> Result<(), nwg::NwgError> {
1065        let parent = parent.map(Into::into);
1066        data.latest_parent.set(parent);
1067        data.ui.get_mut().set_parent(parent);
1068        if data.ui.get_mut().eager_build() {
1069            nwg::PartialUi::build_partial(data.ui.get_mut(), parent)?;
1070        }
1071        Ok(())
1072    }
1073
1074    fn process_event(&self, evt: nwg::Event, evt_data: &nwg::EventData, handle: ControlHandle) {
1075        match self.ui.try_borrow() {
1076            Ok(ui) => ui.process_event(evt, evt_data, handle),
1077            // Events can be sent while we are constructing the inner UI, since
1078            // we can't clone `EventData` we just ignore them:
1079            Err(e) => tracing::error!(
1080                "Failed to process event {evt:?} on a `LazyUi<{}>` type: {e}",
1081                std::any::type_name::<T>()
1082            ),
1083        }
1084    }
1085
1086    fn handles(&self) -> Vec<&'_ ControlHandle> {
1087        vec![]
1088    }
1089}
1090impl<T> core::ops::Deref for LazyUi<T> {
1091    type Target = RefCell<T>;
1092    fn deref(&self) -> &Self::Target {
1093        &self.ui
1094    }
1095}
1096impl<T> core::ops::DerefMut for LazyUi<T> {
1097    fn deref_mut(&mut self) -> &mut Self::Target {
1098        &mut self.ui
1099    }
1100}
1101
1102/// Can be used as a `nwg_control` to store the parent that a `PartialUi` is
1103/// created with. This control can then be referred to as the parent of another
1104/// control and that will simply forward the captured parent. (Make sure this
1105/// capture control is placed before any control that uses it as a parent.)
1106#[derive(Default, PartialEq, Eq)]
1107pub struct ParentCapture {
1108    pub captured_parent: Option<ControlHandle>,
1109}
1110impl ParentCapture {
1111    pub fn builder() -> ParentCaptureBuilder {
1112        ParentCaptureBuilder(Self::default())
1113    }
1114    pub fn handle(&self) -> ControlHandle {
1115        self.captured_parent.unwrap_or(ControlHandle::NoHandle)
1116    }
1117}
1118pub struct ParentCaptureBuilder(ParentCapture);
1119impl ParentCaptureBuilder {
1120    pub fn parent<C: Into<ControlHandle>>(mut self, p: C) -> Self {
1121        self.0.captured_parent = Some(p.into());
1122        self
1123    }
1124    pub fn build(self, out: &mut ParentCapture) -> Result<(), nwg::NwgError> {
1125        *out = self.0;
1126        Ok(())
1127    }
1128}
1129impl From<&ParentCapture> for ControlHandle {
1130    fn from(control: &ParentCapture) -> Self {
1131        control.handle()
1132    }
1133}
1134impl From<&mut ParentCapture> for ControlHandle {
1135    fn from(control: &mut ParentCapture) -> Self {
1136        control.handle()
1137    }
1138}
1139impl PartialEq<ControlHandle> for ParentCapture {
1140    fn eq(&self, other: &ControlHandle) -> bool {
1141        self.handle() == *other
1142    }
1143}
1144impl PartialEq<ParentCapture> for ControlHandle {
1145    fn eq(&self, other: &ParentCapture) -> bool {
1146        *self == other.handle()
1147    }
1148}
1149
1150/// Uses a single thread to serve multiple sleep requests.
1151pub struct TimerThread {
1152    join_handle: std::thread::JoinHandle<()>,
1153    send_time_request: mpsc::Sender<(Instant, Box<dyn FnOnce() + Send + 'static>)>,
1154}
1155impl TimerThread {
1156    pub fn new() -> Self {
1157        let (tx, rx) = mpsc::channel();
1158        let join_handle = std::thread::Builder::new()
1159            .name("TimerThread".to_string())
1160            .spawn(move || Self::background_work(rx))
1161            .expect("Failed to spawn timer thread");
1162        Self {
1163            join_handle,
1164            send_time_request: tx,
1165        }
1166    }
1167    /// Call a function and catch all potential panics.
1168    fn safe_call(f: impl FnOnce()) {
1169        /// Dropping a value might panic, so we catch that in a custom Drop.
1170        struct SafeDrop(Option<Box<dyn std::any::Any + Send>>);
1171        impl Drop for SafeDrop {
1172            fn drop(&mut self) {
1173                while let Some(value) = self.0.take() {
1174                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1175                        drop(value);
1176                    }));
1177                    match result {
1178                        Ok(()) => {}
1179                        Err(e) => {
1180                            self.0 = Some(e);
1181                        }
1182                    }
1183                }
1184            }
1185        }
1186        if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
1187            drop(SafeDrop(Some(e)));
1188        }
1189    }
1190    fn background_work(rx: mpsc::Receiver<(Instant, Box<dyn FnOnce() + Send + 'static>)>) {
1191        let mut times = BTreeMap::<Instant, Box<dyn FnOnce() + Send + 'static>>::new();
1192        loop {
1193            let (new_time, f) = if let Some(first_time) = times.first_entry() {
1194                let sleep_to = first_time.key();
1195                let Some(timeout) = sleep_to.checked_duration_since(Instant::now()) else {
1196                    let f = first_time.remove();
1197                    Self::safe_call(f);
1198                    continue;
1199                };
1200                match rx.recv_timeout(timeout) {
1201                    Ok(msg) => msg,
1202                    Err(RecvTimeoutError::Disconnected) => {
1203                        // No more messages, finish waiting for existing messages:
1204                        for (sleep_to, f) in times.into_iter() {
1205                            if let Some(timeout) = sleep_to.checked_duration_since(Instant::now()) {
1206                                std::thread::sleep(timeout);
1207                            }
1208                            Self::safe_call(f);
1209                        }
1210                        break;
1211                    }
1212                    Err(RecvTimeoutError::Timeout) => {
1213                        let f = first_time.remove();
1214                        Self::safe_call(f);
1215                        continue;
1216                    }
1217                }
1218            } else {
1219                match rx.recv() {
1220                    Ok(msg) => msg,
1221                    Err(mpsc::RecvError) => break,
1222                }
1223            };
1224            times.insert(new_time, f);
1225        }
1226    }
1227    pub fn notify_at(&self, time: Instant, f: impl FnOnce() + Send + 'static) {
1228        self.send_time_request
1229            .send((time, Box::new(f)))
1230            .expect("Background timer thread has exited");
1231    }
1232    /// Notify a waker when the time has occurred. Sets `Err(true)` for inside
1233    /// the `Mutex` after the time has elapsed, so there is no point to queue a
1234    /// waker there after that.
1235    fn notify_waker(
1236        &self,
1237        time: Instant,
1238        waker: Arc<std::sync::Mutex<Result<std::task::Waker, bool>>>,
1239    ) {
1240        self.notify_at(time, move || {
1241            let result = std::mem::replace(&mut *waker.lock().unwrap(), Err(true));
1242            if let Ok(waker) = result {
1243                waker.wake();
1244            }
1245        })
1246    }
1247    pub fn delay_future(&self, delay: Duration) -> impl std::future::Future<Output = ()> {
1248        self.notify_future(Instant::now() + delay)
1249    }
1250    /// Create a future that will become ready at the specified time.
1251    pub fn notify_future(&self, time: Instant) -> impl std::future::Future<Output = ()> {
1252        struct WaitFut(Arc<std::sync::Mutex<Result<std::task::Waker, bool>>>);
1253        impl std::future::Future for WaitFut {
1254            type Output = ();
1255
1256            fn poll(
1257                self: std::pin::Pin<&mut Self>,
1258                cx: &mut std::task::Context<'_>,
1259            ) -> std::task::Poll<Self::Output> {
1260                let this = &mut self.get_mut().0;
1261                let mut guard = this.lock().unwrap();
1262                if let &Err(true) = &*guard {
1263                    std::task::Poll::Ready(())
1264                } else {
1265                    *guard = Ok(cx.waker().clone());
1266                    std::task::Poll::Pending
1267                }
1268            }
1269        }
1270        let shared = Arc::new(std::sync::Mutex::new(Err(false)));
1271        self.notify_waker(time, shared.clone());
1272        WaitFut(shared)
1273    }
1274    pub fn get_global() -> &'static Self {
1275        static GLOBAL: OnceLock<TimerThread> = OnceLock::new();
1276        GLOBAL.get_or_init(Self::new)
1277    }
1278}
1279impl Default for TimerThread {
1280    fn default() -> Self {
1281        Self::new()
1282    }
1283}
1284
1285/// An alternative to [`nwg::AnimationTimer`] that has less CPU usage.
1286///
1287/// Note: this is a [`nwg::PartialUi`] instead of a control because it needs to
1288/// listen to events.
1289///
1290/// # Examples
1291///
1292/// ```rust
1293/// extern crate native_windows_derive as nwd;
1294/// extern crate native_windows_gui as nwg;
1295///
1296/// use virtual_desktop_manager::nwg_ext::{FastTimer, ParentCapture};
1297/// use std::time::Duration;
1298///
1299/// #[derive(nwd::NwgPartial, Default)]
1300/// struct MyUi {
1301///     /// Captures the parent that this partial UI is instantiated with.
1302///     #[nwg_control]
1303///     capture: ParentCapture,
1304///
1305///     #[nwg_partial(parent: capture)]
1306///     #[nwg_events((notice, OnNotice): [Self::on_tick])]
1307///     my_timer: FastTimer,
1308/// }
1309/// impl MyUi {
1310///     pub fn start_interval(&self) {
1311///         self.my_timer.start_interval(Duration::from_millis(100));
1312///     }
1313///     pub fn on_tick(&self) {
1314///         // Do something every 100 milliseconds...
1315///     }
1316/// }
1317///
1318///# fn main() {}
1319/// ```
1320#[derive(nwd::NwgPartial)]
1321pub struct FastTimer {
1322    #[nwg_control]
1323    #[nwg_events( OnNotice: [Self::on_notice] )]
1324    pub notice: nwg::Notice,
1325    callback: RefCell<Box<dyn Fn() + 'static>>,
1326    cancel_latest: RefCell<Arc<AtomicBool>>,
1327    /// `Some` if an interval is configured in which case the duration between
1328    /// ticks is stored as well as when the next tick was scheduled.
1329    interval_config: Cell<Option<(Duration, Instant)>>,
1330}
1331impl FastTimer {
1332    pub fn set_callback(&self, callback: impl Fn() + 'static) {
1333        *self.callback.borrow_mut() = Box::new(callback);
1334    }
1335    /// This will cancel any queued timeout or interval.
1336    pub fn cancel_last(&self) {
1337        let cancel_latest = self.cancel_latest.borrow();
1338        cancel_latest.store(true, std::sync::atomic::Ordering::Release);
1339        self.interval_config.set(None);
1340    }
1341    pub fn notify_after(&self, duration: Duration) {
1342        self.notify_at(
1343            Instant::now()
1344                .checked_add(duration)
1345                .expect("Time is out of bounds"),
1346        );
1347    }
1348    pub fn notify_at(&self, time_to_notify_at: Instant) {
1349        let sender = self.notice.sender();
1350        let canceled = {
1351            let mut cancel_latest = self.cancel_latest.borrow_mut();
1352            cancel_latest.store(true, std::sync::atomic::Ordering::Release);
1353            let canceled = Arc::new(AtomicBool::new(false));
1354            *cancel_latest = canceled.clone();
1355            canceled
1356        };
1357        TimerThread::get_global().notify_at(time_to_notify_at, move || {
1358            if !canceled.load(std::sync::atomic::Ordering::Acquire) {
1359                sender.notice();
1360            }
1361        })
1362    }
1363    pub fn start_interval(&self, between_ticks: Duration) {
1364        let target_time = Instant::now() + between_ticks;
1365        self.interval_config.set(Some((between_ticks, target_time)));
1366        self.notify_at(target_time);
1367    }
1368    fn on_notice(&self) {
1369        if let Some((between_ticks, target_time)) = self.interval_config.get() {
1370            let mut new_target = target_time + between_ticks;
1371            let now = Instant::now();
1372            if new_target < now {
1373                // System might have been asleep or something, just restart
1374                // interval from current time.
1375                new_target = now + between_ticks;
1376            }
1377            self.notify_at(new_target);
1378        }
1379        self.callback.borrow()();
1380    }
1381}
1382impl Default for FastTimer {
1383    fn default() -> Self {
1384        Self {
1385            notice: Default::default(),
1386            callback: RefCell::new(Box::new(|| {})),
1387            cancel_latest: RefCell::new(Arc::new(AtomicBool::new(false))),
1388            interval_config: Cell::new(None),
1389        }
1390    }
1391}
1392impl Drop for FastTimer {
1393    fn drop(&mut self) {
1394        self.cancel_last();
1395    }
1396}
1397
1398/// An alternative to [`nwg::AnimationTimer`] that has less CPU usage.
1399///
1400/// # Examples
1401///
1402/// ```rust,no_run
1403/// extern crate native_windows_gui as nwg;
1404/// extern crate native_windows_derive as nwd;
1405///
1406/// use nwd::{NwgUi, NwgPartial};
1407/// use nwg::NativeUi;
1408/// use virtual_desktop_manager::nwg_ext::FastTimerControl;
1409/// use std::time::Duration;
1410///
1411/// #[derive(NwgUi, Default)]
1412/// pub struct MyUi {
1413///     #[nwg_control]
1414///     window: nwg::MessageWindow,
1415///
1416///     #[nwg_control(interval: Duration::from_millis(25))]
1417///     #[nwg_events(OnNotice: [Self::on_tick])]
1418///     my_timer: FastTimerControl,
1419///
1420///     count: std::cell::Cell<u32>,
1421///
1422///     #[nwg_partial(parent: window)]
1423///     sub: MySubUi,
1424/// }
1425/// impl MyUi {
1426///     pub fn on_tick(&self) {
1427///         // Do something every 2 milliseconds...
1428///         self.count.set(self.count.get() + 1);
1429///     }
1430/// }
1431///
1432/// #[derive(NwgPartial, Default)]
1433/// struct MySubUi {
1434///     #[nwg_control(interval: Duration::from_millis(110))]
1435///     #[nwg_events(OnNotice: [Self::on_sub_tick])]
1436///     my_sub_timer: FastTimerControl,
1437/// }
1438/// impl MySubUi {
1439///     pub fn on_sub_tick(&self) {
1440///         // Do something every 10 milliseconds...
1441///         nwg::stop_thread_dispatch();
1442///     }
1443/// }
1444///
1445/// fn main() {
1446///     nwg::init().expect("Failed to init Native Windows GUI");
1447///     let ui = MyUi::build_ui(Default::default()).expect("Failed to build UI");
1448///     nwg::dispatch_thread_events();
1449///     assert_eq!(ui.count.get(), 4);
1450/// }
1451/// ```
1452#[derive(Default)]
1453pub struct FastTimerControl {
1454    pub notice: nwg::Notice,
1455    is_last_active: RefCell<Arc<AtomicBool>>,
1456}
1457impl FastTimerControl {
1458    pub fn builder() -> FastTimerControlBuilder {
1459        FastTimerControlBuilder {
1460            parent: None,
1461            interval: None,
1462        }
1463    }
1464
1465    /// True if the timer is waiting for the next timeout or interval. This
1466    /// means that an [`nwg::Event::OnNotice`] event will be emitted in the
1467    /// future unless the timer is canceled.
1468    pub fn is_waiting(&self) -> bool {
1469        self.is_last_active
1470            .borrow()
1471            .load(std::sync::atomic::Ordering::Acquire)
1472    }
1473    /// This will cancel any queued timeout or interval.
1474    pub fn cancel_last(&self) {
1475        let last_active = self.is_last_active.borrow();
1476        last_active.store(false, std::sync::atomic::Ordering::Release);
1477    }
1478    fn new_enable_signal(&self) -> Arc<AtomicBool> {
1479        let mut last_active = self.is_last_active.borrow_mut();
1480        last_active.store(false, std::sync::atomic::Ordering::Release);
1481        let new_active = Arc::new(AtomicBool::new(true));
1482        *last_active = new_active.clone();
1483        new_active
1484    }
1485
1486    pub fn notify_after(&self, duration: Duration) {
1487        self.notify_at(
1488            Instant::now()
1489                .checked_add(duration)
1490                .expect("Time is out of bounds"),
1491        );
1492    }
1493    pub fn notify_at(&self, time_to_notify_at: Instant) {
1494        let sender = self.notice.sender();
1495        let is_active = self.new_enable_signal();
1496        TimerThread::get_global().notify_at(time_to_notify_at, move || {
1497            if is_active.load(std::sync::atomic::Ordering::Acquire) {
1498                is_active.store(false, std::sync::atomic::Ordering::Release);
1499                sender.notice();
1500            }
1501        })
1502    }
1503    pub fn start_interval(&self, between_ticks: Duration) {
1504        struct CallbackState {
1505            target_time: Instant,
1506            between_ticks: Duration,
1507            sender: nwg::NoticeSender,
1508            is_active: Arc<AtomicBool>,
1509            timer_thread: &'static TimerThread,
1510        }
1511        impl CallbackState {
1512            fn into_callback(mut self) -> impl FnOnce() + Send + 'static {
1513                move || {
1514                    if self.is_active.load(std::sync::atomic::Ordering::Acquire) {
1515                        self.sender.notice();
1516
1517                        self.target_time += self.between_ticks;
1518                        let now = Instant::now();
1519                        if self.target_time < now {
1520                            // System might have been asleep or something, just restart
1521                            // interval from current time.
1522                            self.target_time = now + self.between_ticks;
1523                        }
1524
1525                        let timer_thread = self.timer_thread;
1526                        let target_time = self.target_time;
1527                        timer_thread.notify_at(target_time, self.into_callback());
1528                    }
1529                }
1530            }
1531        }
1532        let target_time = Instant::now() + between_ticks;
1533        let timer_thread = TimerThread::get_global();
1534        let state = CallbackState {
1535            target_time,
1536            between_ticks,
1537            sender: self.notice.sender(),
1538            is_active: self.new_enable_signal(),
1539            timer_thread,
1540        };
1541        timer_thread.notify_at(target_time, state.into_callback());
1542    }
1543}
1544impl PartialEq for FastTimerControl {
1545    fn eq(&self, other: &Self) -> bool {
1546        self.notice == other.notice
1547    }
1548}
1549impl Eq for FastTimerControl {}
1550impl Drop for FastTimerControl {
1551    fn drop(&mut self) {
1552        self.cancel_last();
1553    }
1554}
1555
1556pub struct FastTimerControlBuilder {
1557    parent: Option<ControlHandle>,
1558    interval: Option<Duration>,
1559}
1560impl FastTimerControlBuilder {
1561    pub fn parent<C: Into<ControlHandle>>(mut self, p: C) -> Self {
1562        self.parent = Some(p.into());
1563        self
1564    }
1565    pub fn interval(mut self, interval: Duration) -> Self {
1566        self.interval = Some(interval);
1567        self
1568    }
1569    pub fn build(self, out: &mut FastTimerControl) -> Result<(), nwg::NwgError> {
1570        out.cancel_last();
1571
1572        let mut notice_builder = nwg::Notice::builder();
1573        if let Some(parent) = self.parent {
1574            notice_builder = notice_builder.parent(parent);
1575        }
1576        notice_builder.build(&mut out.notice)?;
1577
1578        if let Some(interval) = self.interval {
1579            out.start_interval(interval);
1580        }
1581
1582        Ok(())
1583    }
1584}
1585impl From<&FastTimerControl> for ControlHandle {
1586    fn from(control: &FastTimerControl) -> Self {
1587        control.notice.handle
1588    }
1589}
1590impl From<&mut FastTimerControl> for ControlHandle {
1591    fn from(control: &mut FastTimerControl) -> Self {
1592        control.notice.handle
1593    }
1594}
1595impl PartialEq<ControlHandle> for FastTimerControl {
1596    fn eq(&self, other: &ControlHandle) -> bool {
1597        self.notice.handle == *other
1598    }
1599}
1600impl PartialEq<FastTimerControl> for ControlHandle {
1601    fn eq(&self, other: &FastTimerControl) -> bool {
1602        *self == other.notice.handle
1603    }
1604}