gardesk/tarmac / 2a31cf7

Browse files

core: filter stale focus_return_memory by adjacent_distance

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
2a31cf7e6eba803679d0e167847213eb21ac149d
Parents
2c106c7
Tree
738c81e

2 changed files

StatusFile+-
M tarmac/src/core/state.rs 98 1
M tarmac/src/core/tree.rs 1 1
tarmac/src/core/state.rsmodified
@@ -843,11 +843,42 @@ impl WmState {
843
 
843
 
844
             if !at_edge && let Some(default_target) = Node::find_adjacent(&geoms, from, direction) {
844
             if !at_edge && let Some(default_target) = Node::find_adjacent(&geoms, from, direction) {
845
                 let candidates = Node::adjacent_candidates(&geoms, from, direction);
845
                 let candidates = Node::adjacent_candidates(&geoms, from, direction);
846
+                let from_rect = geoms.iter().find(|(w, _)| *w == from).map(|(_, r)| *r);
847
+                let default_rect = geoms
848
+                    .iter()
849
+                    .find(|(w, _)| *w == default_target)
850
+                    .map(|(_, r)| *r);
846
                 let target = self
851
                 let target = self
847
                     .focus_return_memory
852
                     .focus_return_memory
848
                     .get(&(from, direction))
853
                     .get(&(from, direction))
849
                     .copied()
854
                     .copied()
850
-                    .filter(|remembered| candidates.contains(remembered))
855
+                    .filter(|remembered| {
856
+                        if !candidates.contains(remembered) {
857
+                            return false;
858
+                        }
859
+                        // Only honor remembered target when its distance is
860
+                        // close to the default's. Otherwise topology has
861
+                        // changed (e.g., a new window appeared between
862
+                        // source and remembered target) and the memory is
863
+                        // stale.
864
+                        let Some(remembered_rect) = geoms
865
+                            .iter()
866
+                            .find(|(w, _)| *w == *remembered)
867
+                            .map(|(_, r)| *r)
868
+                        else {
869
+                            return false;
870
+                        };
871
+                        let (Some(fr), Some(dr)) = (from_rect, default_rect) else {
872
+                            return true;
873
+                        };
874
+                        let d_default = Node::adjacent_distance(&fr, &dr, direction);
875
+                        let d_remembered =
876
+                            Node::adjacent_distance(&fr, &remembered_rect, direction);
877
+                        // Memory wins when within 32px (a typical gap +
878
+                        // border) of the default's distance — that's the
879
+                        // tie-breaker regime memory is meant for.
880
+                        d_remembered - d_default <= 32.0
881
+                    })
851
                     .unwrap_or(default_target);
882
                     .unwrap_or(default_target);
852
                 self.focus_window(target);
883
                 self.focus_window(target);
853
                 self.remember_focus_transition(from, direction, target, &geoms);
884
                 self.remember_focus_transition(from, direction, target, &geoms);
@@ -3837,6 +3868,72 @@ mod tests {
3837
         );
3868
         );
3838
     }
3869
     }
3839
 
3870
 
