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
This commit is contained in:
LGUG2Z
2021-09-04 09:41:45 -07:00
parent 2a4e6fa6da
commit ce3c742e09
12 changed files with 202 additions and 14 deletions

18
Cargo.lock generated
View File

@@ -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"

View File

@@ -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]

View File

@@ -44,6 +44,18 @@ impl Container {
}
}
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
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 {

View File

@@ -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()?;

View File

@@ -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();
}

View File

@@ -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(())
}

View File

@@ -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<Mutex<WindowManager>>) {
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),
}
}
}
_ => {}
}
}
});
}

View File

@@ -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());

View File

@@ -44,14 +44,17 @@ pub struct WindowManager {
pub incoming_events: Arc<Mutex<Receiver<WindowManagerEvent>>>,
pub command_listener: UnixListener,
pub is_paused: bool,
pub autoraise: bool,
pub hotwatch: Hotwatch,
pub virtual_desktop_id: Option<usize>,
pub has_pending_raise_op: bool,
}
#[derive(Debug, Serialize)]
pub struct State {
pub monitors: Ring<Monitor>,
pub is_paused: bool,
pub autoraise: bool,
pub float_identifiers: Vec<String>,
pub manage_identifiers: Vec<String>,
pub layered_exe_whitelist: Vec<String>,
@@ -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()

View File

@@ -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,
}

View File

@@ -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<isize> {
Result::from(WindowsResult::from(unsafe { WindowFromPoint(point) }))
}
pub fn window_at_cursor_pos() -> Result<isize> {
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<bool> {
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,

View File

@@ -263,6 +263,38 @@ impl Workspace {
idx
}
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
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) {