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 {
4747
     pub current_edge_cursor: Option<(XWindow, crate::x11::events::ResizeEdge)>,
4848
     /// garbar child process (managed automatically when gar.bar is configured)
4949
     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>,
5053
 }
5154
 
5255
 impl WindowManager {
@@ -143,6 +146,7 @@ impl WindowManager {
143146
             dock_struts: HashMap::new(),
144147
             current_edge_cursor: None,
145148
             garbar_process: None,
149
+            directional_focus_memory: HashMap::new(),
146150
         })
147151
     }
148152
 
@@ -434,6 +438,11 @@ impl WindowManager {
434438
             // Remove from focus history
435439
             self.focus_history.retain(|&w| w != window);
436440
 
441
+            // Remove directional focus memory entries involving this window
442
+            self.directional_focus_memory.retain(|(src, _), tgt| {
443
+                *src != window && *tgt != window
444
+            });
445
+
437446
             // Update focus if this was the focused window
438447
             if self.focused_window == Some(window) {
439448
                 // Find next window from focus history that's on this workspace
gar/src/core/tree.rsmodified
@@ -6,7 +6,7 @@ pub enum SplitDirection {
66
     Vertical,
77
 }
88
 
9
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1010
 pub enum Direction {
1111
     Left,
1212
     Right,
@@ -286,10 +286,13 @@ impl Node {
286286
     }
287287
 
288288
     /// 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.
289291
     pub fn find_adjacent(
290292
         geometries: &[(XWindow, Rect)],
291293
         from: XWindow,
292294
         direction: Direction,
295
+        preferred: Option<XWindow>,
293296
     ) -> Option<XWindow> {
294297
         let from_rect = geometries.iter().find(|(w, _)| *w == from)?.1;
295298
 
@@ -316,6 +319,13 @@ impl Node {
316319
             })
317320
             .collect();
318321
 
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
+
319329
         // Find the closest window in the direction
320330
         // Prioritize alignment perpendicular to movement, then distance in movement direction
321331
         candidates
gar/src/x11/events.rsmodified
@@ -1219,13 +1219,53 @@ impl WindowManager {
12191219
         let screen = self.screen_rect();
12201220
         let geometries = self.current_workspace().tree.calculate_geometries(screen);
12211221
 
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
+
12231263
             // Focus new window (keyboard navigation, warp pointer)
12241264
             // set_focus handles grab/ungrab for old and new windows
12251265
             self.set_focus(target, true)?;
12261266
             self.conn.flush()?;
12271267
 
1228
-            tracing::debug!("Focused {:?} to window {}", direction, target);
1268
+            tracing::debug!("Focused {:?} to window {} (preferred: {:?})", direction, target, preferred);
12291269
         } else {
12301270
             // No adjacent window on this workspace - try adjacent monitor
12311271
             self.focus_adjacent_monitor(direction)?;
@@ -1293,7 +1333,7 @@ impl WindowManager {
12931333
         let screen = self.screen_rect();
12941334
         let geometries = self.current_workspace().tree.calculate_geometries(screen);
12951335
 
1296
-        if let Some(target) = Node::find_adjacent(&geometries, focused, direction) {
1336
+        if let Some(target) = Node::find_adjacent(&geometries, focused, direction, None) {
12971337
             // Swap the windows in the tree
12981338
             self.current_workspace_mut().tree.swap(focused, target);
12991339