3871
+    /// Regression: a stale focus_return_memory entry from a prior layout
3872
+    /// (when only the far-left window existed as a Left candidate from
3873
+    /// the source) must not override the obvious nearest-neighbor
3874
+    /// default after a new window appears between source and the
3875
+    /// remembered target. Reproduces the widescreen layout where
3876
+    /// pressing Left from the bottom-right tile would skip the new
3877
+    /// middle-tall window and land on the far-left tile.
3878
+    #[test]
3879
+    fn focus_memory_yields_to_closer_default_when_topology_grows() {
3880
+        let mut state = WmState::new();
3881
+        // Original 3-window layout: far-left tall (1), upper-right (2),
3882
+        // lower-right (3). From 3, Left has only 1 as a candidate.
3883
+        let geoms_before = vec![
3884
+            (1u32, Rect::new(0.0, 0.0, 500.0, 1000.0)),
3885
+            (2u32, Rect::new(500.0, 0.0, 500.0, 500.0)),
3886
+            (3u32, Rect::new(500.0, 500.0, 500.0, 500.0)),
3887
+        ];
3888
+        state.remember_focus_transition(3, Direction::Left, 1, &geoms_before);
3889
+        assert_eq!(
3890
+            state.focus_return_memory.get(&(3, Direction::Left)).copied(),
3891
+            Some(1)
3892
+        );
3893
+
3894
+        // New 4-window layout: a middle-tall (4) appears between the
3895
+        // left column and the right column. Now Left from 3 should
3896
+        // pick 4, not the stale memorized 1.
3897
+        let geoms_after = vec![
3898
+            (1u32, Rect::new(0.0, 0.0, 250.0, 1000.0)),
3899
+            (4u32, Rect::new(250.0, 0.0, 250.0, 1000.0)),
3900
+            (2u32, Rect::new(500.0, 0.0, 500.0, 500.0)),
3901
+            (3u32, Rect::new(500.0, 500.0, 500.0, 500.0)),
3902
+        ];
3903
+        let from_rect = geoms_after[3].1;
3904
+        let default_target = Node::find_adjacent(&geoms_after, 3, Direction::Left).unwrap();
3905
+        assert_eq!(default_target, 4);
3906
+        let candidates = Node::adjacent_candidates(&geoms_after, 3, Direction::Left);
3907
+        let default_rect = geoms_after
3908
+            .iter()
3909
+            .find(|(w, _)| *w == default_target)
3910
+            .map(|(_, r)| *r)
3911
+            .unwrap();
3912
+        let target = state
3913
+            .focus_return_memory
3914
+            .get(&(3, Direction::Left))
3915
+            .copied()
3916
+            .filter(|remembered| {
3917
+                if !candidates.contains(remembered) {
3918
+                    return false;
3919
+                }
3920
+                let Some(remembered_rect) = geoms_after
3921
+                    .iter()
3922
+                    .find(|(w, _)| *w == *remembered)
3923
+                    .map(|(_, r)| *r)
3924
+                else {
3925
+                    return false;
3926
+                };
3927
+                let d_default =
3928
+                    Node::adjacent_distance(&from_rect, &default_rect, Direction::Left);
3929
+                let d_remembered =
3930
+                    Node::adjacent_distance(&from_rect, &remembered_rect, Direction::Left);
3931
+                d_remembered - d_default <= 32.0
3932
+            })
3933
+            .unwrap_or(default_target);
3934
+        assert_eq!(target, 4, "stale memory should yield to closer default");
3935
+    }
3936
+
3840
     #[test]
3937
     #[test]
3841
     fn mouse_hover_on_inactive_stack_sliver_does_not_change_focus() {
3938
     fn mouse_hover_on_inactive_stack_sliver_does_not_change_focus() {
3842
         let mut state = WmState::new();
3939
         let mut state = WmState::new();
tarmac/src/core/tree.rsmodified
@@ -558,7 +558,7 @@ impl Node {
558
     /// overlaps with the source. Ties broken by horizontal gap.
558
     /// overlaps with the source. Ties broken by horizontal gap.
559
     /// For Up/Down: prefer the closest window whose horizontal extent
559
     /// For Up/Down: prefer the closest window whose horizontal extent
560
     /// overlaps with the source. Ties broken by vertical gap.
560
     /// overlaps with the source. Ties broken by vertical gap.
561
-    fn adjacent_distance(from: &Rect, to: &Rect, direction: Direction) -> f64 {
561
+    pub fn adjacent_distance(from: &Rect, to: &Rect, direction: Direction) -> f64 {
562
         match direction {
562
         match direction {
563
             Direction::Left | Direction::Right => {
563
             Direction::Left | Direction::Right => {
564
                 // Horizontal gap: edge-to-edge distance
564
                 // Horizontal gap: edge-to-edge distance