From 60018d6e402aa5cd3647caad2b96546a59f31851 Mon Sep 17 00:00:00 2001 From: ZouaouiB Date: Fri, 23 Oct 2026 01:32:25 +0100 Subject: [PATCH] Windows: the Coucou window opens and closes with Windows' own motion The Coucou window is created hidden and only shown and hidden afterwards, so opening it (Settings in the island, the tray, the Start menu) and closing it had no animation: it popped in and out. It now rises out of its taskbar button and folds back into it, the system's minimise/restore animation. To open, it is shown minimised (the button comes back), then restored 60 ms later; to close, it is minimised, then hidden once the fold is over (400 ms), which takes the button off the taskbar as before. Reopened during the fold, the pending hide is dropped. Nothing is drawn by the page, and Windows' Animation effects setting turns it off like for any other window. Linux keeps the plain hide and show. All in hub_motion.rs; show_hub() and the close handler call open() and close(). --- windows/src-tauri/src/hub_motion.rs | 116 ++++++++++++++++++++++++++++ windows/src-tauri/src/lib.rs | 7 +- 2 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 windows/src-tauri/src/hub_motion.rs diff --git a/windows/src-tauri/src/hub_motion.rs b/windows/src-tauri/src/hub_motion.rs new file mode 100644 index 0000000000..bc30179d40 --- /dev/null +++ b/windows/src-tauri/src/hub_motion.rs @@ -0,0 +1,116 @@ +// The Coucou window opens and closes with Windows 11's own motion: +// it rises out of its taskbar button and folds back down into it, the system's +// minimise / restore animation. Nothing is drawn by the page, and Windows' own +// "Animation effects" setting turns it off like for any other window. +// +// Closing still ends with the window hidden, as before: minimised first, so +// the fold plays, then hidden once it has, which takes the button off the +// taskbar. Opening reverses it: shown minimised, which puts the button back, +// then restored from there. Elsewhere (Linux), plain hide and show. + +use tauri::WebviewWindow; + +/// Shows the window, focused. +pub fn open(win: &WebviewWindow) { + #[cfg(windows)] + if native::open(win) { + return; + } + let _ = win.unminimize(); + let _ = win.show(); + let _ = win.set_focus(); +} + +/// Hides the window. +pub fn close(win: &WebviewWindow) { + #[cfg(windows)] + if native::close(win) { + return; + } + let _ = win.hide(); +} + +#[cfg(windows)] +mod native { + use std::sync::atomic::{AtomicU64, Ordering}; + use std::time::Duration; + + use tauri::WebviewWindow; + use windows::Win32::Foundation::HWND; + use windows::Win32::UI::WindowsAndMessaging::{ + IsIconic, IsWindowVisible, ShowWindow, SW_HIDE, SW_MINIMIZE, SW_RESTORE, SW_SHOWMINNOACTIVE, + }; + + /// Lets the taskbar add the button before the window rises from it: restored + /// at once, Windows has no button to start the motion from. + const BUTTON_MS: u64 = 60; + /// Past the fold (about 250 ms on Windows 11) before the window is hidden. + const FOLD_MS: u64 = 400; + + /// Bumped by every open and close: a hide still waiting for its fold to end + /// is dropped when the window was opened again in the meantime. + static MOTION: AtomicU64 = AtomicU64::new(0); + + fn raw_hwnd(win: &WebviewWindow) -> Option { + let raw = win.hwnd().ok()?.0 as isize; + (raw != 0).then_some(raw) + } + + fn hwnd(raw: isize) -> HWND { + HWND(raw as *mut _) + } + + pub fn open(win: &WebviewWindow) -> bool { + let Some(raw) = raw_hwnd(win) else { return false }; + let motion = MOTION.fetch_add(1, Ordering::SeqCst) + 1; + let hidden = unsafe { !IsWindowVisible(hwnd(raw)).as_bool() }; + if !hidden && unsafe { !IsIconic(hwnd(raw)).as_bool() } { + let _ = win.set_focus(); + return true; + } + if hidden { + // Back on the taskbar, folded: the button the window rises from. + unsafe { + let _ = ShowWindow(hwnd(raw), SW_SHOWMINNOACTIVE); + } + } + let win = win.clone(); + std::thread::spawn(move || { + std::thread::sleep(Duration::from_millis(if hidden { BUTTON_MS } else { 0 })); + let this = win.clone(); + let _ = win.run_on_main_thread(move || { + if MOTION.load(Ordering::SeqCst) != motion { + return; + } + unsafe { + let _ = ShowWindow(hwnd(raw), SW_RESTORE); + } + let _ = this.set_focus(); + }); + }); + true + } + + pub fn close(win: &WebviewWindow) -> bool { + let Some(raw) = raw_hwnd(win) else { return false }; + let motion = MOTION.fetch_add(1, Ordering::SeqCst) + 1; + unsafe { + let _ = ShowWindow(hwnd(raw), SW_MINIMIZE); + } + let win = win.clone(); + std::thread::spawn(move || { + std::thread::sleep(Duration::from_millis(FOLD_MS)); + let _ = win.run_on_main_thread(move || { + if MOTION.load(Ordering::SeqCst) != motion { + return; + } + unsafe { + if IsIconic(hwnd(raw)).as_bool() { + let _ = ShowWindow(hwnd(raw), SW_HIDE); + } + } + }); + }); + true + } +} diff --git a/windows/src-tauri/src/lib.rs b/windows/src-tauri/src/lib.rs index 5e544f80c6..c68bac2a77 100644 --- a/windows/src-tauri/src/lib.rs +++ b/windows/src-tauri/src/lib.rs @@ -11,6 +11,7 @@ mod files; mod flow; mod github; mod hooks; +mod hub_motion; mod i18n; mod identity; mod integrations; @@ -709,7 +710,7 @@ fn create_settings_window(app: &AppHandle) { // Closing it must only hide it, or it could never be reopened. tauri::WindowEvent::CloseRequested { api, .. } => { api.prevent_close(); - let _ = this.hide(); + hub_motion::close(&this); set_hub_shown(&this, false); } // Minimised (a 0×0 resize on Windows) or back from it. @@ -756,9 +757,7 @@ pub fn show_hub(app: &AppHandle, page: Option<&str>) { if let Some(page) = page { let _ = win.emit("hub-page", page.to_string()); } - let _ = win.unminimize(); - let _ = win.show(); - let _ = win.set_focus(); + hub_motion::open(&win); set_hub_shown(&win, true); }