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