From 51366ba30380aeb5f5c3dd63b8e167b1c263a848 Mon Sep 17 00:00:00 2001 From: rpop0 <38384209+rpop0@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:57:19 +0200 Subject: [PATCH] feat: hide and show windows via cloaking from COM lib (#792) --- Cargo.lock | 3 +- packages/wm/Cargo.toml | 1 + .../wm/src/common/commands/platform_sync.rs | 8 +- .../wm/src/common/commands/reload_config.rs | 11 +- packages/wm/src/common/platform/com.rs | 100 ++++++++++++++++++ packages/wm/src/common/platform/mod.rs | 2 + .../wm/src/common/platform/native_window.rs | 66 ++++++++---- packages/wm/src/user_config.rs | 12 +++ resources/assets/sample-config.yaml | 7 ++ 9 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 packages/wm/src/common/platform/com.rs diff --git a/Cargo.lock b/Cargo.lock index 7d3e9b964..a7fe98671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -2293,6 +2293,7 @@ dependencies = [ "tray-icon", "uuid", "windows", + "windows-interface", ] [[package]] diff --git a/packages/wm/Cargo.toml b/packages/wm/Cargo.toml index 72d7fb94f..ad3b0690e 100644 --- a/packages/wm/Cargo.toml +++ b/packages/wm/Cargo.toml @@ -61,3 +61,4 @@ windows = { version = "0.52", features = [ "Win32_UI_TextServices", "Win32_UI_WindowsAndMessaging", ] } +windows-interface = { version = "0.52" } diff --git a/packages/wm/src/common/commands/platform_sync.rs b/packages/wm/src/common/commands/platform_sync.rs index cf8a2aa47..5145953d0 100644 --- a/packages/wm/src/common/commands/platform_sync.rs +++ b/packages/wm/src/common/commands/platform_sync.rs @@ -23,7 +23,7 @@ pub fn platform_sync( config: &UserConfig, ) -> anyhow::Result<()> { if !state.pending_sync.containers_to_redraw.is_empty() { - redraw_containers(state)?; + redraw_containers(state, config)?; state.pending_sync.containers_to_redraw.clear(); } @@ -106,7 +106,10 @@ fn sync_focus( Ok(()) } -fn redraw_containers(state: &mut WmState) -> anyhow::Result<()> { +fn redraw_containers( + state: &mut WmState, + config: &UserConfig, +) -> anyhow::Result<()> { for window in &state.windows_to_redraw() { let workspace = window.workspace().context("Window has no workspace.")?; @@ -138,6 +141,7 @@ fn redraw_containers(state: &mut WmState) -> anyhow::Result<()> { &window.state(), &rect, is_visible, + &config.value.general.hide_method, window.has_pending_dpi_adjustment(), ) { warn!("Failed to set window position: {}", err); diff --git a/packages/wm/src/common/commands/reload_config.rs b/packages/wm/src/common/commands/reload_config.rs index d3fa766fc..47648ebf2 100644 --- a/packages/wm/src/common/commands/reload_config.rs +++ b/packages/wm/src/common/commands/reload_config.rs @@ -4,7 +4,7 @@ use tracing::{info, warn}; use crate::{ app_command::InvokeCommand, containers::traits::{CommonGetters, TilingSizeGetters}, - user_config::{ParsedConfig, UserConfig, WindowRuleEvent}, + user_config::{HideMethod, ParsedConfig, UserConfig, WindowRuleEvent}, windows::{commands::run_window_rules, traits::WindowGetters}, wm_event::WmEvent, wm_state::WmState, @@ -35,6 +35,15 @@ pub fn reload_config( update_window_effects(&old_config, state, config)?; + // Ensure all windows are shown when hide method is changed. + if old_config.general.hide_method != config.value.general.hide_method + && old_config.general.hide_method == HideMethod::Hide + { + for window in state.windows() { + let _ = window.native().show(); + } + } + // Clear active binding modes. state.binding_modes = Vec::new(); diff --git a/packages/wm/src/common/platform/com.rs b/packages/wm/src/common/platform/com.rs new file mode 100644 index 000000000..a08cf1293 --- /dev/null +++ b/packages/wm/src/common/platform/com.rs @@ -0,0 +1,100 @@ +use anyhow::Context; +use windows::{ + core::{ComInterface, IUnknown, IUnknown_Vtbl, GUID, HRESULT}, + Win32::System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, IServiceProvider, + CLSCTX_ALL, COINIT_APARTMENTTHREADED, + }, +}; + +/// COM class identifier (CLSID) for the Windows Shell that implements the +/// `IServiceProvider` interface. +pub const CLSID_IMMERSIVE_SHELL: GUID = + GUID::from_u128(0xC2F03A33_21F5_47FA_B4BB_156362A2F239); + +thread_local! { + /// Manages per-thread COM initialization. COM must be initialized on each + /// thread that uses it, so we store this in thread-local storage to handle + /// the setup and cleanup automatically. + pub static COM_INIT: ComInit = ComInit::new(); +} + +pub struct ComInit(); + +impl ComInit { + /// Initializes COM on the current thread with apartment threading model. + /// `COINIT_APARTMENTTHREADED` is required for shell COM objects. + /// + /// # Panics + /// + /// Panics if COM initialization fails. This is typically only possible + /// if COM is already initialized with an incompatible threading model. + pub fn new() -> Self { + unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) } + .expect("Unable to initialize COM."); + + Self() + } +} + +impl Drop for ComInit { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +/// Returns an instance of `IServiceProvider`. +pub fn iservice_provider() -> anyhow::Result { + COM_INIT.with(|_| unsafe { + CoCreateInstance(&CLSID_IMMERSIVE_SHELL, None, CLSCTX_ALL) + .context("Unable to create `IServiceProvider` instance.") + }) +} + +/// Returns an instance of `IApplicationViewCollection`. +pub fn iapplication_view_collection( + provider: &IServiceProvider, +) -> anyhow::Result { + COM_INIT.with(|_| { + unsafe { provider.QueryService(&IApplicationViewCollection::IID) } + .context( + "Failed to query for `IApplicationViewCollection` instance.", + ) + }) +} + +/// Undocumented COM interface for Windows shell functionality. +/// +/// Note that filler methods are added to match the vtable layout. +#[windows_interface::interface("1841c6d7-4f9d-42c0-af41-8747538f10e5")] +pub unsafe trait IApplicationViewCollection: IUnknown { + pub unsafe fn m1(&self); + pub unsafe fn m2(&self); + pub unsafe fn m3(&self); + pub unsafe fn get_view_for_hwnd( + &self, + window: isize, + application_view: *mut Option, + ) -> HRESULT; +} + +/// Undocumented COM interface for managing views in the Windows shell. +/// +/// Note that filler methods are added to match the vtable layout. +#[windows_interface::interface("372E1D3B-38D3-42E4-A15B-8AB2B178F513")] +pub unsafe trait IApplicationView: IUnknown { + pub unsafe fn m1(&self); + pub unsafe fn m2(&self); + pub unsafe fn m3(&self); + pub unsafe fn m4(&self); + pub unsafe fn m5(&self); + pub unsafe fn m6(&self); + pub unsafe fn m7(&self); + pub unsafe fn m8(&self); + pub unsafe fn m9(&self); + pub unsafe fn set_cloak( + &self, + cloak_type: u32, + cloak_flag: i32, + ) -> HRESULT; +} diff --git a/packages/wm/src/common/platform/mod.rs b/packages/wm/src/common/platform/mod.rs index dcf3c9274..8ad4c8283 100644 --- a/packages/wm/src/common/platform/mod.rs +++ b/packages/wm/src/common/platform/mod.rs @@ -1,3 +1,4 @@ +mod com; mod event_listener; mod event_window; mod keyboard_hook; @@ -7,6 +8,7 @@ mod platform; mod single_instance; mod window_event_hook; +pub use com::*; pub use event_listener::*; pub use event_window::*; pub use keyboard_hook::*; diff --git a/packages/wm/src/common/platform/native_window.rs b/packages/wm/src/common/platform/native_window.rs index 04d196111..0bc5920b5 100644 --- a/packages/wm/src/common/platform/native_window.rs +++ b/packages/wm/src/common/platform/native_window.rs @@ -23,20 +23,20 @@ use windows::{ SetForegroundWindow, SetWindowLongPtrW, SetWindowPos, ShowWindowAsync, GWL_EXSTYLE, GWL_STYLE, GW_OWNER, HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, - SWP_HIDEWINDOW, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOMOVE, - SWP_NOOWNERZORDER, SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER, - SWP_SHOWWINDOW, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, - SW_SHOWNA, WINDOW_EX_STYLE, WINDOW_STYLE, WM_CLOSE, WS_CAPTION, - WS_CHILD, WS_DLGFRAME, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, - WS_MAXIMIZEBOX, WS_THICKFRAME, + SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOMOVE, SWP_NOOWNERZORDER, + SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, + SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOWNA, WINDOW_EX_STYLE, + WINDOW_STYLE, WM_CLOSE, WS_CAPTION, WS_CHILD, WS_DLGFRAME, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_MAXIMIZEBOX, WS_THICKFRAME, }, }, }, }; +use super::{iapplication_view_collection, iservice_provider, COM_INIT}; use crate::{ common::{Color, LengthValue, Memo, Rect, RectDelta}, - user_config::CornerStyle, + user_config::{CornerStyle, HideMethod}, windows::WindowState, }; @@ -507,6 +507,23 @@ impl NativeWindow { Ok(()) } + pub fn set_visible( + &self, + visible: bool, + hide_method: &HideMethod, + ) -> anyhow::Result<()> { + match hide_method { + HideMethod::Hide => { + if visible { + self.show() + } else { + self.hide() + } + } + HideMethod::Cloak => self.set_cloaked(!visible), + } + } + pub fn show(&self) -> anyhow::Result<()> { unsafe { ShowWindowAsync(HWND(self.handle), SW_SHOWNA) }.ok()?; Ok(()) @@ -517,11 +534,31 @@ impl NativeWindow { Ok(()) } + pub fn set_cloaked(&self, cloaked: bool) -> anyhow::Result<()> { + COM_INIT.with(|_| -> anyhow::Result<()> { + let view_collection = + iapplication_view_collection(&iservice_provider()?)?; + + let mut view = None; + unsafe { view_collection.get_view_for_hwnd(self.handle, &mut view) } + .ok()?; + + let view = view + .context("Unable to get application view by window handle.")?; + + // Ref: https://github.com/Ciantic/AltTabAccessor/issues/1#issuecomment-1426877843 + unsafe { view.set_cloak(1, if cloaked { 2 } else { 0 }) } + .ok() + .context("Failed to cloak window.") + }) + } + pub fn set_position( &self, state: &WindowState, rect: &Rect, is_visible: bool, + hide_method: &HideMethod, has_pending_dpi_adjustment: bool, ) -> anyhow::Result<()> { // Restore window if it's minimized/maximized and shouldn't be. This is @@ -549,12 +586,6 @@ impl NativeWindow { | SWP_NOSENDCHANGING | SWP_ASYNCWINDOWPOS; - // Whether to show or hide the window. - match is_visible { - true => swp_flags |= SWP_SHOWWINDOW, - false => swp_flags |= SWP_HIDEWINDOW, - }; - // Whether the window should be shown above all other windows. let z_order = match state { WindowState::Floating(config) if config.shown_on_top => HWND_TOPMOST, @@ -564,14 +595,11 @@ impl NativeWindow { _ => HWND_NOTOPMOST, }; + // Whether to show or hide the window. + self.set_visible(is_visible, hide_method)?; + match state { WindowState::Minimized => { - if !is_visible { - self.hide()?; - } else { - self.show()?; - } - if !self.is_minimized()? { self.minimize()?; } diff --git a/packages/wm/src/user_config.rs b/packages/wm/src/user_config.rs index d25406c89..9802f5bdd 100644 --- a/packages/wm/src/user_config.rs +++ b/packages/wm/src/user_config.rs @@ -413,6 +413,18 @@ pub struct GeneralConfig { /// Commands to run after the WM config has reloaded. #[serde(default)] pub config_reload_commands: Vec, + + /// How windows should be hidden when switching workspaces. + #[serde(default)] + pub hide_method: HideMethod, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum HideMethod { + Hide, + #[default] + Cloak, } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/resources/assets/sample-config.yaml b/resources/assets/sample-config.yaml index 2b1be0f46..7e9ca2a4b 100644 --- a/resources/assets/sample-config.yaml +++ b/resources/assets/sample-config.yaml @@ -27,6 +27,13 @@ general: # - 'window_focus': Jump when focus changes between windows. trigger: 'monitor_focus' + # How windows should be hidden when switching workspaces. + # - 'cloak': Recommended. Hides windows with no animation and keeps them + # visible in the taskbar. + # - 'hide': Legacy method (v3.5 and earlier) that has a brief animation, + # but has stability issues with some apps. + hide_method: 'cloak' + gaps: # Whether to scale the gaps with the DPI of the monitor. scale_with_dpi: true