virtual_desktop_manager/
vd.rs

1//! Monitor and manage virtual desktops. Calls into the [`winvd`] crate, either
2//! using static or dynamic calls (using `VirtualDesktopAccessor.dll`).
3
4use std::fmt;
5
6use windows::{core::GUID, Win32::Foundation::HWND};
7
8#[cfg(not(any(feature = "winvd_dynamic", feature = "winvd_static")))]
9compile_error!("One of the features 'winvd_dynamic' and 'winvd_static' must be enabled; otherwise the program can't interact with virtual desktops at all.");
10
11pub mod dynamic {
12    #![cfg(feature = "winvd_dynamic")]
13
14    use std::{fmt, sync::OnceLock};
15
16    use libloading::{library_filename, Library, Symbol};
17    use windows::{core::GUID, Win32::Foundation::HWND};
18
19    static LIBRARY: OnceLock<Result<Library, libloading::Error>> = OnceLock::new();
20
21    /// # Safety
22    ///
23    /// Must be safe to call `libloading::Library::new` with
24    /// "VirtualDesktopAccessor.dll". This means any initialization code in that
25    /// dynamic library must be safe to call.
26    pub unsafe fn loaded_library() -> Result<&'static Library, &'static libloading::Error> {
27        let res = LIBRARY.get_or_init(|| {
28            let name = library_filename("VirtualDesktopAccessor");
29            unsafe { Library::new(name) }
30        });
31        match &res {
32            Ok(lib) => Ok(lib),
33            Err(err) => Err(err),
34        }
35    }
36
37    static SYMBOLS: OnceLock<Result<VdSymbols<'static>, &'static libloading::Error>> =
38        OnceLock::new();
39
40    /// # Safety
41    ///
42    /// Must be safe to call `libloading::Library::new` with
43    /// "VirtualDesktopAccessor.dll". This means any initialization code in that
44    /// dynamic library must be safe to call.
45    ///
46    /// Must also be safe to load the expected symbols from that library, so if
47    /// a symbol exists then it must have the correct signature.
48    pub unsafe fn loaded_symbols() -> Result<&'static VdSymbols<'static>, &'static libloading::Error>
49    {
50        let res = SYMBOLS.get_or_init(|| unsafe { Ok(VdSymbols::new(loaded_library()?)) });
51
52        match &res {
53            Ok(lib) => Ok(lib),
54            Err(err) => Err(err),
55        }
56    }
57    pub fn get_loaded_symbols(
58    ) -> Option<Result<&'static VdSymbols<'static>, &'static libloading::Error>> {
59        let res = SYMBOLS.get()?;
60        Some(match &res {
61            Ok(lib) => Ok(lib),
62            Err(err) => Err(err),
63        })
64    }
65
66    trait CheckError {
67        fn is_error(&self) -> bool;
68    }
69    impl CheckError for i32 {
70        fn is_error(&self) -> bool {
71            *self == -1
72        }
73    }
74    impl CheckError for GUID {
75        fn is_error(&self) -> bool {
76            *self == GUID::default()
77        }
78    }
79
80    macro_rules! define_symbols {
81        (
82            $(
83                $(#[no_error $(@ $no_error:tt)?])?
84                $(#[optional $(@ $optional:tt)?])?
85                $(unsafe $(@ $unsafe:tt)?)? fn $name:ident($($arg:ident: $t:ty),* $(,)?) -> $ret:ty {}
86            )*
87        ) => {
88            #[derive(Debug, Clone)]
89            pub enum DynamicError {
90                $(
91                    #[allow(dead_code)]
92                    $name { $($arg: $t,)* },
93                )*
94                Missing(&'static str),
95            }
96            impl fmt::Display for DynamicError {
97                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98                    match self {
99                        $(DynamicError::$name { $($arg,)* } => write!(
100                            f, concat!(
101                                "Failed to call \"{fn_name}\" in dynamic library with arguments [",
102                                $(stringify!($arg), ": {:?}, ",)*
103                                "]"
104                            ),
105                            $($arg,)*
106                            fn_name = stringify!($name),
107                        ),)*
108                        DynamicError::Missing(method) => write!(f, "The method \"{method}\" was not found in the dynamic library"),
109                    }
110                }
111            }
112
113            #[allow(non_snake_case, dead_code)]
114            pub struct VdSymbols<'lib> {
115                $(
116                    pub $name: Result<Symbol<'lib, $(unsafe $($unsafe)?)? extern "C" fn($($t),*) -> $ret>, libloading::Error>,
117                )*
118            }
119            impl<'lib> VdSymbols<'lib> {
120                /// Load symbols from the specified library.
121                ///
122                /// # Safety
123                ///
124                /// The symbol names must have the correct function signatures.
125                pub unsafe fn new(lib: &'lib Library) -> Self {
126                    Self {
127                        $(
128                            // Safety: the caller assures us that the symbol
129                            // name matches the expected function signature
130                            $name: unsafe {
131                                lib.get(concat!(stringify!($name), "\0").as_bytes())
132                            },
133                        )*
134                    }
135                }
136                pub fn ensure_required_methods_exists(&self) -> Result<(), DynamicError> {
137                    $(
138                        #[cfg(all($(any( $($optional)? ))?))]
139                        if let Err(_) = &self.$name {
140                            return Err(DynamicError::Missing(stringify!($name)));
141                        }
142                    )*
143                    Ok(())
144                }
145
146                $(
147                    /// Call the specified function in the dynamic library.
148                    ///
149                    /// # Safety
150                    ///
151                    /// Depends on the function. Likely any pointers must be
152                    /// valid.
153                    #[allow(non_snake_case)]
154                    pub $(unsafe $($unsafe)?)? fn $name(&self, $($arg: $t),*) -> Result<$ret, DynamicError> {
155                        tracing::trace!("Dynamic library call to {}", stringify!($name));
156                        let sym = match &self.$name {
157                            Ok(sym) => sym,
158                            Err(_) => return Err(DynamicError::Missing(stringify!($name))),
159                        };
160                        let res = sym($($arg),*);
161                        #[cfg(all($(any( $($no_error)? ))?))]
162                        {
163                            if CheckError::is_error(&res) {
164                                return Err(DynamicError::$name { $($arg,)* })
165                            }
166                        }
167                        Ok(res)
168                    }
169                )*
170            }
171        };
172    }
173    // These names were copied from:
174    // https://github.com/Ciantic/VirtualDesktopAccessor/blob/126b9e04f4f01d434af06c20d8200d0659547774/README.md#reference-of-exported-dll-functions
175    define_symbols!(
176        fn GetCurrentDesktopNumber() -> i32 {}
177        fn GetDesktopCount() -> i32 {}
178        fn GetDesktopIdByNumber(number: i32) -> GUID {} // Untested
179        fn GetDesktopNumberById(desktop_id: GUID) -> i32 {} // Untested
180        fn GetWindowDesktopId(hwnd: HWND) -> GUID {}
181        fn GetWindowDesktopNumber(hwnd: HWND) -> i32 {}
182        fn IsWindowOnCurrentVirtualDesktop(hwnd: HWND) -> i32 {}
183        fn MoveWindowToDesktopNumber(hwnd: HWND, desktop_number: i32) -> i32 {}
184        fn GoToDesktopNumber(desktop_number: i32) -> i32 {}
185        #[optional] // Win11 only
186        unsafe fn SetDesktopName(desktop_number: i32, in_name_ptr: *const i8) -> i32 {}
187        #[optional] // Win11 only
188        unsafe fn GetDesktopName(
189            desktop_number: i32,
190            out_utf8_ptr: *mut u8,
191            out_utf8_len: usize,
192        ) -> i32 {
193        }
194        unsafe fn RegisterPostMessageHook(listener_hwnd: HWND, message_offset: u32) -> i32 {}
195        unsafe fn UnregisterPostMessageHook(listener_hwnd: HWND) -> i32 {}
196        fn IsPinnedWindow(hwnd: HWND) -> i32 {}
197        fn PinWindow(hwnd: HWND) -> i32 {}
198        fn UnPinWindow(hwnd: HWND) -> i32 {}
199        fn IsPinnedApp(hwnd: HWND) -> i32 {}
200        fn PinApp(hwnd: HWND) -> i32 {}
201        fn UnPinApp(hwnd: HWND) -> i32 {}
202        fn IsWindowOnDesktopNumber(hwnd: HWND, desktop_number: i32) -> i32 {}
203        #[optional] // Win11 only
204        fn CreateDesktop() -> i32 {}
205        #[optional] // Win11 only
206        fn RemoveDesktop(remove_desktop_number: i32, fallback_desktop_number: i32) -> i32 {}
207    );
208
209    impl From<DynamicError> for super::Error {
210        fn from(err: DynamicError) -> Self {
211            Self::DynamicCall(err)
212        }
213    }
214}
215
216/// Wrapper around [`winvd::Desktop`].
217#[derive(Copy, Clone, Debug, Eq, PartialEq)]
218pub enum Desktop {
219    #[cfg(feature = "winvd_static")]
220    Static(winvd::Desktop),
221    Index(u32),
222    Guid(GUID),
223}
224impl Desktop {
225    pub fn get_index(&self) -> Result<u32, Error> {
226        match self {
227            #[cfg(feature = "winvd_static")]
228            Self::Static(d) => Ok(d.get_index()?),
229            Self::Index(i) => Ok(*i),
230            Self::Guid(guid) => {
231                #[cfg(feature = "winvd_dynamic")]
232                if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
233                    return Ok(symbols.GetDesktopNumberById(*guid)? as u32);
234                }
235                #[cfg(feature = "winvd_static")]
236                {
237                    return Ok(winvd::get_desktop(*guid).get_index()?);
238                }
239                #[allow(unreachable_code)]
240                {
241                    Err(no_dynamic_library_error())
242                }
243            }
244        }
245    }
246    pub fn get_name(&self) -> Result<String, Error> {
247        match self {
248            #[cfg(feature = "winvd_static")]
249            Self::Static(d) => Ok(d.get_name()?),
250            _ => {
251                #[cfg(feature = "winvd_dynamic")]
252                if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
253                    let mut buf = vec![0u8; 256];
254                    let desktop_number = self.get_index()? as i32;
255                    let out_utf8_len = buf.len();
256                    let out_utf8_ptr = buf.as_mut_ptr();
257                    // res is -1 if len was to short.
258                    let res = unsafe {
259                        symbols.GetDesktopName(desktop_number, out_utf8_ptr, out_utf8_len)?
260                    };
261                    if res == 0 {
262                        // winvd::Desktop::get_name returned an error
263                        return Err(Error::DynamicCall(dynamic::DynamicError::GetDesktopName {
264                            desktop_number,
265                            out_utf8_ptr,
266                            out_utf8_len,
267                        }));
268                    }
269                    // find first nul byte:
270                    if let Some(first_nul) = buf.iter().position(|&byte| byte == b'\0') {
271                        buf.truncate(first_nul + 1);
272                    }
273                    let mut name = std::ffi::CString::from_vec_with_nul(buf)
274                        .map_err(|_| Error::DesktopNameWithoutNul)?
275                        .into_string()
276                        .map_err(|e| {
277                            Error::NonUtf8DesktopName(
278                                String::from_utf8_lossy(e.into_cstring().as_bytes()).into_owned(),
279                            )
280                        })?;
281                    name.shrink_to_fit();
282                    return Ok(name);
283                }
284                #[cfg(feature = "winvd_static")]
285                {
286                    return Ok(winvd::Desktop::from(*self).get_name()?);
287                }
288                #[allow(unreachable_code)]
289                {
290                    Err(no_dynamic_library_error())
291                }
292            }
293        }
294    }
295}
296#[cfg(feature = "winvd_static")]
297impl From<winvd::Desktop> for Desktop {
298    fn from(d: winvd::Desktop) -> Self {
299        Self::Static(d)
300    }
301}
302#[cfg(feature = "winvd_static")]
303impl From<Desktop> for winvd::Desktop {
304    fn from(d: Desktop) -> Self {
305        match d {
306            Desktop::Static(d) => d,
307            Desktop::Index(i) => winvd::get_desktop(i),
308            Desktop::Guid(g) => winvd::get_desktop(g),
309        }
310    }
311}
312impl From<u32> for Desktop {
313    fn from(i: u32) -> Self {
314        Self::Index(i)
315    }
316}
317impl From<i32> for Desktop {
318    fn from(i: i32) -> Self {
319        Self::Index(i as u32)
320    }
321}
322impl From<GUID> for Desktop {
323    fn from(g: GUID) -> Self {
324        Self::Guid(g)
325    }
326}
327
328/// Get desktop by index or GUID (Same as [`winvd::get_desktop`]).
329///
330/// # Examples
331/// * Get first desktop by index `get_desktop(0)`
332/// * Get second desktop by index `get_desktop(1)`
333/// * Get desktop by GUID `get_desktop(GUID(0, 0, 0, [0, 0, 0, 0, 0, 0, 0, 0]))`
334///
335/// Note: This function does not check if the desktop exists.
336pub fn get_desktop<T>(desktop: T) -> Desktop
337where
338    T: Into<Desktop>,
339{
340    desktop.into()
341}
342
343/// Same as [`winvd::DesktopEvent`]
344#[derive(Debug, Clone, Eq, PartialEq)]
345pub enum DesktopEvent {
346    DesktopCreated(Desktop),
347    DesktopDestroyed {
348        destroyed: Desktop,
349        fallback: Desktop,
350    },
351    DesktopChanged {
352        new: Desktop,
353        old: Desktop,
354    },
355    DesktopNameChanged(Desktop, String),
356    DesktopWallpaperChanged(Desktop, String),
357    DesktopMoved {
358        desktop: Desktop,
359        old_index: i64,
360        new_index: i64,
361    },
362    WindowChanged(HWND),
363}
364#[cfg(feature = "winvd_static")]
365impl From<winvd::DesktopEvent> for DesktopEvent {
366    fn from(event: winvd::DesktopEvent) -> Self {
367        match event {
368            winvd::DesktopEvent::DesktopCreated(d) => Self::DesktopCreated(d.into()),
369            winvd::DesktopEvent::DesktopDestroyed {
370                destroyed,
371                fallback,
372            } => Self::DesktopDestroyed {
373                destroyed: destroyed.into(),
374                fallback: fallback.into(),
375            },
376            winvd::DesktopEvent::DesktopChanged { new, old } => Self::DesktopChanged {
377                new: new.into(),
378                old: old.into(),
379            },
380            winvd::DesktopEvent::DesktopNameChanged(d, name) => {
381                Self::DesktopNameChanged(d.into(), name)
382            }
383            winvd::DesktopEvent::DesktopWallpaperChanged(d, path) => {
384                Self::DesktopWallpaperChanged(d.into(), path)
385            }
386            winvd::DesktopEvent::DesktopMoved {
387                desktop,
388                old_index,
389                new_index,
390            } => Self::DesktopMoved {
391                desktop: desktop.into(),
392                old_index,
393                new_index,
394            },
395            winvd::DesktopEvent::WindowChanged(hwnd) => Self::WindowChanged(hwnd),
396        }
397    }
398}
399
400#[derive(Debug, Clone)]
401pub enum Error {
402    #[cfg(feature = "winvd_dynamic")]
403    DynamicCall(dynamic::DynamicError),
404    #[cfg(feature = "winvd_dynamic")]
405    FailedToLoadDynamicLibrary(&'static libloading::Error),
406    /// Need to call `load_dynamic_library`.
407    NotLoadedDynamicLibrary,
408    #[cfg(feature = "winvd_static")]
409    StaticCall(winvd::Error),
410    NonUtf8DesktopName(String),
411    DesktopNameWithoutNul,
412}
413#[cfg(feature = "winvd_static")]
414impl From<winvd::Error> for Error {
415    fn from(value: winvd::Error) -> Self {
416        Self::StaticCall(value)
417    }
418}
419impl fmt::Display for Error {
420    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421        match self {
422            #[cfg(feature = "winvd_dynamic")]
423            Self::DynamicCall(err) => fmt::Display::fmt(err, f),
424            #[cfg(feature = "winvd_dynamic")]
425            Self::FailedToLoadDynamicLibrary(err) => {
426                write!(f, "Failed to load dynamic library VirtualDesktopAccessor.dll: {err}")
427            }
428            Self::NotLoadedDynamicLibrary => write!(
429                f,
430                "Tried to call a virtual desktop function before loading the dynamic library VirtualDesktopAccessor.dll"
431            ),
432            #[cfg(feature = "winvd_static")]
433            Self::StaticCall(err) => write!(
434                f,
435                "Failed to call virtual desktop function in static library: {err:?}"
436            ),
437            Self::NonUtf8DesktopName(name) => write!(f, "Non-UTF8 desktop name: {name}"),
438            Self::DesktopNameWithoutNul => write!(f, "Invalid virtual desktop name"),
439        }
440    }
441}
442impl std::error::Error for Error {}
443
444pub type Result<T, E = Error> = std::result::Result<T, E>;
445
446/// Load the dynamic library "VirtualDesktopAccessor.dll".
447///
448/// # Errors
449///
450/// If the dynamic library wasn't loaded and **no** static library was included
451/// when the executable was built. The program can then not interact with
452/// virtual desktops and should probably exit.
453///
454/// # Safety
455///
456/// Must be safe to call `libloading::Library::new` with
457/// "VirtualDesktopAccessor.dll". This means any initialization code in that
458/// dynamic library must be safe to call.
459///
460/// Must also be safe to load the expected symbols from that library, so if a
461/// symbol exists then it must have the correct signature.
462///
463/// tl;dr; If a "VirtualDesktopAccessor.dll" file exists then it has a correct
464/// implementation.
465pub unsafe fn load_dynamic_library() -> Result<(), Error> {
466    #[cfg(feature = "winvd_dynamic")]
467    {
468        let res = unsafe { dynamic::loaded_symbols() };
469        let res = match res {
470            Err(e) => {
471                tracing::warn!("Failed to load VirtualDesktopAccessor.dll: {e}");
472                Err(Error::FailedToLoadDynamicLibrary(e))
473            }
474            Ok(symbols) => {
475                if let Err(e) = symbols.ensure_required_methods_exists() {
476                    tracing::error!("Failed to load VirtualDesktopAccessor.dll: {e}");
477                    Err(Error::DynamicCall(e))
478                } else {
479                    tracing::info!("Successfully loaded VirtualDesktopAccessor.dll");
480                    Ok(())
481                }
482            }
483        };
484        if cfg!(feature = "winvd_static") {
485            // Fallback to static library included in the executable
486            Ok(())
487        } else {
488            res
489        }
490    }
491    #[cfg(not(feature = "winvd_dynamic"))]
492    {
493        Ok(())
494    }
495}
496
497pub fn has_loaded_dynamic_library_successfully() -> bool {
498    #[cfg(feature = "winvd_dynamic")]
499    {
500        matches!(dynamic::get_loaded_symbols(), Some(Ok(symbols)) if symbols.ensure_required_methods_exists().is_ok())
501    }
502    #[cfg(not(feature = "winvd_dynamic"))]
503    {
504        false
505    }
506}
507
508/// Returns an error that indicates why no dynamic library was loaded.
509///
510/// # Panics
511///
512/// If the dynamic library was loaded.
513fn no_dynamic_library_error() -> Error {
514    #[cfg(feature = "winvd_dynamic")]
515    {
516        match dynamic::get_loaded_symbols() {
517            Some(Err(e)) => return Error::FailedToLoadDynamicLibrary(e),
518            Some(Ok(_)) => panic!("Should have called the loaded function instead of reporting that no library was loaded"),
519            None => (),
520        }
521    }
522    Error::NotLoadedDynamicLibrary
523}
524
525/// Wrapper around [`winvd::get_desktop_count`] (but prefers dynamic loaded
526/// library if it exists).
527pub fn get_desktop_count() -> Result<u32> {
528    #[cfg(feature = "winvd_dynamic")]
529    {
530        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
531            return Ok(symbols.GetDesktopCount()? as u32);
532        }
533    }
534    #[cfg(feature = "winvd_static")]
535    {
536        return Ok(winvd::get_desktop_count()?);
537    }
538    #[allow(unreachable_code)]
539    Err(no_dynamic_library_error())
540}
541
542/// Wrapper around [`winvd::get_current_desktop`] (but prefers dynamic loaded
543/// library if it exists).
544pub fn get_current_desktop() -> Result<Desktop> {
545    #[cfg(feature = "winvd_dynamic")]
546    {
547        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
548            return Ok(Desktop::Index(symbols.GetCurrentDesktopNumber()? as u32));
549        }
550    }
551    #[cfg(feature = "winvd_static")]
552    {
553        return Ok(winvd::get_current_desktop()?.into());
554    }
555    #[allow(unreachable_code)]
556    Err(no_dynamic_library_error())
557}
558
559/// Wrapper around [`winvd::move_window_to_desktop`] (but prefers dynamic loaded
560/// library if it exists).
561pub fn move_window_to_desktop(desktop: Desktop, hwnd: &HWND) -> Result<()> {
562    #[cfg(feature = "winvd_dynamic")]
563    {
564        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
565            symbols.MoveWindowToDesktopNumber(*hwnd, desktop.get_index()? as i32)?;
566            return Ok(());
567        }
568    }
569    #[cfg(feature = "winvd_static")]
570    {
571        winvd::move_window_to_desktop(winvd::Desktop::from(desktop), hwnd)?;
572        return Ok(());
573    }
574    #[allow(unreachable_code)]
575    Err(no_dynamic_library_error())
576}
577
578/// Wrapper around [`winvd::pin_window`] (but prefers dynamic loaded library if
579/// it exists).
580pub fn pin_window(hwnd: HWND) -> Result<()> {
581    #[cfg(feature = "winvd_dynamic")]
582    {
583        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
584            symbols.PinWindow(hwnd)?;
585            return Ok(());
586        }
587    }
588    #[cfg(feature = "winvd_static")]
589    {
590        winvd::pin_window(hwnd)?;
591        return Ok(());
592    }
593    #[allow(unreachable_code)]
594    Err(no_dynamic_library_error())
595}
596
597/// Wrapper around [`winvd::unpin_window`] (but prefers dynamic loaded library
598/// if it exists).
599pub fn unpin_window(hwnd: HWND) -> Result<()> {
600    #[cfg(feature = "winvd_dynamic")]
601    {
602        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
603            symbols.UnPinWindow(hwnd)?;
604            return Ok(());
605        }
606    }
607    #[cfg(feature = "winvd_static")]
608    {
609        winvd::unpin_window(hwnd)?;
610        return Ok(());
611    }
612    #[allow(unreachable_code)]
613    Err(no_dynamic_library_error())
614}
615
616/// Wrapper around [`winvd::switch_desktop`] (but prefers dynamic loaded
617/// library if it exists).
618pub fn switch_desktop(desktop: Desktop) -> Result<()> {
619    #[cfg(feature = "winvd_dynamic")]
620    {
621        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
622            symbols.GoToDesktopNumber(desktop.get_index()? as i32)?;
623            return Ok(());
624        }
625    }
626    #[cfg(feature = "winvd_static")]
627    {
628        winvd::switch_desktop(winvd::Desktop::from(desktop))?;
629        return Ok(());
630    }
631    #[allow(unreachable_code)]
632    Err(no_dynamic_library_error())
633}
634
635/// Wrapper around [`winvd::switch_desktop_with_animation`] (but prefers dynamic loaded
636/// library if it exists).
637pub fn switch_desktop_with_animation(desktop: Desktop) -> Result<()> {
638    #[cfg(feature = "winvd_static")]
639    {
640        winvd::switch_desktop_with_animation(winvd::Desktop::from(desktop))?;
641        return Ok(());
642    }
643    #[allow(unreachable_code)]
644    Err(Error::DynamicCall(
645        dynamic::DynamicError::GoToDesktopNumber {
646            desktop_number: desktop.get_index().unwrap_or(1) as i32,
647        },
648    ))
649}
650
651/// Wrapper around [`winvd::remove_desktop`] (but prefers dynamic loaded
652/// library if it exists).
653pub fn remove_desktop(desktop: Desktop, fallback_desktop: Desktop) -> Result<()> {
654    #[cfg(feature = "winvd_dynamic")]
655    {
656        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
657            symbols.RemoveDesktop(
658                desktop.get_index()? as i32,
659                fallback_desktop.get_index()? as i32,
660            )?;
661            return Ok(());
662        }
663    }
664    #[cfg(feature = "winvd_static")]
665    {
666        winvd::remove_desktop(
667            winvd::Desktop::from(desktop),
668            winvd::Desktop::from(fallback_desktop),
669        )?;
670        return Ok(());
671    }
672    #[allow(unreachable_code)]
673    Err(no_dynamic_library_error())
674}
675
676/// Wrapper around [`winvd::create_desktop`] (but prefers dynamic loaded
677/// library if it exists).
678pub fn create_desktop() -> Result<Desktop> {
679    #[cfg(feature = "winvd_dynamic")]
680    {
681        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
682            return Ok(Desktop::Index(symbols.CreateDesktop()? as u32));
683        }
684    }
685    #[cfg(feature = "winvd_static")]
686    {
687        return Ok(Desktop::Static(winvd::create_desktop()?));
688    }
689    #[allow(unreachable_code)]
690    Err(no_dynamic_library_error())
691}
692
693/// Wrapper around [`winvd::get_desktops`] (but prefers dynamic loaded
694/// library if it exists).
695pub fn get_desktops() -> Result<Vec<Desktop>> {
696    #[cfg(feature = "winvd_dynamic")]
697    {
698        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
699            return Ok((0..symbols.GetDesktopCount()?)
700                .map(|i| Desktop::Index(i as u32))
701                .collect());
702        }
703    }
704    #[cfg(feature = "winvd_static")]
705    {
706        return Ok(winvd::get_desktops()?
707            .into_iter()
708            .map(Desktop::from)
709            .collect());
710    }
711    #[allow(unreachable_code)]
712    Err(no_dynamic_library_error())
713}
714
715pub fn get_window_desktop(hwnd: HWND) -> Result<Desktop> {
716    #[cfg(feature = "winvd_dynamic")]
717    {
718        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
719            return Ok(Desktop::Guid(symbols.GetWindowDesktopId(hwnd)?));
720        }
721    }
722    #[cfg(feature = "winvd_static")]
723    {
724        return Ok(winvd::get_desktop_by_window(hwnd)?.into());
725    }
726    #[allow(unreachable_code)]
727    Err(no_dynamic_library_error())
728}
729
730pub fn is_pinned_window(hwnd: HWND) -> Result<bool> {
731    #[cfg(feature = "winvd_dynamic")]
732    {
733        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
734            return Ok(symbols.IsPinnedWindow(hwnd)? != 0);
735        }
736    }
737    #[cfg(feature = "winvd_static")]
738    {
739        return Ok(winvd::is_pinned_window(hwnd)?);
740    }
741    #[allow(unreachable_code)]
742    Err(no_dynamic_library_error())
743}
744
745pub fn is_pinned_app(hwnd: HWND) -> Result<bool> {
746    #[cfg(feature = "winvd_dynamic")]
747    {
748        if let Some(Ok(symbols)) = dynamic::get_loaded_symbols() {
749            return Ok(symbols.IsPinnedApp(hwnd)? != 0);
750        }
751    }
752    #[cfg(feature = "winvd_static")]
753    {
754        return Ok(winvd::is_pinned_app(hwnd)?);
755    }
756    #[allow(unreachable_code)]
757    Err(no_dynamic_library_error())
758}
759
760/// Start flashing a window's icon in the taskbar.
761pub fn start_flashing_window(hwnd: HWND) {
762    use windows::Win32::UI::WindowsAndMessaging::{
763        FlashWindowEx, FLASHWINFO, FLASHW_TIMERNOFG, FLASHW_TRAY,
764    };
765
766    let info = FLASHWINFO {
767        cbSize: std::mem::size_of::<FLASHWINFO>() as u32,
768        // A handle to the window to be flashed. The window can be either opened or minimized.
769        hwnd,
770        dwFlags: FLASHW_TIMERNOFG | FLASHW_TRAY,
771        // The number of times to flash the window.
772        uCount: 0,
773        // The rate at which the window is to be flashed, in milliseconds. If zero, the function uses the default cursor blink rate.
774        dwTimeout: 0,
775    };
776    // The return value specifies the window's state before the new flash
777    // information is applied. If the window caption/title was drawn as active
778    // before the call, the return value is true. Otherwise, the return value is
779    // false.
780    let _ = unsafe { FlashWindowEx(&info) };
781}
782
783/// Calls [`stop_flashing_window`] using the simple async runtime provided by
784/// [`crate::block_on`].
785///
786/// # Cancellation
787///
788/// If the program exits before this function completes then some windows might
789/// remain hidden and never become visible again.
790pub fn stop_flashing_windows_blocking(
791    windows: Vec<(HWND, Option<Desktop>)>,
792) -> Result<(), Box<dyn std::error::Error>> {
793    tracing::debug!(?windows, "stop_flashing_windows_blocking");
794    if windows.is_empty() {
795        return Ok(());
796    }
797    let error = std::cell::OnceCell::new();
798    crate::block_on::block_on(crate::block_on::simple_join(windows.into_iter().map(
799        |(hwnd, target)| {
800            let error = &error;
801            async move {
802                if let Err(e) = stop_flashing_window(hwnd, target).await {
803                    let _ = error.set(e);
804                }
805            }
806        },
807    )));
808    if let Some(e) = error.into_inner() {
809        Err(e)
810    } else {
811        Ok(())
812    }
813}
814
815/// Stop a window from flashing orange in the Windows taskbar.
816///
817/// # Timeline
818///
819/// 1. Stop the flashing, but the taskbar icon might remain visible even when
820///    the window isn't on that virtual desktop.
821/// 2. Hide the window so that the taskbar icon is removed.
822/// 3. Show the window again, this will move the window to the current virtual
823///    desktop.
824/// 4. Move the window to the target desktop.
825///
826/// # Cancellation
827///
828/// If the program exits before this future completes or is canceled then some
829/// windows might remain hidden and never become visible again.
830pub async fn stop_flashing_window(
831    hwnd: HWND,
832    target_desktop: Option<Desktop>,
833) -> Result<(), Box<dyn std::error::Error>> {
834    use crate::nwg_ext::TimerThread;
835    use std::time::{Duration, Instant};
836    use windows::Win32::UI::WindowsAndMessaging::{
837        FlashWindowEx, GetWindowInfo, ShowWindow, FLASHWINFO, FLASHW_STOP, SW_HIDE, SW_SHOWNA,
838        WINDOWINFO, WS_VISIBLE,
839    };
840
841    // This move might be canceled by later operations but that might take a
842    // while so move window to give the user immediate feedback:
843    if let Some(target_desktop) = target_desktop {
844        move_window_to_desktop(target_desktop, &hwnd)?;
845    };
846
847    // Stop Taskbar Icon Flashing:
848
849    // Flashes the specified window. It does not change the active state of the
850    // window.
851    let info = FLASHWINFO {
852        cbSize: std::mem::size_of::<FLASHWINFO>() as u32,
853        // A handle to the window to be flashed. The window can be either opened or minimized.
854        hwnd,
855        dwFlags: FLASHW_STOP,
856        // The number of times to flash the window.
857        uCount: 0,
858        // The rate at which the window is to be flashed, in milliseconds. If zero, the function uses the default cursor blink rate.
859        dwTimeout: 0,
860    };
861    // The return value specifies the window's state before the new flash
862    // information is applied. If the window caption/title was drawn as active
863    // before the call, the return value is true. Otherwise, the return value is
864    // false.
865    let _ = unsafe { FlashWindowEx(&info) };
866
867    // Hide window and then show it again (fixes always visible taskbar icons):
868    let was_visible;
869    {
870        /// Safeguard to make absolutely sure the window is shown again.
871        struct ShowGuard {
872            hwnd: Option<HWND>,
873            hidden_at: Instant,
874        }
875        impl Drop for ShowGuard {
876            fn drop(&mut self) {
877                let Some(hwnd) = self.hwnd else {
878                    return;
879                };
880                let wake_at = self.hidden_at + Duration::from_millis(1000);
881                let wait = wake_at.saturating_duration_since(Instant::now());
882                if !wait.is_zero() {
883                    std::thread::sleep(wait);
884                }
885                let _ = unsafe { ShowWindow(hwnd, SW_SHOWNA) };
886            }
887        }
888        let mut show_guard = ShowGuard {
889            hwnd: Some(hwnd),
890            hidden_at: Instant::now(),
891        };
892
893        // After sending flash stop: wait for flashing to stop otherwise it is reapplied when window is shown.
894        TimerThread::get_global()
895            .delay_future(Duration::from_millis(1000))
896            .await;
897
898        // Hide (and Later Show) to update taskbar visibility (fixes always visible taskbar icons):
899        was_visible = unsafe { ShowWindow(hwnd, SW_HIDE) }.as_bool();
900        if was_visible {
901            // Wait needed before showing window again to stop flashing windows:
902            // Wait time Minimum: 30ms is quite reliable. Under 20ms nearly always fails.
903            // 100ms can fail if system is under heavy load.
904
905            // Wait for window to become hidden:
906            let retry_times = [
907                // Delay,  Total Wait
908                100,    // 100    ms
909                400,    // 500    ms
910                500,    // 1_000   ms
911                1_000,  // 2_000   ms
912                3_000,  // 5_000   ms
913                5_000,  // 10_000  ms
914                5_000,  // 15_000  ms
915                5_000,  // 20_000  ms
916                10_000, // 30_000  ms
917                30_000, // 60_000  ms
918                        //60_000,      // 120_000 ms
919                        //120_000,     // 240_000 ms
920                        //120_000,     // 360_000 ms
921            ]
922            .map(Duration::from_millis);
923            for time in retry_times {
924                TimerThread::get_global().delay_future(time).await;
925
926                let mut info = WINDOWINFO {
927                    cbSize: std::mem::size_of::<WINDOWINFO>() as u32,
928                    ..Default::default()
929                };
930                if unsafe { GetWindowInfo(hwnd, &mut info) }.is_ok()
931                    && (info.dwStyle.0 & WS_VISIBLE.0 == 0)
932                {
933                    // Window is hidden
934                    break;
935                }
936            }
937
938            // Then re-show it:
939            let _ = unsafe { ShowWindow(hwnd, SW_SHOWNA) };
940
941            // Cancel show safeguard:
942            show_guard.hwnd = None;
943        }
944    }
945
946    // Move (back) to wanted virtual desktop:
947    {
948        if !was_visible {
949            // Can't move a hidden window to another virtual desktop.
950            return Ok(());
951        }
952        let Some(target_desktop) = target_desktop else {
953            // Leave the window at the current virtual desktop.
954            return Ok(());
955        };
956
957        // After hiding and then showing the window it can either be visible on the
958        // taskbar for all virtual desktops or the window might have been moved to
959        // the current desktop.
960        //
961        // Reapply virtual desktop info to ensure it is moved to the right place:
962
963        // Note that too many attempts will cause windows taskbar and virtual
964        // desktop switching to slow down and maybe freeze.
965        // - 1000 attempts per window for 15 windows will causes explorer to freeze.
966        // - 100 attempts per window for 15 windows will cause a slight slowdown.
967        // - On newer Windows versions this has gotten dramatically slower.
968
969        let retry_times = [
970            // Delay, Total Wait
971            0,   // 0 ms   if there is no lag then this might actually work.
972            25,  // 25 ms  20% of windows are shown before 25 ms.
973            25,  // 50 ms  75% of windows are shown before 50 ms.
974            50,  // 100 ms
975            400, // 500 ms
976        ]
977        .map(Duration::from_millis);
978
979        for time in retry_times {
980            if !time.is_zero() {
981                // Note: could use std::thread::sleep for times that are less than 50ms...
982                TimerThread::get_global().delay_future(time).await;
983            }
984
985            let Ok(current) = get_window_desktop(hwnd) else {
986                // Not shown yet...
987                continue;
988            };
989            if current == target_desktop {
990                // Is at the right place!
991                break;
992            }
993            // For some of these move attempts the window might still be hidden and
994            // so impossible to move:
995            let _ = move_window_to_desktop(target_desktop, &hwnd);
996        }
997    }
998
999    Ok(())
1000}