gardesk/gar / a231cc9

Browse files

Add directional focus memory for window navigation

When navigating between windows with arrow keys, gar now remembers which
window was last focused in each direction. This fixes the issue where
navigating back and forth between a tall window and stacked windows would
always default to the topmost window instead of returning to the previously
focused one.

The memory is stored only when windows share the same row (for Left/Right)
or column (for Up/Down), preventing interference with natural navigation.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
a231cc99e8694fa5deeec1e64aff7593a611f743
Parents
7056e22
Tree
0236679

3 changed files

StatusFile+-
M gar/src/core/mod.rs 9 0
M gar/src/core/tree.rs 11 1
M gar/src/x11/events.rs 43 3
gar/src/core/mod.rsmodified
@@ -47,6 +47,9 @@ pub struct WindowManager {
47
     pub current_edge_cursor: Option<(XWindow, crate::x11::events::ResizeEdge)>,
47
     pub current_edge_cursor: Option<(XWindow, crate::x11::events::ResizeEdge)>,
48
     /// garbar child process (managed automatically when gar.bar is configured)
48
     /// garbar child process (managed automatically when gar.bar is configured)
49
     pub garbar_process: Option<std::process::Child>,
49
     pub garbar_process: Option<std::process::Child>,
50
+    /// Directional focus memory: (source_window, direction) -> last_target_window
51
+    /// Used to remember which window was focused when navigating in a direction
52
+    pub directional_focus_memory: HashMap<(XWindow, Direction), XWindow>,
50
 }
53
 }
51
 
54
 
52
 impl WindowManager {
55
 impl WindowManager {
@@ -143,6 +146,7 @@ impl WindowManager {
143
             dock_struts: HashMap::new(),
146
             dock_struts: HashMap::new(),
144
             current_edge_cursor: None,
147
             current_edge_cursor: None,
145
             garbar_process: None,
148
             garbar_process: None,
149
+            directional_focus_memory: HashMap::new(),
146
         })
150
         })
147
     }
151
     }
148
 
152
 
