gardesk/tarmac / ee3fd8e

Browse files

Track recent internal focus activations as a ring

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
ee3fd8ecb6d29d24674960436cec088f878aa263
Parents
13d8bb3
Tree
d8db2d2

1 changed file

StatusFile+-
M tarmac/src/core/state.rs 88 14
tarmac/src/core/state.rsmodified
@@ -53,7 +53,12 @@ pub struct WmState {
5353
     ffm_cooldown_until: Option<std::time::Instant>,
5454
     ffm_last_window: Option<WindowId>,
5555
     focus_return_memory: HashMap<(WindowId, super::tree::Direction), WindowId>,
56
-    pending_internal_focus: Option<(WindowId, i32, std::time::Instant)>,
56
+    /// Recent intentional focus activations (window id, app pid, expiry).
57
+    /// Acts as a small ring so a burst of internal activations (e.g. a
58
+    /// workspace switch followed by a follow-up FFM focus) can all be
59
+    /// matched against the lagging `NSWorkspace.frontmostApplication` poll
60
+    /// without the most recent entry clobbering older ones.
61
+    recent_internal_focus: Vec<(WindowId, i32, std::time::Instant)>,
5762
     drag: Option<DragState>,
5863
     pub focus_follows_mouse: bool,
5964
     pub mouse_follows_focus: bool,
@@ -90,7 +95,7 @@ impl WmState {
9095
             ffm_cooldown_until: None,
9196
             ffm_last_window: None,
9297
             focus_return_memory: HashMap::new(),
93
-            pending_internal_focus: None,
98
+            recent_internal_focus: Vec::new(),
9499
             drag: None,
95100
             focus_follows_mouse: true,
96101
             mouse_follows_focus: true,
@@ -928,30 +933,60 @@ impl WmState {
928933
             .retain(|(from, _), to| *from != window && *to != window);
929934
     }
930935
 
936
+    /// Cap on `recent_internal_focus` ring size. Bounds memory and bounds the
937
+    /// linear scan in `should_ignore_external_focus`. A workspace switch
938
+    /// produces ~1 internal focus per visible window, so 16 is plenty.
939
+    const RECENT_FOCUS_CAPACITY: usize = 16;
940
+    /// How long an entry in `recent_internal_focus` shadows incoming external
941
+    /// focus events. Long enough to cover apply_layout's 50ms sleep plus AX
942
+    /// activation propagation and at least one frontmost-app poll cycle.
943
+    const RECENT_FOCUS_TTL: std::time::Duration = std::time::Duration::from_millis(600);
944
+
945
+    fn prune_recent_internal_focus(&mut self) {
946
+        let now = std::time::Instant::now();
947
+        self.recent_internal_focus.retain(|(_, _, until)| now < *until);
948
+    }
949
+
931950
     fn mark_internal_focus(&mut self, id: WindowId) {
932951
         let Some(window) = self.registry.get(id) else {
933952
             return;
934953
         };
935
-        self.pending_internal_focus = Some((
954
+        let pid = window.app_pid;
955
+        self.prune_recent_internal_focus();
956
+        // Coalesce: if the same window is already pending, just refresh its
957
+        // expiry rather than appending a duplicate.
958
+        if let Some(entry) = self
959
+            .recent_internal_focus
960
+            .iter_mut()
961
+            .find(|(eid, epid, _)| *eid == id && *epid == pid)
962
+        {
963
+            entry.2 = std::time::Instant::now() + Self::RECENT_FOCUS_TTL;
964
+            return;
965
+        }
966
+        if self.recent_internal_focus.len() >= Self::RECENT_FOCUS_CAPACITY {
967
+            self.recent_internal_focus.remove(0);
968
+        }
969
+        self.recent_internal_focus.push((
936970
             id,
937
-            window.app_pid,
938
-            std::time::Instant::now() + std::time::Duration::from_millis(400),
971
+            pid,
972
+            std::time::Instant::now() + Self::RECENT_FOCUS_TTL,
939973
         ));
940974
     }
941975
 
942976
     fn should_ignore_external_focus(&mut self, pid: i32, requested: Option<WindowId>) -> bool {
943
-        let Some((pending_id, pending_pid, until)) = self.pending_internal_focus else {
944
-            return false;
945
-        };
946
-        if std::time::Instant::now() >= until {
947
-            self.pending_internal_focus = None;
977
+        self.prune_recent_internal_focus();
978
+        if self.recent_internal_focus.is_empty() {
948979
             return false;
949980
         }
950
-
951981
         match requested {
952
-            Some(id) if id == pending_id => true,
953
-            None if pid == pending_pid => true,
954
-            _ => false,
982
+            Some(id) => self
983
+                .recent_internal_focus
984
+                .iter()
985
+                .any(|(eid, _, _)| *eid == id),
986
+            None => self
987
+                .recent_internal_focus
988
+                .iter()
989
+                .any(|(_, epid, _)| *epid == pid),
955990
         }
956991
     }
957992
 
@@ -3690,6 +3725,45 @@ mod tests {
36903725
         );
36913726
     }
36923727
 
3728
+    #[test]
3729
+    fn recent_internal_focus_ignores_lagging_poll_for_prior_activation() {
3730
+        // Regression: the single-slot guard let the *previous* activation's
3731
+        // pid leak through once a newer activation overwrote the slot. With
3732
+        // a ring, a frontmost-app poll arriving for the older pid should
3733
+        // still be recognized as self-initiated.
3734
+        let mut state = WmState::new();
3735
+        state.monitors = vec![Monitor {
3736
+            id: 42,
3737
+            frame: Rect::new(0.0, 0.0, 1920.0, 1080.0),
3738
+            usable_frame: Rect::new(0.0, 33.0, 1920.0, 1047.0),
3739
+            is_primary: true,
3740
+            active_workspace: 0,
3741
+        }];
3742
+        state.sync_workspace_visibility();
3743
+        let screen = state.monitors[0].usable_frame;
3744
+
3745
+        state.registry.add(tracked_window(80, 14, "AppA"));
3746
+        state.registry.add(tracked_window(81, 15, "AppB"));
3747
+        {
3748
+            let ws = state.workspaces.get_mut(0);
3749
+            ws.tree.insert_with_rect(80, None, screen);
3750
+            ws.tree.insert_with_rect(81, Some(80), screen);
3751
+            ws.record_focus(80);
3752
+        }
3753
+
3754
+        // Two intentional activations in quick succession — single-slot would
3755
+        // forget the first one.
3756
+        state.focus_window(80);
3757
+        state.focus_window(81);
3758
+
3759
+        // A lagging poll arrives for the older activation's pid.
3760
+        assert!(state.should_ignore_external_focus(14, None));
3761
+        // And for the newer one.
3762
+        assert!(state.should_ignore_external_focus(15, None));
3763
+        // Unrelated pid still passes through.
3764
+        assert!(!state.should_ignore_external_focus(99, None));
3765
+    }
3766
+
36933767
     #[test]
36943768
     fn internal_focus_echo_does_not_trigger_external_mouse_warp() {
36953769
         let mut state = WmState::new();