1use 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 #[nwg_control]
62 capture: ParentCapture,
63
64 #[nwg_control]
67 parent: nwg::MessageWindow,
68
69 #[nwg_partial(parent: parent)]
72 pub invisible_window: LazyUi<InvisibleWindow>,
73
74 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 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 windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW.0
138 } else {
139 0
140 };
141 let parent = if to_refocus {
143 None
146 } else {
147 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 #[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 self.on_refocus_finished();
186 }
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 let res = vd::move_window_to_desktop(desktop, &window_handle).or_else(|_e| {
204 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 self.on_focus_tick(); 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 }
249}
250
251pub struct CustomInvisibleWindow(windows::Win32::Foundation::HWND);
254#[allow(dead_code)]
255impl CustomInvisibleWindow {
256 const CLASS_NAME_UTF8: &'static str = "CustomInvisibleWindow";
257
258 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 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 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 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 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 tracing::error!("Failed to find the created window: {_e:?}");
448 std::thread::sleep(Duration::from_millis(100));
449 try_move()?;
450 }
451
452 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 ui.window.set_visible(true);
465 ui.window.restore();
466 ui.set_foreground();
467 ui.window.set_focus();
468 Ok(())
469
470 }