@@ -434,6 +438,11 @@ impl WindowManager {
434
             // Remove from focus history
438
             // Remove from focus history
435
             self.focus_history.retain(|&w| w != window);
439
             self.focus_history.retain(|&w| w != window);
436
 
440
 
441
+            // Remove directional focus memory entries involving this window
442
+            self.directional_focus_memory.retain(|(src, _), tgt| {
443
+                *src != window && *tgt != window
444
+            });
445
+
437
             // Update focus if this was the focused window
446
             // Update focus if this was the focused window
438
             if self.focused_window == Some(window) {
447
             if self.focused_window == Some(window) {
439
                 // Find next window from focus history that's on this workspace
448
                 // Find next window from focus history that's on this workspace
gar/src/core/tree.rsmodified
@@ -6,7 +6,7 @@ pub enum SplitDirection {
6
     Vertical,
6
     Vertical,
7
 }
7
 }
8
 
8
 
9
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10
 pub enum Direction {
10
 pub enum Direction {
11
     Left,
11
     Left,
12
     Right,
12
     Right,
@@ -286,10 +286,13 @@ impl Node {
286
     }
286
     }
287
 
287
 
288
     /// Find adjacent window in a direction, given geometries.
288
     /// Find adjacent window in a direction, given geometries.
289
+    /// If `preferred` is Some and is a valid candidate, it will be returned.
290
+    /// This enables "window memory" - remembering which window was last focused in a direction.
289
     pub fn find_adjacent(
291
     pub fn find_adjacent(
290
         geometries: &[(XWindow, Rect)],
292
         geometries: &[(XWindow, Rect)],
291
         from: XWindow,
293
         from: XWindow,
292
         direction: Direction,
294
         direction: Direction,
295
+        preferred: Option<XWindow>,
293
     ) -> Option<XWindow> {
296
     ) -> Option<XWindow> {
294
         let from_rect = geometries.iter().find(|(w, _)| *w == from)?.1;
297
         let from_rect = geometries.iter().find(|(w, _)| *w == from)?.1;
295
 
298
 
@@ -316,6 +319,13 @@ impl Node {
316
             })
319
             })
317
             .collect();
320
             .collect();
318
 
321
 
322
+        // If preferred window is a valid candidate, use it (window memory)
323
+        if let Some(pref) = preferred {
324
+            if candidates.iter().any(|(w, _)| *w == pref) {
325
+                return Some(pref);
326
+            }
327
+        }
328
+
319
         // Find the closest window in the direction
329
         // Find the closest window in the direction
320
         // Prioritize alignment perpendicular to movement, then distance in movement direction
330
         // Prioritize alignment perpendicular to movement, then distance in movement direction
321
         candidates
331
         candidates
gar/src/x11/events.rsmodified
@@ -1219,13 +1219,53 @@ impl WindowManager {
1219
         let screen = self.screen_rect();
1219
         let screen = self.screen_rect();
1220
         let geometries = self.current_workspace().tree.calculate_geometries(screen);
1220
         let geometries = self.current_workspace().tree.calculate_geometries(screen);
1221
 
1221
 
1222
-        if let Some(target) = Node::find_adjacent(&geometries, focused, direction) {
1222
+        // Look up remembered window for this direction (window memory)
1223
+        let preferred = self.directional_focus_memory.get(&(focused, direction)).copied();
1224
+
1225
+        if let Some(target) = Node::find_adjacent(&geometries, focused, direction, preferred) {
1226
+            // Store the directional focus memory for next time
1227
+            self.directional_focus_memory.insert((focused, direction), target);
1228
+
1229
+            // Store reverse direction only if windows are aligned (same row/column)
1230
+            // This enables "back" navigation without breaking natural movement
1231
+            if let (Some((_, from_rect)), Some((_, to_rect))) = (
1232
+                geometries.iter().find(|(w, _)| *w == focused),
1233
+                geometries.iter().find(|(w, _)| *w == target),
1234
+            ) {
1235
+                let dominated = match direction {
1236
+                    // For Left/Right: store reverse if windows share vertical space (same row)
1237
+                    Direction::Left | Direction::Right => {
1238
+                        let overlap_start = from_rect.y.max(to_rect.y);
1239
+                        let overlap_end = (from_rect.y + from_rect.height as i16)
1240
+                            .min(to_rect.y + to_rect.height as i16);
1241
+                        overlap_start < overlap_end
1242
+                    }
1243
+                    // For Up/Down: store reverse if windows share horizontal space (same column)
1244
+                    Direction::Up | Direction::Down => {
1245
+                        let overlap_start = from_rect.x.max(to_rect.x);
1246
+                        let overlap_end = (from_rect.x + from_rect.width as i16)
1247
+                            .min(to_rect.x + to_rect.width as i16);
1248
+                        overlap_start < overlap_end
1249
+                    }
1250
+                };
1251
+
1252
+                if dominated {
1253
+                    let opposite = match direction {
1254
+                        Direction::Left => Direction::Right,
1255
+                        Direction::Right => Direction::Left,
1256
+                        Direction::Up => Direction::Down,
1257
+                        Direction::Down => Direction::Up,
1258
+                    };
1259
+                    self.directional_focus_memory.insert((target, opposite), focused);
1260
+                }
1261
+            }
1262
+
1223
             // Focus new window (keyboard navigation, warp pointer)
1263
             // Focus new window (keyboard navigation, warp pointer)
1224
             // set_focus handles grab/ungrab for old and new windows
1264
             // set_focus handles grab/ungrab for old and new windows
1225
             self.set_focus(target, true)?;
1265
             self.set_focus(target, true)?;
1226
             self.conn.flush()?;
1266
             self.conn.flush()?;
1227
 
1267
 
1228
-            tracing::debug!("Focused {:?} to window {}", direction, target);
1268
+            tracing::debug!("Focused {:?} to window {} (preferred: {:?})", direction, target, preferred);
1229
         } else {
1269
         } else {
1230
             // No adjacent window on this workspace - try adjacent monitor
1270
             // No adjacent window on this workspace - try adjacent monitor
1231
             self.focus_adjacent_monitor(direction)?;
1271
             self.focus_adjacent_monitor(direction)?;
@@ -1293,7 +1333,7 @@ impl WindowManager {
1293
         let screen = self.screen_rect();
1333
         let screen = self.screen_rect();
1294
         let geometries = self.current_workspace().tree.calculate_geometries(screen);
1334
         let geometries = self.current_workspace().tree.calculate_geometries(screen);
1295
 
1335
 
1296
-        if let Some(target) = Node::find_adjacent(&geometries, focused, direction) {
1336
+        if let Some(target) = Node::find_adjacent(&geometries, focused, direction, None) {
1297
             // Swap the windows in the tree
1337
             // Swap the windows in the tree
1298
             self.current_workspace_mut().tree.swap(focused, target);
1338
             self.current_workspace_mut().tree.swap(focused, target);
1299
 
1339