Skip to content

Commit

Permalink
fix(wm): handle cross-monitor drag/move events
Browse files Browse the repository at this point in the history
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
  • Loading branch information
LGUG2Z committed Oct 29, 2021
1 parent 14e6329 commit 7fd545c
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 11 deletions.
2 changes: 1 addition & 1 deletion justfile
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion komorebi.sample.with.lib.ahk
Expand Up @@ -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")
Expand Down
119 changes: 110 additions & 9 deletions komorebi/src/process_event.rs
Expand Up @@ -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()
Expand All @@ -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;
Expand All @@ -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)?;
}
} 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)?,
}
None => self.update_focused_workspace(true)?,
}
} else {
tracing::info!("resizing with mouse");
Expand Down
89 changes: 89 additions & 0 deletions komorebi/src/window_manager.rs
Expand Up @@ -53,6 +53,7 @@ pub struct WindowManager {
pub hotwatch: Hotwatch,
pub virtual_desktop_id: Option<usize>,
pub has_pending_raise_op: bool,
pub pending_move_op: Option<(usize, usize, usize)>,
}

#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -153,6 +154,7 @@ impl WindowManager {
hotwatch: Hotwatch::new()?,
virtual_desktop_id,
has_pending_raise_op: false,
pending_move_op: None,
})
}

Expand Down Expand Up @@ -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");
Expand Down
10 changes: 10 additions & 0 deletions komorebi/src/window_manager_event.rs
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down
7 changes: 7 additions & 0 deletions komorebi/src/workspace.rs
Expand Up @@ -471,6 +471,13 @@ impl Workspace {
container
}

pub fn remove_container(&mut self, idx: usize) -> Option<Container> {
let container = self.remove_container_by_idx(idx);
self.focus_previous_container();

container
}

pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
let len = NonZeroUsize::new(self.containers().len())?;

Expand Down

0 comments on commit 7fd545c

Please sign in to comment.