virtual_desktop_manager/
invisible_window.rs

1//! This module can create an invisible window and focus it to allow for
2//! animation when switching virtual desktop.
3
4use std::{any::TypeId, cell::Cell, fmt, ptr::null_mut, rc::Rc, sync::OnceLock, time::Duration};
5
6use nwd::{NwgPartial, NwgUi};
7use nwg::{NativeUi, PartialUi};
8use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::SetForegroundWindow};
9
10use crate::{
11    dynamic_gui::DynamicUiHooks,
12    nwg_ext::{to_utf16, FastTimerControl, LazyUi, ParentCapture},
13    tray::{SystemTray, TrayPlugin, TrayRoot},
14    vd,
15};
16
17#[derive(Default, NwgPartial, NwgUi)]
18pub struct InvisibleWindow {
19    pub parent: Option<nwg::ControlHandle>,
20
21    pub ex_flags: u32,
22
23    #[nwg_control(
24        parent: data.parent,
25        flags: "VISIBLE | POPUP",
26        ex_flags: data.ex_flags,
27        size: (0, 0),
28        title: "",
29    )]
30    pub window: nwg::Window,
31}
32impl InvisibleWindow {
33    pub fn get_handle(&self) -> HWND {
34        HWND(
35            self.window
36                .handle
37                .hwnd()
38                .expect("Tried to use an invisible window that was't created yet")
39                .cast(),
40        )
41    }
42    pub fn set_foreground(&self) {
43        let Some(handle) = self.window.handle.hwnd() else {
44            return;
45        };
46        unsafe {
47            let _ = SetForegroundWindow(HWND(handle.cast()));
48        }
49    }
50}
51
52impl crate::nwg_ext::LazyUiHooks for InvisibleWindow {
53    fn set_parent(&mut self, parent: Option<nwg::ControlHandle>) {
54        self.parent = parent;
55    }
56}
57
58#[derive(nwd::NwgPartial, Default)]
59pub struct SmoothDesktopSwitcher {
60    /// Captures the parent that this partial UI is instantiated with.
61    #[nwg_control]
62    capture: ParentCapture,
63
64    /// Using this kind of window as parent to the invisible window works best
65    /// when we want to switch virtual desktop.
66    #[nwg_control]
67    parent: nwg::MessageWindow,
68
69    /// Note: don't set parent here since that will prevent the window from
70    /// showing up in the task bar.
71    #[nwg_partial(parent: parent)]
72    pub invisible_window: LazyUi<InvisibleWindow>,
73
74    /// `true` if `invisible_window` is created and hasn't been closed yet.
75    active: Cell<bool>,
76
77    #[nwg_control(parent: capture)]
78    #[nwg_events(OnNotice: [Self::on_close_tick])]
79    close_timer: FastTimerControl,
80
81    #[nwg_control(parent: capture)]
82    #[nwg_events(OnNotice: [Self::on_focus_tick])]
83    focus_timer: FastTimerControl,
84
85    #[nwg_control(parent: capture)]
86    #[nwg_events(OnNotice: [Self::on_refocus_tick])]
87    refocus_timer: FastTimerControl,
88
89    #[nwg_control(parent: capture)]
90    #[nwg_events(OnNotice: [Self::on_refocus_finished])]
91    refocus_finished: FastTimerControl,
92
93    started_at: core::cell::Cell<Option<std::time::Instant>>,
94}
95impl fmt::Debug for SmoothDesktopSwitcher {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("SmoothDesktopSwitcher")
98            .field("captured_parent", &self.capture.captured_parent)
99            .field("active", &self.active.get())
100            .field("started_at", &self.started_at)
101            .finish()
102    }
103}
104impl DynamicUiHooks<SystemTray> for SmoothDesktopSwitcher {
105    fn before_partial_build(
106        &mut self,
107        tray_ui: &Rc<SystemTray>,
108        _should_build: &mut bool,
109    ) -> Option<(nwg::ControlHandle, TypeId)> {
110        Some((tray_ui.root().window.handle, TypeId::of::<TrayRoot>()))
111    }
112    fn before_rebuild(&mut self, _dynamic_ui: &Rc<SystemTray>) {
113        self.close_window();
114        *self = Default::default()
115    }
116}
117impl TrayPlugin for SmoothDesktopSwitcher {}
118impl SmoothDesktopSwitcher {
119    pub fn close_window(&self) {
120        let mut window = self.invisible_window.ui.borrow_mut();
121        if !window.window.handle.blank() {
122            // Close previous window:
123            window.window.close();
124            window.window.handle.destroy();
125            self.active.set(false);
126            self.close_timer.cancel_last();
127            self.focus_timer.cancel_last();
128            self.refocus_finished.cancel_last();
129        }
130        self.active.set(false);
131    }
132    fn create_invisible_window(&self, to_refocus: bool) -> HWND {
133        self.close_window();
134        let mut window = self.invisible_window.ui.borrow_mut();
135        window.ex_flags = if to_refocus {
136            // Hide taskbar button (virtual desktop library can't find this window):
137            windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW.0
138        } else {
139            0
140        };
141        // Create new window:
142        let parent = if to_refocus {
143            // This seems to work better for re-capturing focus (but it will
144            // show a taskbar button for the window):
145            None
146        } else {
147            // Virtual desktop move might fail if we don't use this parent:
148            // self.capture.captured_parent
149            Some(self.parent.handle)
150        };
151        window.parent = parent;
152        InvisibleWindow::build_partial(&mut window, parent)
153            .expect("Failed to build invisible window");
154        self.active.set(true);
155        window.get_handle()
156    }
157    /// Open and then quickly close an invisible window to refocus the last
158    /// active window. Useful when closing a context menu or a popup.
159    #[tracing::instrument]
160    pub fn refocus_last_window(&self) {
161        self.started_at.set(Some(std::time::Instant::now()));
162        self.refocus_timer.notify_after(Duration::from_millis(25));
163    }
164    #[tracing::instrument]
165    pub fn cancel_refocus(&self) {
166        self.refocus_timer.cancel_last();
167    }
168    fn on_refocus_tick(&self) {
169        tracing::info!(
170            already_active = self.active.get(),
171            after = ?self.started_at.get().unwrap().elapsed(),
172            "InvisibleWindow::on_refocus_tick()",
173        );
174        if self.active.get() {
175            return;
176        }
177        self.create_invisible_window(true);
178        {
179            let guard = self.invisible_window.borrow();
180            guard.window.set_visible(true);
181            guard.set_foreground();
182            guard.window.set_focus();
183        }
184        // Close after it has gained focus:
185        self.on_refocus_finished();
186        //self.refocus_finished.notify_after(Duration::from_millis(50));
187    }
188    fn on_refocus_finished(&self) {
189        tracing::info!(
190            after = ?self.started_at.get().unwrap().elapsed(),
191            "InvisibleWindow::on_refocus_finished()",
192        );
193        self.close_window();
194    }
195
196    pub fn switch_desktop_to(&self, desktop: vd::Desktop) -> vd::Result<()> {
197        let window_handle = self.create_invisible_window(false);
198
199        // Move to wanted desktop:
200        //
201        // IMPORTANT: don't hold the RefCell lock during this call since it can
202        // call other window procedures to handle events.
203        let res = vd::move_window_to_desktop(desktop, &window_handle).or_else(|_e| {
204            // Sometimes winvd doesn't find the created window. (not often, but
205            // still better to retry than to give an error message)
206            tracing::error!("InvisibleWindow: Failed to find the created window: {_e:?}");
207            std::thread::sleep(Duration::from_millis(100));
208            vd::move_window_to_desktop(desktop, &window_handle)
209        });
210        if let Err(e) = res {
211            self.close_window();
212            return Err(e);
213        }
214
215        self.refocus_timer.cancel_last();
216
217        self.started_at.set(Some(std::time::Instant::now()));
218
219        // Force show the window to steal focus after it has been moved:
220        // self.focus_timer.notify_after(Duration::from_millis(10));
221        self.on_focus_tick(); // Seems like we can do this immediately?
222
223        // Don't close the window immediately since that would cancel the window focus change.
224        self.close_timer.notify_after(Duration::from_millis(125));
225        Ok(())
226    }
227    fn on_focus_tick(&self) {
228        tracing::info!(
229            after = ?self.started_at.get().unwrap().elapsed(),
230            "InvisibleWindow::on_focus_tick()",
231        );
232        let guard = self.invisible_window.borrow();
233        guard.window.set_visible(true);
234        guard.set_foreground();
235        guard.window.set_focus();
236    }
237    fn on_close_tick(&self) {
238        {
239            tracing::info!(
240                after = ?self.started_at.get().unwrap().elapsed(),
241                "InvisibleWindow::on_close_tick()",
242            );
243            self.close_window();
244        }
245
246        // Refocus last window (usually works without this, but this might help):
247        // self.refocus_last_window();
248    }
249}
250
251/// A window that attempts to be as invisible as possible while still allowing
252/// focus so that it can be focused in order to move to another virtual desktop.
253pub struct CustomInvisibleWindow(windows::Win32::Foundation::HWND);
254#[allow(dead_code)]
255impl CustomInvisibleWindow {
256    const CLASS_NAME_UTF8: &'static str = "CustomInvisibleWindow";
257
258    /// Class name in utf16 with trailing nul byte.
259    fn class_name() -> &'static [u16] {
260        static CLASS_NAME_UTF16: OnceLock<Vec<u16>> = OnceLock::new();
261        CLASS_NAME_UTF16.get_or_init(|| to_utf16(Self::CLASS_NAME_UTF8))
262    }
263    /// Create a window class.
264    ///
265    /// Adapted from [`native_windows_gui::win32::window::build_sysclass`].
266    fn create_window_class() -> Result<(), windows::core::Error> {
267        use windows::{
268            core::PCWSTR,
269            Win32::{
270                Foundation::{
271                    GetLastError, ERROR_CLASS_ALREADY_EXISTS, HINSTANCE, HWND, LPARAM, LRESULT,
272                    WPARAM,
273                },
274                Graphics::Gdi::{COLOR_WINDOW, HBRUSH},
275                System::LibraryLoader::GetModuleHandleW,
276                UI::WindowsAndMessaging::{
277                    DefWindowProcW, LoadCursorW, RegisterClassExW, ShowWindow, CS_HREDRAW,
278                    CS_VREDRAW, HICON, IDC_ARROW, SW_HIDE, WM_CLOSE, WM_CREATE, WNDCLASSEXW,
279                },
280            },
281        };
282        /// A blank system procedure used when creating new window class.
283        ///
284        /// Adapted from `blank_window_proc` in [`native_windows_gui::win32::window`].
285        unsafe extern "system" fn blank_window_proc(
286            hwnd: HWND,
287            msg: u32,
288            w: WPARAM,
289            l: LPARAM,
290        ) -> LRESULT {
291            let handled = match msg {
292                WM_CREATE => true,
293                WM_CLOSE => {
294                    let _ = ShowWindow(hwnd, SW_HIDE);
295                    true
296                }
297                _ => false,
298            };
299
300            if handled {
301                LRESULT(0)
302            } else {
303                DefWindowProcW(hwnd, msg, w, l)
304            }
305        }
306
307        let module = unsafe { GetModuleHandleW(PCWSTR::null())? };
308        let class_name = Self::class_name();
309
310        let class = WNDCLASSEXW {
311            cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
312            style: CS_HREDRAW | CS_VREDRAW,
313            lpfnWndProc: Some(blank_window_proc),
314            cbClsExtra: 0,
315            cbWndExtra: 0,
316            hInstance: module.into(),
317            hIcon: HICON(null_mut()),
318            hCursor: unsafe { LoadCursorW(HINSTANCE(null_mut()), IDC_ARROW) }?,
319            hbrBackground: HBRUSH(COLOR_WINDOW.0 as *mut _),
320            lpszMenuName: PCWSTR::null(),
321            lpszClassName: PCWSTR::from_raw(class_name.as_ptr()),
322            hIconSm: HICON(null_mut()),
323        };
324
325        let class_token = unsafe { RegisterClassExW(&class) };
326        if class_token == 0 && unsafe { GetLastError() } != ERROR_CLASS_ALREADY_EXISTS {
327            Err(windows::core::Error::from_win32())
328        } else {
329            Ok(())
330        }
331    }
332    fn lazy_create_window_class() -> windows::core::Result<()> {
333        use std::sync::atomic::{AtomicBool, Ordering};
334        static HAS_INIT: AtomicBool = AtomicBool::new(false);
335        if HAS_INIT.load(Ordering::Acquire) {
336            return Ok(());
337        }
338        if let Err(e) = Self::create_window_class() {
339            tracing::error!(
340                error =? e,
341                class_name = Self::CLASS_NAME_UTF8,
342                "Failed to create window class for invisible window"
343            );
344            Err(e)
345        } else {
346            HAS_INIT.store(true, Ordering::Release);
347            Ok(())
348        }
349    }
350    pub fn create() -> Result<Self, windows::core::Error> {
351        unsafe {
352            use windows::{
353                core::PCWSTR,
354                Win32::{
355                    Foundation::HWND,
356                    System::LibraryLoader::GetModuleHandleW,
357                    UI::WindowsAndMessaging::{
358                        CreateWindowExW, CW_USEDEFAULT, HMENU, WINDOW_EX_STYLE, WINDOW_STYLE,
359                        WS_POPUP, WS_VISIBLE,
360                    },
361                },
362            };
363            Self::lazy_create_window_class()?;
364            let module = GetModuleHandleW(PCWSTR::null())?;
365            let title = [0];
366            let class_name = Self::class_name();
367            let handle = CreateWindowExW(
368                WINDOW_EX_STYLE(0),
369                PCWSTR::from_raw(class_name.as_ptr()),
370                PCWSTR::from_raw(title.as_ptr()),
371                WINDOW_STYLE(0) | WS_POPUP | WS_VISIBLE,
372                CW_USEDEFAULT,
373                CW_USEDEFAULT,
374                0,
375                0,
376                HWND(null_mut()),
377                HMENU(null_mut()),
378                module,
379                None,
380            )?;
381            if handle.0.is_null() {
382                return Err(windows::core::Error::from_win32());
383            }
384            Ok(Self(handle))
385        }
386    }
387    pub fn set_foreground(&self) {
388        unsafe {
389            let _ = SetForegroundWindow(self.0);
390        }
391    }
392    pub fn set_focus(&self) {
393        unsafe {
394            _ = windows::Win32::UI::Input::KeyboardAndMouse::SetFocus(self.0);
395        }
396    }
397}
398impl Drop for CustomInvisibleWindow {
399    fn drop(&mut self) {
400        if let Err(e) = unsafe { windows::Win32::UI::WindowsAndMessaging::DestroyWindow(self.0) } {
401            tracing::warn!(error = ?e, "Failed to destroy window");
402        }
403    }
404}
405
406#[allow(dead_code)]
407pub fn switch_desktop_with_invisible_window(
408    desktop: vd::Desktop,
409    parent: Option<nwg::ControlHandle>,
410) -> Result<(), Box<dyn std::error::Error>> {
411    //{
412    //    let custom = CustomInvisibleWindow::create()?;
413    //    std::thread::sleep(Duration::from_millis(500));
414    //    if let Err(e) = vd::move_window_to_desktop(desktop, &custom.0) {
415    //        tracing::warn!(error = ?e, "Failed to move custom window");
416    //    }
417    //    std::thread::sleep(Duration::from_millis(500));
418    //    custom.set_foreground();
419    //    custom.set_focus();
420    //
421    //    std::thread::sleep(Duration::from_millis(1000));
422    //    panic!("testing");
423    //}
424
425    let mut empty_parent;
426    let parent = if let Some(parent) = parent {
427        Some(parent)
428    } else {
429        empty_parent = nwg::MessageWindow::default();
430        nwg::MessageWindow::builder()
431            .build(&mut empty_parent)
432            .ok()
433            .map(|()| empty_parent.handle)
434    };
435    let ui = InvisibleWindow::build_ui(InvisibleWindow {
436        parent,
437        ex_flags: 0,
438        window: Default::default(),
439    })
440    .expect("Failed to create invisible window");
441
442    // Move the window to the wanted virtual desktop:
443    let try_move =
444        || vd::move_window_to_desktop(desktop, &HWND(ui.window.handle.hwnd().unwrap().cast()));
445    if let Err(_e) = try_move() {
446        // Sometimes winvd doesn't find the created window. (not often, but still)
447        tracing::error!("Failed to find the created window: {_e:?}");
448        std::thread::sleep(Duration::from_millis(100));
449        try_move()?;
450    }
451
452    /// Don't close the window immediately since that would cancel the window focus change.
453    struct Guard<'a>(&'a InvisibleWindow);
454    impl Drop for Guard<'_> {
455        fn drop(&mut self) {
456            std::thread::sleep(Duration::from_millis(100));
457            self.0.window.close();
458        }
459    }
460    let _ui_guard = Guard(&ui);
461
462    // Then force show the window to steal focus after it has been moved:
463    // std::thread::sleep(Duration::from_millis(25));
464    ui.window.set_visible(true);
465    ui.window.restore();
466    ui.set_foreground();
467    ui.window.set_focus();
468    Ok(())
469
470    // OLD CODE that manually created window:
471
472    //use windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
473    //
474    //let mut window = core::mem::ManuallyDrop::new(Default::default());
475    //nwg::Window::builder()
476    //    .flags(nwg::WindowFlags::VISIBLE | nwg::WindowFlags::POPUP)
477    //    .ex_flags(WS_EX_TRANSPARENT.0)
478    //    .size((5, 5))
479    //    .title("")
480    //    .build(&mut window)
481    //    .expect("Failed to build invisible window");
482    //
483    //vd::move_window_to_desktop(
484    //    desktop,
485    //    &(windows::Win32::Foundation::HWND(window.handle.hwnd().unwrap() as isize)),
486    //)?;
487    //Ok(())
488} //