@@ -52,6 +52,7 @@ pub struct WmState { |
| 52 | 52 | event_queue: Rc<RefCell<Vec<QueuedEvent>>>, |
| 53 | 53 | ffm_cooldown_until: Option<std::time::Instant>, |
| 54 | 54 | ffm_last_window: Option<WindowId>, |
| 55 | + focus_return_memory: HashMap<(WindowId, super::tree::Direction), WindowId>, |
| 55 | 56 | pending_internal_focus: Option<(WindowId, i32, std::time::Instant)>, |
| 56 | 57 | drag: Option<DragState>, |
| 57 | 58 | pub focus_follows_mouse: bool, |
@@ -88,6 +89,7 @@ impl WmState { |
| 88 | 89 | event_queue: Rc::new(RefCell::new(Vec::new())), |
| 89 | 90 | ffm_cooldown_until: None, |
| 90 | 91 | ffm_last_window: None, |
| 92 | + focus_return_memory: HashMap::new(), |
| 91 | 93 | pending_internal_focus: None, |
| 92 | 94 | drag: None, |
| 93 | 95 | focus_follows_mouse: true, |
@@ -702,8 +704,16 @@ impl WmState { |
| 702 | 704 | false |
| 703 | 705 | }; |
| 704 | 706 | |
| 705 | | - if !at_edge && let Some(target) = Node::find_adjacent(&geoms, from, direction) { |
| 707 | + if !at_edge && let Some(default_target) = Node::find_adjacent(&geoms, from, direction) { |
| 708 | + let candidates = Node::adjacent_candidates(&geoms, from, direction); |
| 709 | + let target = self |
| 710 | + .focus_return_memory |
| 711 | + .get(&(from, direction)) |
| 712 | + .copied() |
| 713 | + .filter(|remembered| candidates.contains(remembered)) |
| 714 | + .unwrap_or(default_target); |
| 706 | 715 | self.focus_window(target); |
| 716 | + self.remember_focus_transition(from, direction, target); |
| 707 | 717 | if self.mouse_follows_focus |
| 708 | 718 | && let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == target) |
| 709 | 719 | { |
@@ -892,6 +902,32 @@ impl WmState { |
| 892 | 902 | Some(std::time::Instant::now() + std::time::Duration::from_millis(200)); |
| 893 | 903 | } |
| 894 | 904 | |
| 905 | + fn opposite_direction(direction: super::tree::Direction) -> super::tree::Direction { |
| 906 | + use super::tree::Direction; |
| 907 | + match direction { |
| 908 | + Direction::Left => Direction::Right, |
| 909 | + Direction::Right => Direction::Left, |
| 910 | + Direction::Up => Direction::Down, |
| 911 | + Direction::Down => Direction::Up, |
| 912 | + } |
| 913 | + } |
| 914 | + |
| 915 | + fn remember_focus_transition( |
| 916 | + &mut self, |
| 917 | + from: WindowId, |
| 918 | + direction: super::tree::Direction, |
| 919 | + to: WindowId, |
| 920 | + ) { |
| 921 | + self.focus_return_memory.insert((from, direction), to); |
| 922 | + self.focus_return_memory |
| 923 | + .insert((to, Self::opposite_direction(direction)), from); |
| 924 | + } |
| 925 | + |
| 926 | + fn prune_focus_memory_for_window(&mut self, window: WindowId) { |
| 927 | + self.focus_return_memory |
| 928 | + .retain(|(from, _), to| *from != window && *to != window); |
| 929 | + } |
| 930 | + |
| 895 | 931 | fn mark_internal_focus(&mut self, id: WindowId) { |
| 896 | 932 | let Some(window) = self.registry.get(id) else { |
| 897 | 933 | return; |
@@ -2586,6 +2622,7 @@ impl WmState { |
| 2586 | 2622 | self.borders.remove_border(id); |
| 2587 | 2623 | self.registry.remove(id); |
| 2588 | 2624 | self.ax_refs.remove(&id); |
| 2625 | + self.prune_focus_memory_for_window(id); |
| 2589 | 2626 | |
| 2590 | 2627 | // Find the workspace containing this window and remove from it |
| 2591 | 2628 | if let Some(ws_idx) = self.workspaces.find_window(id) { |
@@ -2641,6 +2678,7 @@ impl WmState { |
| 2641 | 2678 | self.observers.remove(&pid); |
| 2642 | 2679 | for w in &removed { |
| 2643 | 2680 | self.ax_refs.remove(&w.id); |
| 2681 | + self.prune_focus_memory_for_window(w.id); |
| 2644 | 2682 | // Find the workspace containing this window and remove from it |
| 2645 | 2683 | if let Some(ws_idx) = self.workspaces.find_window(w.id) { |
| 2646 | 2684 | let ws = self.workspaces.get_mut(ws_idx); |
@@ -2888,6 +2926,7 @@ impl WmState { |
| 2888 | 2926 | tracing::info!(id, app = app_name, "window destroyed -> retiling"); |
| 2889 | 2927 | self.registry.remove(id); |
| 2890 | 2928 | self.ax_refs.remove(&id); |
| 2929 | + self.prune_focus_memory_for_window(id); |
| 2891 | 2930 | // Find the workspace containing this window and remove from it |
| 2892 | 2931 | if let Some(ws_idx) = self.workspaces.find_window(id) { |
| 2893 | 2932 | let ws = self.workspaces.get_mut(ws_idx); |
@@ -3368,6 +3407,43 @@ mod tests { |
| 3368 | 3407 | assert_eq!(state.active_workspace().focused, Some(3)); |
| 3369 | 3408 | } |
| 3370 | 3409 | |
| 3410 | + #[test] |
| 3411 | + fn focus_direction_returns_to_last_ambiguous_tile() { |
| 3412 | + let mut state = WmState::new(); |
| 3413 | + state.monitors = vec![Monitor { |
| 3414 | + id: 42, |
| 3415 | + frame: Rect::new(0.0, 0.0, 1920.0, 1080.0), |
| 3416 | + usable_frame: Rect::new(0.0, 33.0, 1920.0, 1047.0), |
| 3417 | + is_primary: true, |
| 3418 | + active_workspace: 0, |
| 3419 | + }]; |
| 3420 | + state.sync_workspace_visibility(); |
| 3421 | + let screen = state.monitors[0].usable_frame; |
| 3422 | + |
| 3423 | + { |
| 3424 | + let ws = state.workspaces.get_mut(0); |
| 3425 | + ws.tree.insert_with_rect(1, None, screen); |
| 3426 | + ws.tree.insert_with_rect(2, Some(1), screen); |
| 3427 | + ws.tree.insert_with_rect(3, Some(2), screen); |
| 3428 | + ws.record_focus(3); |
| 3429 | + } |
| 3430 | + |
| 3431 | + state.focus_direction(Direction::Left); |
| 3432 | + assert_eq!(state.active_workspace().focused, Some(1)); |
| 3433 | + |
| 3434 | + state.focus_direction(Direction::Right); |
| 3435 | + assert_eq!(state.active_workspace().focused, Some(3)); |
| 3436 | + |
| 3437 | + state.focus_direction(Direction::Up); |
| 3438 | + assert_eq!(state.active_workspace().focused, Some(2)); |
| 3439 | + |
| 3440 | + state.focus_direction(Direction::Left); |
| 3441 | + assert_eq!(state.active_workspace().focused, Some(1)); |
| 3442 | + |
| 3443 | + state.focus_direction(Direction::Right); |
| 3444 | + assert_eq!(state.active_workspace().focused, Some(2)); |
| 3445 | + } |
| 3446 | + |
| 3371 | 3447 | #[test] |
| 3372 | 3448 | fn mouse_hover_on_inactive_stack_sliver_does_not_change_focus() { |
| 3373 | 3449 | let mut state = WmState::new(); |