From 75d72522a2dc972586ddb569e84b136a2c2ddeac Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Thu, 20 Oct 2022 13:54:34 -0700 Subject: [PATCH] fix(wm): listen to displaychange w/ hidden hwnd This commit removes the previous polling strategy on ObjectCreate events and uses a hidden window to listen to WM_DISPLAYCHANGE. Unfortunately, as all monitors change HMONITOR values on monitor attach/detach, even if the monitor remains attached, the only real choice we have when a monitor which previously held windows is detached is to read the entire monitor and workspace state again, as we do when we initialise the window manager for the first time. Since it's possible that the "wrong" monitor in the state has its HMONITOR value updated, we also have to load the configuration again. This commit deprecates WindowManagerEvent::MonitorPoll. fix #267 --- komorebi/src/hidden.rs | 78 ++++++++++++++++++++++++++++ komorebi/src/main.rs | 6 +++ komorebi/src/process_event.rs | 6 +-- komorebi/src/window.rs | 2 +- komorebi/src/window_manager.rs | 24 ++++++++- komorebi/src/window_manager_event.rs | 23 ++------ komorebi/src/windows_api.rs | 22 ++++++++ komorebi/src/windows_callbacks.rs | 24 +++++++++ 8 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 komorebi/src/hidden.rs diff --git a/komorebi/src/hidden.rs b/komorebi/src/hidden.rs new file mode 100644 index 00000000..591f2b81 --- /dev/null +++ b/komorebi/src/hidden.rs @@ -0,0 +1,78 @@ +use std::sync::atomic::Ordering; +use std::time::Duration; + +use color_eyre::Result; +use windows::core::PCSTR; +use windows::Win32::Foundation::HWND; +use windows::Win32::UI::WindowsAndMessaging::DispatchMessageA; +use windows::Win32::UI::WindowsAndMessaging::FindWindowA; +use windows::Win32::UI::WindowsAndMessaging::GetMessageA; +use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW; +use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW; +use windows::Win32::UI::WindowsAndMessaging::MSG; +use windows::Win32::UI::WindowsAndMessaging::WNDCLASSA; + +use crate::windows_callbacks; +use crate::WindowsApi; +use crate::HIDDEN_HWND; +use crate::TRANSPARENCY_COLOUR; + +#[derive(Debug, Clone, Copy)] +pub struct Hidden { + pub(crate) hwnd: isize, +} + +impl From for Hidden { + fn from(hwnd: isize) -> Self { + Self { hwnd } + } +} + +impl Hidden { + pub const fn hwnd(self) -> HWND { + HWND(self.hwnd) + } + + pub fn create(name: &str) -> Result<()> { + let name = format!("{name}\0"); + let instance = WindowsApi::module_handle_w()?; + let class_name = PCSTR(name.as_ptr()); + let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR); + let window_class = WNDCLASSA { + hInstance: instance, + lpszClassName: class_name, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(windows_callbacks::hidden_window), + hbrBackground: brush, + ..Default::default() + }; + + let _atom = WindowsApi::register_class_a(&window_class)?; + + let name_cl = name.clone(); + std::thread::spawn(move || -> Result<()> { + let hwnd = WindowsApi::create_hidden_window(PCSTR(name_cl.as_ptr()), instance)?; + let hidden = Self::from(hwnd); + + let mut message = MSG::default(); + + unsafe { + while GetMessageA(&mut message, hidden.hwnd(), 0, 0).into() { + DispatchMessageA(&message); + std::thread::sleep(Duration::from_millis(10)); + } + } + + Ok(()) + }); + + let mut hwnd = HWND(0); + while hwnd == HWND(0) { + hwnd = unsafe { FindWindowA(PCSTR(name.as_ptr()), PCSTR::null()) }; + } + + HIDDEN_HWND.store(hwnd.0, Ordering::SeqCst); + + Ok(()) + } +} diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index 81070231..085a575d 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -38,6 +38,7 @@ use which::which; use winreg::enums::HKEY_CURRENT_USER; use winreg::RegKey; +use crate::hidden::Hidden; use komorebi_core::HidingBehaviour; use komorebi_core::Rect; use komorebi_core::SocketMessage; @@ -56,6 +57,7 @@ mod ring; mod border; mod container; +mod hidden; mod monitor; mod process_command; mod process_event; @@ -171,6 +173,8 @@ pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0); // 0 0 0 aka pure black, I doubt anyone will want this as a border colour pub const TRANSPARENCY_COLOUR: u32 = 0; +pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0); + fn setup() -> Result<(WorkerGuard, WorkerGuard)> { if std::env::var("RUST_LIB_BACKTRACE").is_err() { std::env::set_var("RUST_LIB_BACKTRACE", "1"); @@ -455,6 +459,8 @@ fn main() -> Result<()> { let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing))); winevent_listener.start(); + Hidden::create("komorebi-hidden")?; + let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new( incoming, )))?)); diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 56263573..60abc0dc 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -74,9 +74,9 @@ impl WindowManager { // Make sure we have the most recently focused monitor from any event match event { - WindowManagerEvent::MonitorPoll(_, window) - | WindowManagerEvent::FocusChange(_, window) + WindowManagerEvent::FocusChange(_, window) | WindowManagerEvent::Show(_, window) + | WindowManagerEvent::DisplayChange(window) | WindowManagerEvent::MoveResizeEnd(_, window) => { self.reconcile_monitors()?; @@ -482,7 +482,7 @@ impl WindowManager { } } } - WindowManagerEvent::MonitorPoll(..) | WindowManagerEvent::MouseCapture(..) => {} + WindowManagerEvent::DisplayChange(..) | WindowManagerEvent::MouseCapture(..) => {} }; if *self.focused_workspace()?.tile() && BORDER_ENABLED.load(Ordering::SeqCst) { diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index aaeaac99..2748a6b2 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -357,7 +357,7 @@ impl Window { #[tracing::instrument(fields(exe, title))] pub fn should_manage(self, event: Option) -> Result { - if let Some(WindowManagerEvent::MonitorPoll(_, _)) = event { + if let Some(WindowManagerEvent::DisplayChange(_)) = event { return Ok(true); } diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index a4fc14dd..6b140b00 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -328,6 +328,8 @@ impl WindowManager { pub fn reconcile_monitors(&mut self) -> Result<()> { let valid_hmonitors = WindowsApi::valid_hmonitors()?; let mut invalid = vec![]; + let mut updated = vec![]; + let mut overlapping = vec![]; for monitor in self.monitors_mut() { if !valid_hmonitors.contains(&monitor.id()) { @@ -335,13 +337,22 @@ impl WindowManager { // If an invalid hmonitor has at least one window in the window manager state, // we can attempt to update its hmonitor id in-place so that it doesn't get reaped + // + // This needs to be done because when monitors are attached and detached, even + // monitors that remained connected get assigned new HMONITOR values if let Some(workspace) = monitor.focused_workspace() { if let Some(container) = workspace.focused_container() { if let Some(window) = container.focused_window() { let actual_hmonitor = WindowsApi::monitor_from_window(window.hwnd()); if actual_hmonitor != monitor.id() { monitor.set_id(actual_hmonitor); - mark_as_invalid = false; + + if updated.contains(&actual_hmonitor) { + overlapping.push(monitor.clone()); + } else { + mark_as_invalid = false; + updated.push(actual_hmonitor); + } } } } @@ -356,6 +367,17 @@ impl WindowManager { // Remove any invalid monitors from our state self.monitors_mut().retain(|m| !invalid.contains(&m.id())); + // If monitor IDs are overlapping we are fucked and need to load + // monitor and workspace state again, followed by the configuration + if !overlapping.is_empty() { + WindowsApi::load_monitor_information(&mut self.monitors)?; + WindowsApi::load_workspace_information(&mut self.monitors)?; + + std::thread::spawn(|| { + load_configuration().expect("could not load configuration"); + }); + } + let invisible_borders = self.invisible_borders; let offset = self.work_area_offset; diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index 96b1f7e1..62a045b4 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -22,7 +22,7 @@ pub enum WindowManagerEvent { Manage(Window), Unmanage(Window), Raise(Window), - MonitorPoll(WinEvent, Window), + DisplayChange(Window), } impl Display for WindowManagerEvent { @@ -77,12 +77,8 @@ impl Display for WindowManagerEvent { Self::Raise(window) => { write!(f, "Raise (Window: {})", window) } - Self::MonitorPoll(winevent, window) => { - write!( - f, - "MonitorPoll (WinEvent: {}, Window: {})", - winevent, window - ) + Self::DisplayChange(window) => { + write!(f, "DisplayChange (Window: {})", window) } } } @@ -99,9 +95,9 @@ impl WindowManagerEvent { | Self::MoveResizeStart(_, window) | Self::MoveResizeEnd(_, window) | Self::MouseCapture(_, window) - | Self::MonitorPoll(_, window) | Self::Raise(window) | Self::Manage(window) + | Self::DisplayChange(window) | Self::Unmanage(window) => window, } } @@ -147,17 +143,6 @@ impl WindowManagerEvent { None } } - WinEvent::ObjectCreate => { - if let Ok(title) = window.title() { - // Hidden COM support mechanism window that fires this event on both DPI/scaling - // changes and resolution changes, a good candidate for polling - if title == "OLEChannelWnd" { - return Option::from(Self::MonitorPoll(winevent, window)); - } - } - - None - } _ => None, } } diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index b88f4a2c..c51df37d 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -104,7 +104,9 @@ use windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX; use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE; use windows::Win32::UI::WindowsAndMessaging::WNDCLASSA; use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC; +use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED; use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED; +use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE; use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW; use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX; use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX; @@ -744,6 +746,26 @@ impl WindowsApi { .process() } + pub fn create_hidden_window(name: PCSTR, instance: HINSTANCE) -> Result { + unsafe { + CreateWindowExA( + WS_EX_NOACTIVATE, + name, + name, + WS_DISABLED, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + None, + None, + instance, + None, + ) + } + .process() + } + pub fn invalidate_border_rect() -> Result<()> { unsafe { InvalidateRect(HWND(BORDER_HWND.load(Ordering::SeqCst)), None, false) } .ok() diff --git a/komorebi/src/windows_callbacks.rs b/komorebi/src/windows_callbacks.rs index 294ee11f..9803077a 100644 --- a/komorebi/src/windows_callbacks.rs +++ b/komorebi/src/windows_callbacks.rs @@ -22,6 +22,7 @@ use windows::Win32::UI::Accessibility::HWINEVENTHOOK; use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage; use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY; +use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE; use windows::Win32::UI::WindowsAndMessaging::WM_PAINT; use crate::container::Container; @@ -158,3 +159,26 @@ pub extern "system" fn border_window( } } } + +pub extern "system" fn hidden_window( + window: HWND, + message: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + unsafe { + match message as u32 { + WM_DISPLAYCHANGE => { + let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 }); + WINEVENT_CALLBACK_CHANNEL + .lock() + .0 + .send(event_type) + .expect("could not send message on WINEVENT_CALLBACK_CHANNEL"); + + LRESULT(0) + } + _ => DefWindowProcW(window, message, wparam, lparam), + } + } +}