From ce3c742e09351067a38027e60c60685063fcdb0a Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 4 Sep 2021 09:41:45 -0700 Subject: [PATCH] feat(ffm): add custom ffm/autoraise implementation This commit implements an initial attempt at a custom focus follows mouse and autoraise implementation which only responds to hwnds managed by komorebi. I was browsing GitHub and came across the winput crate which has a clean API for tracking both mouse movements and button presses, which seems to be just enough to get this functionality working. Once again, Chromium and Electron are the bane of every platform they run on and Windows is no exception, so I've had to add a hack to work around the legacy Chrome windows that get drawn on top of Electron apps with the Chrome_RenderWidgetHostHWND class. It is fairly naive; it just looks up an alternative (and hopefully correct) hwnd based on the exe name, but this will no doubt be fragile when it comes to applications that have multiple windows spawned from the same exe. For now I've opted to keep the same komorebic commands for enabling, disabling and toggling focus-follows-mouse, in order to preserve backwards compat, but those commands will now enable and disable this custom implementation instead of the native Windows X-Mouse implementation. Perhaps in the future the specific implementation to target could be specified through the use of an optional flag. re #7 --- Cargo.lock | 18 +++++++-- komorebi/Cargo.toml | 1 + komorebi/src/container.rs | 12 ++++++ komorebi/src/main.rs | 3 ++ komorebi/src/process_command.rs | 13 ++----- komorebi/src/process_event.rs | 5 +++ komorebi/src/process_movement.rs | 36 ++++++++++++++++++ komorebi/src/window.rs | 21 ++++++++++ komorebi/src/window_manager.rs | 57 ++++++++++++++++++++++++++++ komorebi/src/window_manager_event.rs | 5 +++ komorebi/src/windows_api.rs | 13 +++++++ komorebi/src/workspace.rs | 32 ++++++++++++++++ 12 files changed, 202 insertions(+), 14 deletions(-) create mode 100644 komorebi/src/process_movement.rs diff --git a/Cargo.lock b/Cargo.lock index a1d043ec..486d7952 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,6 +530,7 @@ dependencies = [ "tracing-subscriber", "uds_windows", "which", + "winput", "winvd", ] @@ -1159,9 +1160,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7" +checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" dependencies = [ "proc-macro2", "quote", @@ -1170,9 +1171,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ef9905d4f98c059046037078f070e66de0f5ac69e5bb63f6bf805b570b3b51" +checksum = "92d77883450d697c0010e60db3d940ed130b0ed81d27485edee981621b434e52" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", @@ -1493,6 +1494,15 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d5cf83fb08083438c5c46723e6206b2970da57ce314f80b57724439aaacab" +[[package]] +name = "winput" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4bec39938e0ae68b300e2a4197b6437f13d53d1c146c6e297e346a71d5dde9" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winvd" version = "0.0.20" diff --git a/komorebi/Cargo.toml b/komorebi/Cargo.toml index ffc97a27..ca16b1a8 100644 --- a/komorebi/Cargo.toml +++ b/komorebi/Cargo.toml @@ -30,6 +30,7 @@ tracing-appender = "0.1" tracing-subscriber = "0.2" uds_windows = "1" which = "4" +winput = "0.2" winvd = "0.0.20" [features] diff --git a/komorebi/src/container.rs b/komorebi/src/container.rs index ec555895..dd772cb0 100644 --- a/komorebi/src/container.rs +++ b/komorebi/src/container.rs @@ -44,6 +44,18 @@ impl Container { } } + pub fn hwnd_from_exe(&self, exe: &str) -> Option { + for window in self.windows() { + if let Ok(window_exe) = window.exe() { + if exe == window_exe { + return Option::from(window.hwnd); + } + } + } + + None + } + pub fn contains_window(&self, hwnd: isize) -> bool { for window in self.windows() { if window.hwnd == hwnd { diff --git a/komorebi/src/main.rs b/komorebi/src/main.rs index 1d8917a1..7a20fa8a 100644 --- a/komorebi/src/main.rs +++ b/komorebi/src/main.rs @@ -25,6 +25,7 @@ use which::which; use crate::process_command::listen_for_commands; use crate::process_event::listen_for_events; +use crate::process_movement::listen_for_movements; use crate::window_manager::WindowManager; use crate::window_manager_event::WindowManagerEvent; use crate::windows_api::WindowsApi; @@ -36,6 +37,7 @@ mod container; mod monitor; mod process_command; mod process_event; +mod process_movement; mod set_window_position; mod styles; mod window; @@ -227,6 +229,7 @@ fn main() -> Result<()> { wm.lock().init()?; listen_for_commands(wm.clone()); listen_for_events(wm.clone()); + listen_for_movements(wm.clone()); load_configuration()?; diff --git a/komorebi/src/process_command.rs b/komorebi/src/process_command.rs index 2e655e55..df2447b2 100644 --- a/komorebi/src/process_command.rs +++ b/komorebi/src/process_command.rs @@ -16,7 +16,6 @@ use komorebi_core::StateQuery; use crate::window_manager; use crate::window_manager::WindowManager; -use crate::windows_api::WindowsApi; use crate::FLOAT_IDENTIFIERS; use crate::MANAGE_IDENTIFIERS; use crate::TRAY_AND_MULTI_WINDOW_CLASSES; @@ -214,18 +213,12 @@ impl WindowManager { } SocketMessage::FocusFollowsMouse(enable) => { if enable { - WindowsApi::enable_focus_follows_mouse()?; + self.autoraise = true; } else { - WindowsApi::disable_focus_follows_mouse()?; - } - } - SocketMessage::ToggleFocusFollowsMouse => { - if WindowsApi::focus_follows_mouse()? { - WindowsApi::disable_focus_follows_mouse()?; - } else { - WindowsApi::enable_focus_follows_mouse()?; + self.autoraise = false; } } + SocketMessage::ToggleFocusFollowsMouse => self.autoraise = !self.autoraise, SocketMessage::ReloadConfiguration => { Self::reload_configuration(); } diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index b97eabfa..4a493506 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -91,6 +91,10 @@ impl WindowManager { } match event { + WindowManagerEvent::Raise(window) => { + window.raise()?; + self.has_pending_raise_op = false; + } WindowManagerEvent::Minimize(_, window) | WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => { @@ -315,6 +319,7 @@ impl WindowManager { .open(hwnd_json)?; serde_json::to_writer_pretty(&file, &known_hwnds)?; + tracing::info!("processed: {}", event.window().to_string()); Ok(()) } diff --git a/komorebi/src/process_movement.rs b/komorebi/src/process_movement.rs new file mode 100644 index 00000000..0cadf234 --- /dev/null +++ b/komorebi/src/process_movement.rs @@ -0,0 +1,36 @@ +use std::sync::Arc; + +use parking_lot::Mutex; +use winput::message_loop; +use winput::message_loop::Event; +use winput::Action; + +use crate::window_manager::WindowManager; + +#[tracing::instrument] +pub fn listen_for_movements(wm: Arc>) { + std::thread::spawn(move || { + let mut ignore_movement = false; + + let receiver = message_loop::start().expect("could not start winput message loop"); + + loop { + match receiver.next_event() { + // Don't want to send any raise events while we are dragging or resizing + Event::MouseButton { action, .. } => match action { + Action::Press => ignore_movement = true, + Action::Release => ignore_movement = false, + }, + Event::MouseMoveRelative { .. } => { + if !ignore_movement { + match wm.lock().raise_window_at_cursor_pos() { + Ok(_) => {} + Err(error) => tracing::error!("{}", error), + } + } + } + _ => {} + } + } + }); +} diff --git a/komorebi/src/window.rs b/komorebi/src/window.rs index 4e877991..4868333a 100644 --- a/komorebi/src/window.rs +++ b/komorebi/src/window.rs @@ -156,6 +156,27 @@ impl Window { WindowsApi::maximize_window(self.hwnd()); } + pub fn raise(self) -> Result<()> { + // Attach komorebi thread to Window thread + let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd()); + let current_thread_id = WindowsApi::current_thread_id(); + WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true)?; + + // Raise Window to foreground + match WindowsApi::set_foreground_window(self.hwnd()) { + Ok(_) => {} + Err(error) => { + tracing::error!( + "could not set as foreground window, but continuing execution of focus(): {}", + error + ); + } + }; + + // This isn't really needed when the above command works as expected via AHK + WindowsApi::set_focus(self.hwnd()) + } + pub fn focus(self) -> Result<()> { // Attach komorebi thread to Window thread let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd()); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 86f3c8ee..f5da1244 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -44,14 +44,17 @@ pub struct WindowManager { pub incoming_events: Arc>>, pub command_listener: UnixListener, pub is_paused: bool, + pub autoraise: bool, pub hotwatch: Hotwatch, pub virtual_desktop_id: Option, + pub has_pending_raise_op: bool, } #[derive(Debug, Serialize)] pub struct State { pub monitors: Ring, pub is_paused: bool, + pub autoraise: bool, pub float_identifiers: Vec, pub manage_identifiers: Vec, pub layered_exe_whitelist: Vec, @@ -65,6 +68,7 @@ impl From<&mut WindowManager> for State { Self { monitors: wm.monitors.clone(), is_paused: wm.is_paused, + autoraise: wm.autoraise, float_identifiers: FLOAT_IDENTIFIERS.lock().clone(), manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(), layered_exe_whitelist: LAYERED_EXE_WHITELIST.lock().clone(), @@ -128,8 +132,10 @@ impl WindowManager { incoming_events: incoming, command_listener: listener, is_paused: false, + autoraise: false, hotwatch: Hotwatch::new()?, virtual_desktop_id, + has_pending_raise_op: false, }) } @@ -360,6 +366,51 @@ impl WindowManager { Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?) } + #[tracing::instrument(skip(self))] + pub fn raise_window_at_cursor_pos(&mut self) -> Result<()> { + if !self.autoraise { + return Ok(()); + } + + if self.has_pending_raise_op { + Ok(()) + } else { + let mut hwnd = WindowsApi::window_at_cursor_pos()?; + let mut known_hwnd = false; + for monitor in self.monitors() { + for workspace in monitor.workspaces() { + if workspace.contains_window(hwnd) { + known_hwnd = true; + } + } + } + + if !known_hwnd { + let class = Window { hwnd }.class()?; + // Just Chromium and Electron fucking up everything, again + if class == *"Chrome_RenderWidgetHostHWND" { + for monitor in self.monitors() { + for workspace in monitor.workspaces() { + if let Some(exe_hwnd) = workspace.hwnd_from_exe(&Window { hwnd }.exe()?) + { + hwnd = exe_hwnd; + known_hwnd = true; + } + } + } + } + } + + if known_hwnd && self.focused_window()?.hwnd != hwnd { + let event = WindowManagerEvent::Raise(Window { hwnd }); + self.has_pending_raise_op = true; + Ok(WINEVENT_CALLBACK_CHANNEL.lock().0.send(event)?) + } else { + Ok(()) + } + } + } + #[tracing::instrument(skip(self))] pub fn update_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> { tracing::info!("updating"); @@ -1080,6 +1131,12 @@ impl WindowManager { .ok_or_else(|| anyhow!("there is no container")) } + pub fn focused_window(&self) -> Result<&Window> { + self.focused_container()? + .focused_window() + .ok_or_else(|| anyhow!("there is no window")) + } + fn focused_window_mut(&mut self) -> Result<&mut Window> { self.focused_container_mut()? .focused_window_mut() diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index e97c5d78..1547fe9b 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -16,6 +16,7 @@ pub enum WindowManagerEvent { MouseCapture(WinEvent, Window), Manage(Window), Unmanage(Window), + Raise(Window), } impl Display for WindowManagerEvent { @@ -60,6 +61,9 @@ impl Display for WindowManagerEvent { winevent, window ) } + WindowManagerEvent::Raise(window) => { + write!(f, "Raise (Window: {})", window) + } } } } @@ -74,6 +78,7 @@ impl WindowManagerEvent { | WindowManagerEvent::Show(_, window) | WindowManagerEvent::MoveResizeEnd(_, window) | WindowManagerEvent::MouseCapture(_, window) + | WindowManagerEvent::Raise(window) | WindowManagerEvent::Manage(window) | WindowManagerEvent::Unmanage(window) => window, } diff --git a/komorebi/src/windows_api.rs b/komorebi/src/windows_api.rs index 7e5ffcd9..a688756a 100644 --- a/komorebi/src/windows_api.rs +++ b/komorebi/src/windows_api.rs @@ -60,6 +60,7 @@ use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW; use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowPos; use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow; use bindings::Windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW; +use bindings::Windows::Win32::UI::WindowsAndMessaging::WindowFromPoint; use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE; use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_STYLE; use bindings::Windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT; @@ -349,6 +350,14 @@ impl WindowsApi { Ok(cursor_pos) } + pub fn window_from_point(point: POINT) -> Result { + Result::from(WindowsResult::from(unsafe { WindowFromPoint(point) })) + } + + pub fn window_at_cursor_pos() -> Result { + Self::window_from_point(Self::cursor_pos()?) + } + pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> { Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2)) } @@ -554,6 +563,7 @@ impl WindowsApi { )) } + #[allow(dead_code)] pub fn system_parameters_info_w( action: SYSTEM_PARAMETERS_INFO_ACTION, ui_param: u32, @@ -565,6 +575,7 @@ impl WindowsApi { })) } + #[allow(dead_code)] pub fn focus_follows_mouse() -> Result { let mut is_enabled: BOOL = unsafe { std::mem::zeroed() }; @@ -578,6 +589,7 @@ impl WindowsApi { Ok(is_enabled.into()) } + #[allow(dead_code)] pub fn enable_focus_follows_mouse() -> Result<()> { Self::system_parameters_info_w( SPI_SETACTIVEWINDOWTRACKING, @@ -587,6 +599,7 @@ impl WindowsApi { ) } + #[allow(dead_code)] pub fn disable_focus_follows_mouse() -> Result<()> { Self::system_parameters_info_w( SPI_SETACTIVEWINDOWTRACKING, diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index 32d07172..b23f3cd0 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -263,6 +263,38 @@ impl Workspace { idx } + pub fn hwnd_from_exe(&self, exe: &str) -> Option { + for container in self.containers() { + if let Some(hwnd) = container.hwnd_from_exe(exe) { + return Option::from(hwnd); + } + } + + if let Some(window) = self.maximized_window() { + if let Ok(window_exe) = window.exe() { + if exe == window_exe { + return Option::from(window.hwnd); + } + } + } + + if let Some(container) = self.monocle_container() { + if let Some(hwnd) = container.hwnd_from_exe(exe) { + return Option::from(hwnd); + } + } + + for window in self.floating_windows() { + if let Ok(window_exe) = window.exe() { + if exe == window_exe { + return Option::from(window.hwnd); + } + } + } + + None + } + pub fn contains_window(&self, hwnd: isize) -> bool { for container in self.containers() { if container.contains_window(hwnd) {