@@ -705,21 +705,43 @@ impl WmState { |
| 705 | 705 | let geoms = |
| 706 | 706 | ws.tree |
| 707 | 707 | .calculate_geometries_with_gaps(sr, self.gap_inner, self.gap_outer, true); |
| 708 | | - if let Some(target) = Node::find_adjacent(&geoms, from, direction) { |
| 709 | | - self.focus_window(target); |
| 710 | | - if self.mouse_follows_focus |
| 711 | | - && let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == target) |
| 712 | | - { |
| 713 | | - warp_mouse_to_center(rect); |
| 714 | | - self.ffm_cooldown_until = |
| 715 | | - Some(std::time::Instant::now() + std::time::Duration::from_millis(200)); |
| 716 | | - self.ffm_last_window = Some(target); |
| 708 | + |
| 709 | + // Check if focused window is at the monitor edge in the requested |
| 710 | + // direction. If so, cross monitors instead of spiraling into the BSP tree. |
| 711 | + let at_edge = if let Some((_, rect)) = geoms.iter().find(|(w, _)| *w == from) { |
| 712 | + let tolerance = self.gap_outer + 2.0; |
| 713 | + match direction { |
| 714 | + Direction::Right => { |
| 715 | + (rect.x + rect.width) >= (sr.x + sr.width - tolerance) |
| 716 | + } |
| 717 | + Direction::Left => rect.x <= (sr.x + tolerance), |
| 718 | + Direction::Down => { |
| 719 | + (rect.y + rect.height) >= (sr.y + sr.height - tolerance) |
| 720 | + } |
| 721 | + Direction::Up => rect.y <= (sr.y + tolerance), |
| 722 | + } |
| 723 | + } else { |
| 724 | + false |
| 725 | + }; |
| 726 | + |
| 727 | + if !at_edge { |
| 728 | + if let Some(target) = Node::find_adjacent(&geoms, from, direction) { |
| 729 | + self.focus_window(target); |
| 730 | + if self.mouse_follows_focus |
| 731 | + && let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == target) |
| 732 | + { |
| 733 | + warp_mouse_to_center(rect); |
| 734 | + self.ffm_cooldown_until = Some( |
| 735 | + std::time::Instant::now() + std::time::Duration::from_millis(200), |
| 736 | + ); |
| 737 | + self.ffm_last_window = Some(target); |
| 738 | + } |
| 739 | + return; |
| 717 | 740 | } |
| 718 | | - return; |
| 719 | 741 | } |
| 720 | 742 | } |
| 721 | 743 | |
| 722 | | - // No adjacent window (or empty workspace) — try crossing monitors |
| 744 | + // At monitor edge or no adjacent window — try crossing monitors |
| 723 | 745 | if self.monitors.len() <= 1 { |
| 724 | 746 | return; |
| 725 | 747 | } |
@@ -779,17 +801,41 @@ impl WmState { |
| 779 | 801 | None => return, |
| 780 | 802 | }; |
| 781 | 803 | let sr = self.focused_rect(); |
| 782 | | - let geoms = ws.tree.calculate_geometries(sr); |
| 783 | | - if let Some(target) = Node::find_adjacent(&geoms, focused, direction) { |
| 784 | | - tracing::debug!(focused, target, ?direction, "swap_direction"); |
| 785 | | - if self.active_workspace_mut().tree.swap(focused, target) { |
| 786 | | - self.apply_layout(); |
| 787 | | - self.fix_oversized_windows(); |
| 804 | + let geoms = ws.tree.calculate_geometries_with_gaps( |
| 805 | + sr, |
| 806 | + self.gap_inner, |
| 807 | + self.gap_outer, |
| 808 | + true, |
| 809 | + ); |
| 810 | + |
| 811 | + // Check if the focused window touches the monitor edge in the |
| 812 | + // requested direction. If so, skip intra-workspace swap and move |
| 813 | + // to the adjacent monitor. This prevents the BSP spiral from |
| 814 | + // trapping windows at the edge. |
| 815 | + let at_edge = if let Some((_, rect)) = geoms.iter().find(|(w, _)| *w == focused) { |
| 816 | + let tolerance = self.gap_outer + 2.0; |
| 817 | + match direction { |
| 818 | + Direction::Right => (rect.x + rect.width) >= (sr.x + sr.width - tolerance), |
| 819 | + Direction::Left => rect.x <= (sr.x + tolerance), |
| 820 | + Direction::Down => (rect.y + rect.height) >= (sr.y + sr.height - tolerance), |
| 821 | + Direction::Up => rect.y <= (sr.y + tolerance), |
| 822 | + } |
| 823 | + } else { |
| 824 | + false |
| 825 | + }; |
| 826 | + |
| 827 | + if !at_edge { |
| 828 | + if let Some(target) = Node::find_adjacent(&geoms, focused, direction) { |
| 829 | + tracing::debug!(focused, target, ?direction, "swap_direction"); |
| 830 | + if self.active_workspace_mut().tree.swap(focused, target) { |
| 831 | + self.apply_layout(); |
| 832 | + self.fix_oversized_windows(); |
| 833 | + } |
| 834 | + return; |
| 788 | 835 | } |
| 789 | | - return; |
| 790 | 836 | } |
| 791 | 837 | |
| 792 | | - // No adjacent window — move window to adjacent monitor |
| 838 | + // At monitor edge or no adjacent window — move to adjacent monitor |
| 793 | 839 | if self.monitors.len() <= 1 { |
| 794 | 840 | return; |
| 795 | 841 | } |