Skip to content

Commit

Permalink
feat: hide and show windows via cloaking from COM lib (#792)
Browse files Browse the repository at this point in the history
  • Loading branch information
rpop0 authored Nov 7, 2024
1 parent 04e6c2f commit 51366ba
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 23 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/wm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ windows = { version = "0.52", features = [
"Win32_UI_TextServices",
"Win32_UI_WindowsAndMessaging",
] }
windows-interface = { version = "0.52" }
8 changes: 6 additions & 2 deletions packages/wm/src/common/commands/platform_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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.")?;
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion packages/wm/src/common/commands/reload_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();

Expand Down
100 changes: 100 additions & 0 deletions packages/wm/src/common/platform/com.rs
Original file line number Diff line number Diff line change
@@ -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<IServiceProvider> {
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<IApplicationViewCollection> {
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<IApplicationView>,
) -> 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;
}
2 changes: 2 additions & 0 deletions packages/wm/src/common/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod com;
mod event_listener;
mod event_window;
mod keyboard_hook;
Expand All @@ -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::*;
Expand Down
66 changes: 47 additions & 19 deletions packages/wm/src/common/platform/native_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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(())
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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()?;
}
Expand Down
12 changes: 12 additions & 0 deletions packages/wm/src/user_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,18 @@ pub struct GeneralConfig {
/// Commands to run after the WM config has reloaded.
#[serde(default)]
pub config_reload_commands: Vec<InvokeCommand>,

/// 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)]
Expand Down
7 changes: 7 additions & 0 deletions resources/assets/sample-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 51366ba

Please sign in to comment.