From 7fd545ca35e75788f9c8d995157b38786b7377d0 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 29 Oct 2021 09:08:35 -0700 Subject: [PATCH] fix(wm): handle cross-monitor drag/move events This commit ensures that when a window is dragged across a monitor boundary, the ownership of the window container will be transferred to the target monitor's currently focused workspace. In order to achieve this, a new WindowManagerEvent variant has been added, MoveResizeStart, which will store an optional pending_move_op on the WindowManager struct. This must be consumed at the beginning of the handler for MoveResizeEnd. This is necessary because as soon as the window is dragged across a monitor boundary, an event is sent (and handled) to update the currently focused monitor and workspace as the target monitor and workspace, and we still need to have the information about the original monitor, workspace and container in order to make comparisons and ultimately remove the origin container to be able to transfer it. fix #58 --- justfile | 2 +- komorebi.sample.with.lib.ahk | 2 +- komorebi/src/process_event.rs | 119 +++++++++++++++++++++++++-- komorebi/src/window_manager.rs | 89 ++++++++++++++++++++ komorebi/src/window_manager_event.rs | 10 +++ komorebi/src/workspace.rs | 7 ++ 6 files changed, 218 insertions(+), 11 deletions(-) diff --git a/justfile b/justfile index 4534bf90..e76fa626 100644 --- a/justfile +++ b/justfile @@ -13,7 +13,7 @@ install-komorebic: cargo +stable install --path komorebic --locked install-komorebi: - cargo +stable install --path komorebic --locked + cargo +stable install --path komorebi --locked install: just install-komorebic diff --git a/komorebi.sample.with.lib.ahk b/komorebi.sample.with.lib.ahk index 89c39350..8e09a196 100644 --- a/komorebi.sample.with.lib.ahk +++ b/komorebi.sample.with.lib.ahk @@ -34,7 +34,7 @@ WorkspaceTiling(0, 4, "disable") ; Everything floats here ; Configure floating rules FloatRule("class", "SunAwtDialog") ; All the IntelliJ popups -FloatRule("title", "Control Panek") +FloatRule("title", "Control Panel") FloatRule("class", "TaskManagerWindow") FloatRule("exe", "Wally.exe") FloatRule("exe", "wincompose.exe") diff --git a/komorebi/src/process_event.rs b/komorebi/src/process_event.rs index 3073e429..eaaead1e 100644 --- a/komorebi/src/process_event.rs +++ b/komorebi/src/process_event.rs @@ -208,7 +208,32 @@ impl WindowManager { self.update_focused_workspace(false)?; } } + WindowManagerEvent::MoveResizeStart(_, _) => { + let monitor_idx = self.focused_monitor_idx(); + let workspace_idx = self + .focused_monitor() + .ok_or_else(|| anyhow!("there is no monitor with this idx"))? + .focused_workspace_idx(); + let container_idx = self + .focused_monitor() + .ok_or_else(|| anyhow!("there is no monitor with this idx"))? + .focused_workspace() + .ok_or_else(|| anyhow!("there is no workspace with this idx"))? + .focused_container_idx(); + + self.pending_move_op = Option::from((monitor_idx, workspace_idx, container_idx)); + } WindowManagerEvent::MoveResizeEnd(_, window) => { + // We need this because if the event ends on a different monitor, + // that monitor will already have been focused and updated in the state + let pending = self.pending_move_op; + // Always consume the pending move op whenever this event is handled + self.pending_move_op = None; + + let target_monitor_idx = self + .monitor_idx_from_current_pos() + .ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?; + let workspace = self.focused_workspace_mut()?; if workspace .floating_windows() @@ -218,12 +243,32 @@ impl WindowManager { return Ok(()); } - let focused_idx = workspace.focused_container_idx(); + let focused_container_idx = workspace.focused_container_idx(); + + let mut new_position = WindowsApi::window_rect(window.hwnd())?; + let old_position = *workspace .latest_layout() - .get(focused_idx) - .ok_or_else(|| anyhow!("there is no latest layout"))?; - let mut new_position = WindowsApi::window_rect(window.hwnd())?; + .get(focused_container_idx) + // If the move was to another monitor with an empty workspace, the + // workspace here will refer to that empty workspace, which won't + // have any latest layout set. We fall back to a Default for Rect + // which allows us to make a reasonable guess that the drag has taken + // place across a monitor boundary to an empty workspace + .unwrap_or(&Rect::default()); + + // This will be true if we have moved to an empty workspace on another monitor + let mut moved_across_monitors = old_position == Rect::default(); + + if let Some((origin_monitor_idx, _, _)) = pending { + // If we didn't move to another monitor with an empty workspace, it is + // still possible that we moved to another monitor with a populated workspace + if !moved_across_monitors { + // So we'll check if the origin monitor index and the target monitor index + // are different, if they are, we can set the override + moved_across_monitors = origin_monitor_idx != target_monitor_idx; + } + } // Adjust for the invisible borders new_position.left += invisible_borders.left; @@ -238,16 +283,72 @@ impl WindowManager { bottom: new_position.bottom - old_position.bottom, }; - let is_move = resize.right == 0 && resize.bottom == 0; + // If we have moved across the monitors, use that override, otherwise determine + // if a move has taken place by ruling out a resize + let is_move = moved_across_monitors || resize.right == 0 && resize.bottom == 0; if is_move { tracing::info!("moving with mouse"); - match workspace.container_idx_from_current_point() { - Some(target_idx) => { - workspace.swap_containers(focused_idx, target_idx); + + if moved_across_monitors { + if let Some(( + origin_monitor_idx, + origin_workspace_idx, + origin_container_idx, + )) = pending + { + let target_workspace_idx = self + .monitors() + .get(target_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this idx"))? + .focused_workspace_idx(); + + let target_container_idx = self + .monitors() + .get(target_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this idx"))? + .focused_workspace() + .ok_or_else(|| { + anyhow!("there is no focused workspace for this monitor") + })? + .container_idx_from_current_point() + // Default to 0 in the case of an empty workspace + .unwrap_or(0); + + self.transfer_container( + ( + origin_monitor_idx, + origin_workspace_idx, + origin_container_idx, + ), + ( + target_monitor_idx, + target_workspace_idx, + target_container_idx, + ), + )?; + + // We want to make sure both the origin and target monitors are updated, + // so that we don't have ghost tiles until we force an interaction on + // the origin monitor's focused workspace + self.focus_monitor(origin_monitor_idx)?; + self.focus_workspace(origin_workspace_idx)?; + self.update_focused_workspace(false)?; + + self.focus_monitor(target_monitor_idx)?; + self.focus_workspace(target_workspace_idx)?; self.update_focused_workspace(false)?; } - None => self.update_focused_workspace(true)?, + } else { + // Here we handle a simple move on the same monitor which is treated as + // a container swap + match workspace.container_idx_from_current_point() { + Some(target_idx) => { + workspace.swap_containers(focused_container_idx, target_idx); + self.update_focused_workspace(false)?; + } + None => self.update_focused_workspace(true)?, + } } } else { tracing::info!("resizing with mouse"); diff --git a/komorebi/src/window_manager.rs b/komorebi/src/window_manager.rs index 5a816444..d630110b 100644 --- a/komorebi/src/window_manager.rs +++ b/komorebi/src/window_manager.rs @@ -53,6 +53,7 @@ pub struct WindowManager { pub hotwatch: Hotwatch, pub virtual_desktop_id: Option, pub has_pending_raise_op: bool, + pub pending_move_op: Option<(usize, usize, usize)>, } #[derive(Debug, Serialize)] @@ -153,6 +154,7 @@ impl WindowManager { hotwatch: Hotwatch::new()?, virtual_desktop_id, has_pending_raise_op: false, + pending_move_op: None, }) } @@ -538,6 +540,93 @@ impl WindowManager { } } + #[tracing::instrument(skip(self))] + pub fn transfer_container( + &mut self, + origin: (usize, usize, usize), + target: (usize, usize, usize), + ) -> Result<()> { + let (origin_monitor_idx, origin_workspace_idx, origin_container_idx) = origin; + let (target_monitor_idx, target_workspace_idx, target_container_idx) = target; + + let origin_container = self + .monitors_mut() + .get_mut(origin_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .workspaces_mut() + .get_mut(origin_workspace_idx) + .ok_or_else(|| anyhow!("there is no workspace at this index"))? + .remove_container(origin_container_idx) + .ok_or_else(|| anyhow!("there is no container at this index"))?; + + let target_workspace = self + .monitors_mut() + .get_mut(target_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .workspaces_mut() + .get_mut(target_workspace_idx) + .ok_or_else(|| anyhow!("there is no workspace at this index"))?; + + target_workspace + .containers_mut() + .insert(target_container_idx, origin_container); + + target_workspace.focus_container(target_container_idx); + + Ok(()) + } + + #[tracing::instrument(skip(self))] + pub fn swap_containers( + &mut self, + origin: (usize, usize, usize), + target: (usize, usize, usize), + ) -> Result<()> { + let (origin_monitor_idx, origin_workspace_idx, origin_container_idx) = origin; + let (target_monitor_idx, target_workspace_idx, target_container_idx) = target; + + let origin_container = self + .monitors_mut() + .get_mut(origin_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .workspaces_mut() + .get_mut(origin_workspace_idx) + .ok_or_else(|| anyhow!("there is no workspace at this index"))? + .remove_container(origin_container_idx) + .ok_or_else(|| anyhow!("there is no container at this index"))?; + + let target_container = self + .monitors_mut() + .get_mut(target_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .workspaces_mut() + .get_mut(target_workspace_idx) + .ok_or_else(|| anyhow!("there is no workspace at this index"))? + .remove_container(target_container_idx); + + self.monitors_mut() + .get_mut(target_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .workspaces_mut() + .get_mut(target_workspace_idx) + .ok_or_else(|| anyhow!("there is no workspace at this index"))? + .containers_mut() + .insert(target_container_idx, origin_container); + + if let Some(target_container) = target_container { + self.monitors_mut() + .get_mut(origin_monitor_idx) + .ok_or_else(|| anyhow!("there is no monitor at this index"))? + .workspaces_mut() + .get_mut(origin_workspace_idx) + .ok_or_else(|| anyhow!("there is no workspace at this index"))? + .containers_mut() + .insert(origin_container_idx, target_container); + } + + Ok(()) + } + #[tracing::instrument(skip(self))] pub fn update_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> { tracing::info!("updating"); diff --git a/komorebi/src/window_manager_event.rs b/komorebi/src/window_manager_event.rs index 07913e53..5f70c1bd 100644 --- a/komorebi/src/window_manager_event.rs +++ b/komorebi/src/window_manager_event.rs @@ -15,6 +15,7 @@ pub enum WindowManagerEvent { Hide(WinEvent, Window), Minimize(WinEvent, Window), Show(WinEvent, Window), + MoveResizeStart(WinEvent, Window), MoveResizeEnd(WinEvent, Window), MouseCapture(WinEvent, Window), Manage(Window), @@ -51,6 +52,13 @@ impl Display for WindowManagerEvent { WindowManagerEvent::Show(winevent, window) => { write!(f, "Show (WinEvent: {}, Window: {})", winevent, window) } + WindowManagerEvent::MoveResizeStart(winevent, window) => { + write!( + f, + "MoveResizeStart (WinEvent: {}, Window: {})", + winevent, window + ) + } WindowManagerEvent::MoveResizeEnd(winevent, window) => { write!( f, @@ -87,6 +95,7 @@ impl WindowManagerEvent { | WindowManagerEvent::Hide(_, window) | WindowManagerEvent::Minimize(_, window) | WindowManagerEvent::Show(_, window) + | WindowManagerEvent::MoveResizeStart(_, window) | WindowManagerEvent::MoveResizeEnd(_, window) | WindowManagerEvent::MouseCapture(_, window) | WindowManagerEvent::MonitorPoll(_, window) @@ -113,6 +122,7 @@ impl WindowManagerEvent { WinEvent::ObjectFocus | WinEvent::SystemForeground => { Option::from(Self::FocusChange(winevent, window)) } + WinEvent::SystemMoveSizeStart => Option::from(Self::MoveResizeStart(winevent, window)), WinEvent::SystemMoveSizeEnd => Option::from(Self::MoveResizeEnd(winevent, window)), WinEvent::SystemCaptureStart | WinEvent::SystemCaptureEnd => { Option::from(Self::MouseCapture(winevent, window)) diff --git a/komorebi/src/workspace.rs b/komorebi/src/workspace.rs index b841e3ce..d1b965f7 100644 --- a/komorebi/src/workspace.rs +++ b/komorebi/src/workspace.rs @@ -471,6 +471,13 @@ impl Workspace { container } + pub fn remove_container(&mut self, idx: usize) -> Option { + let container = self.remove_container_by_idx(idx); + self.focus_previous_container(); + + container + } + pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option { let len = NonZeroUsize::new(self.containers().len())?;