gardesk/gar / 186c6d5

Browse files

Fix focus-follows-mouse feedback loop, add monitor warp

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
186c6d538f8bfd0e98dd099d4617283d5cc806d6
Parents
db679da
Tree
58ebc95

2 changed files

StatusFile+-
M gar/src/core/mod.rs 124 74
M gar/src/x11/events.rs 164 54
gar/src/core/mod.rsmodified
@@ -10,7 +10,7 @@ pub use workspace::Workspace;
1010
 
1111
 use std::collections::HashMap;
1212
 use std::sync::{Arc, Mutex};
13
-use x11rb::protocol::xproto::Window as XWindow;
13
+use x11rb::protocol::xproto::{ConnectionExt, Window as XWindow};
1414
 
1515
 use crate::config::{Config, LuaConfig, LuaState, RuleActions, WindowMatch};
1616
 use crate::ipc::IpcServer;
@@ -32,6 +32,8 @@ pub struct WindowManager {
3232
     pub running: bool,
3333
     pub drag_state: Option<DragState>,
3434
     pub ipc_server: Option<IpcServer>,
35
+    /// Timestamp of last pointer warp - used to suppress EnterNotify feedback loop
36
+    pub last_warp: std::time::Instant,
3537
 }
3638
 
3739
 impl WindowManager {
@@ -111,6 +113,7 @@ impl WindowManager {
111113
             running: true,
112114
             drag_state: None,
113115
             ipc_server,
116
+            last_warp: std::time::Instant::now(),
114117
         })
115118
     }
116119
 
@@ -371,29 +374,61 @@ impl WindowManager {
371374
         self.conn.set_focus(window)?;
372375
         self.conn.set_active_window(Some(window))?;
373376
         self.update_borders()?;
377
+
378
+        // Warp pointer to center of focused window (mouse follows focus)
379
+        if let Err(e) = self.conn.warp_pointer_to_window(window) {
380
+            tracing::warn!("Failed to warp pointer: {}", e);
381
+        }
382
+        // Record warp time to suppress EnterNotify feedback loop
383
+        self.last_warp = std::time::Instant::now();
384
+
374385
         Ok(())
375386
     }
376387
 
377
-    /// Update border colors for all windows based on focus state.
388
+    /// Warp pointer to center of a monitor (for focus without windows)
389
+    pub fn warp_to_monitor(&mut self, monitor_idx: usize) -> Result<()> {
390
+        let geom = self.monitors[monitor_idx].geometry;
391
+        let center_x = geom.x + (geom.width / 2) as i16;
392
+        let center_y = geom.y + (geom.height / 2) as i16;
393
+
394
+        self.conn.conn.warp_pointer(
395
+            x11rb::NONE,
396
+            self.conn.root,
397
+            0, 0, 0, 0,
398
+            center_x,
399
+            center_y,
400
+        )?;
401
+        self.last_warp = std::time::Instant::now();
402
+        self.conn.flush()?;
403
+        Ok(())
404
+    }
405
+
406
+    /// Update border colors for all visible windows based on focus state.
378407
     pub fn update_borders(&mut self) -> Result<()> {
379408
         let focused = self.focused_window;
380409
         let focused_color = self.config.border_color_focused;
381410
         let unfocused_color = self.config.border_color_unfocused;
382411
         let border_width = self.config.border_width;
383412
 
384
-        // Update borders for all windows (tiled + floating)
385
-        for window in self.current_workspace().all_windows() {
386
-            let color = if Some(window) == focused {
387
-                focused_color
388
-            } else {
389
-                unfocused_color
390
-            };
391
-            self.conn.set_border(window, border_width, color)?;
413
+        // Get all visible workspace indices
414
+        let visible_ws: Vec<usize> = self.monitors.iter().map(|m| m.active_workspace).collect();
415
+
416
+        // Update borders for all windows on visible workspaces
417
+        for ws_idx in visible_ws {
418
+            for window in self.workspaces[ws_idx].all_windows() {
419
+                let color = if Some(window) == focused {
420
+                    focused_color
421
+                } else {
422
+                    unfocused_color
423
+                };
424
+                self.conn.set_border(window, border_width, color)?;
425
+            }
392426
         }
393427
         Ok(())
394428
     }
395429
 
396
-    /// Apply the current layout to all windows.
430
+    /// Apply the current layout to all visible windows across all monitors.
431
+    /// Each monitor displays its active_workspace.
397432
     /// Stacking order: tiled windows at bottom, floating windows on top (in list order).
398433
     pub fn apply_layout(&mut self) -> Result<()> {
399434
         use x11rb::protocol::xproto::{ConfigureWindowAux, ConnectionExt, StackMode};
@@ -403,82 +438,87 @@ impl WindowManager {
403438
         let gap_inner = self.config.gap_inner as i16;
404439
         let half_gap = gap_inner / 2;
405440
 
406
-        // Apply outer gap to screen rect
407
-        let screen = self.screen_rect();
408
-        let work_area = Rect::new(
409
-            screen.x + gap_outer,
410
-            screen.y + gap_outer,
411
-            screen.width.saturating_sub(2 * gap_outer as u16),
412
-            screen.height.saturating_sub(2 * gap_outer as u16),
413
-        );
414
-
415
-        tracing::debug!(
416
-            "apply_layout: screen={:?}, work_area={:?}, tiled_count={}, floating_count={}",
417
-            screen,
418
-            work_area,
419
-            self.current_workspace().tree.window_count(),
420
-            self.current_workspace().floating.len()
421
-        );
422
-
423
-        // 1. Configure tiled windows from the BSP tree
424
-        let geometries = self.current_workspace().tree.calculate_geometries(work_area);
425
-        for (window, rect) in &geometries {
426
-            // Apply inner gap: shrink each window by half_gap on each side
427
-            let gapped_x = rect.x + half_gap;
428
-            let gapped_y = rect.y + half_gap;
429
-            let gapped_width = rect.width.saturating_sub(gap_inner as u16);
430
-            let gapped_height = rect.height.saturating_sub(gap_inner as u16);
431
-
432
-            // Account for border width
433
-            let final_width = gapped_width.saturating_sub(2 * border_width as u16);
434
-            let final_height = gapped_height.saturating_sub(2 * border_width as u16);
441
+        // Collect visible workspaces (one per monitor)
442
+        let visible_workspaces: Vec<(usize, Rect)> = self.monitors
443
+            .iter()
444
+            .map(|m| (m.active_workspace, m.geometry))
445
+            .collect();
435446
 
436
-            tracing::debug!(
437
-                "apply_layout: TILED window={} at ({}, {}) size {}x{}",
438
-                window, gapped_x, gapped_y, final_width.max(1), final_height.max(1)
447
+        // Layout each monitor's active workspace
448
+        for (ws_idx, screen) in &visible_workspaces {
449
+            let work_area = Rect::new(
450
+                screen.x + gap_outer,
451
+                screen.y + gap_outer,
452
+                screen.width.saturating_sub(2 * gap_outer as u16),
453
+                screen.height.saturating_sub(2 * gap_outer as u16),
439454
             );
440455
 
441
-            self.conn.configure_window(
442
-                *window,
443
-                gapped_x,
444
-                gapped_y,
445
-                final_width.max(1),
446
-                final_height.max(1),
447
-                border_width,
448
-            )?;
449
-        }
456
+            let ws = &self.workspaces[*ws_idx];
457
+            tracing::debug!(
458
+                "apply_layout: ws={} screen={:?}, work_area={:?}, tiled={}, floating={}",
459
+                ws_idx + 1, screen, work_area,
460
+                ws.tree.window_count(), ws.floating.len()
461
+            );
450462
 
451
-        // 2. Configure floating windows and stack them above tiled
452
-        // Get floating window IDs (in stacking order: first = bottom, last = top)
453
-        let floating_ids: Vec<XWindow> = self.current_workspace().floating.clone();
463
+            // 1. Configure tiled windows from the BSP tree
464
+            let geometries = ws.tree.calculate_geometries(work_area);
465
+            for (window, rect) in &geometries {
466
+                // Apply inner gap: shrink each window by half_gap on each side
467
+                let gapped_x = rect.x + half_gap;
468
+                let gapped_y = rect.y + half_gap;
469
+                let gapped_width = rect.width.saturating_sub(gap_inner as u16);
470
+                let gapped_height = rect.height.saturating_sub(gap_inner as u16);
454471
 
455
-        for window_id in floating_ids {
456
-            // Get the window's floating geometry from our state
457
-            if let Some(win) = self.windows.get(&window_id) {
458
-                let geom = win.floating_geometry;
459
-                let adjusted_width = geom.width.saturating_sub(2 * border_width as u16);
460
-                let adjusted_height = geom.height.saturating_sub(2 * border_width as u16);
472
+                // Account for border width
473
+                let final_width = gapped_width.saturating_sub(2 * border_width as u16);
474
+                let final_height = gapped_height.saturating_sub(2 * border_width as u16);
461475
 
462476
                 tracing::debug!(
463
-                    "apply_layout: FLOATING window={} at ({}, {}) size {}x{} (raising)",
464
-                    window_id, geom.x, geom.y, adjusted_width.max(1), adjusted_height.max(1)
477
+                    "apply_layout: TILED window={} at ({}, {}) size {}x{}",
478
+                    window, gapped_x, gapped_y, final_width.max(1), final_height.max(1)
465479
                 );
466480
 
467
-                // Configure geometry
468481
                 self.conn.configure_window(
469
-                    window_id,
470
-                    geom.x,
471
-                    geom.y,
472
-                    adjusted_width.max(1),
473
-                    adjusted_height.max(1),
482
+                    *window,
483
+                    gapped_x,
484
+                    gapped_y,
485
+                    final_width.max(1),
486
+                    final_height.max(1),
474487
                     border_width,
475488
                 )?;
489
+            }
476490
 
477
-                // Raise to top of stack (each subsequent window goes above the previous)
478
-                let aux = ConfigureWindowAux::new().stack_mode(StackMode::ABOVE);
479
-                self.conn.conn.configure_window(window_id, &aux)?;
480
-            } else {
481
-                tracing::warn!("apply_layout: floating window {} not in windows map!", window_id);
491
+            // 2. Configure floating windows and stack them above tiled
492
+            let floating_ids: Vec<XWindow> = ws.floating.clone();
493
+
494
+            for window_id in floating_ids {
495
+                // Get the window's floating geometry from our state
496
+                if let Some(win) = self.windows.get(&window_id) {
497
+                    let geom = win.floating_geometry;
498
+                    let adjusted_width = geom.width.saturating_sub(2 * border_width as u16);
499
+                    let adjusted_height = geom.height.saturating_sub(2 * border_width as u16);
500
+
501
+                    tracing::debug!(
502
+                        "apply_layout: FLOATING window={} at ({}, {}) size {}x{} (raising)",
503
+                        window_id, geom.x, geom.y, adjusted_width.max(1), adjusted_height.max(1)
504
+                    );
505
+
506
+                    // Configure geometry
507
+                    self.conn.configure_window(
508
+                        window_id,
509
+                        geom.x,
510
+                        geom.y,
511
+                        adjusted_width.max(1),
512
+                        adjusted_height.max(1),
513
+                        border_width,
514
+                    )?;
515
+
516
+                    // Raise to top of stack (each subsequent window goes above the previous)
517
+                    let aux = ConfigureWindowAux::new().stack_mode(StackMode::ABOVE);
518
+                    self.conn.conn.configure_window(window_id, &aux)?;
519
+                } else {
520
+                    tracing::warn!("apply_layout: floating window {} not in windows map!", window_id);
521
+                }
482522
             }
483523
         }
484524
 
@@ -486,6 +526,16 @@ impl WindowManager {
486526
         self.conn.flush()?;
487527
         Ok(())
488528
     }
529
+
530
+    /// Check if a workspace is currently visible (active on any monitor).
531
+    pub fn is_workspace_visible(&self, ws_idx: usize) -> bool {
532
+        self.monitors.iter().any(|m| m.active_workspace == ws_idx)
533
+    }
534
+
535
+    /// Get all currently visible workspace indices.
536
+    pub fn visible_workspaces(&self) -> Vec<usize> {
537
+        self.monitors.iter().map(|m| m.active_workspace).collect()
538
+    }
489539
 }
490540
 
491541
 /// Check if window properties match rule criteria (case-insensitive substring match).
gar/src/x11/events.rsmodified
@@ -3,8 +3,8 @@ use std::process::Command;
33
 use x11rb::connection::Connection as X11Connection;
44
 use x11rb::protocol::xproto::{
55
     ButtonPressEvent, ButtonReleaseEvent, ConfigureRequestEvent, ConfigureWindowAux, ConnectionExt,
6
-    DestroyNotifyEvent, EventMask, KeyPressEvent, MapRequestEvent, ModMask, MotionNotifyEvent,
7
-    StackMode, UnmapNotifyEvent,
6
+    DestroyNotifyEvent, EnterNotifyEvent, EventMask, KeyPressEvent, MapRequestEvent, ModMask,
7
+    MotionNotifyEvent, NotifyMode, StackMode, UnmapNotifyEvent,
88
 };
99
 use x11rb::protocol::Event;
1010
 
@@ -165,7 +165,7 @@ impl WindowManager {
165165
             Event::MotionNotify(e) => self.handle_motion_notify(e)?,
166166
             Event::KeyPress(e) => self.handle_key_press(e)?,
167167
             Event::EnterNotify(e) => {
168
-                tracing::trace!("EnterNotify for window {}", e.event);
168
+                self.handle_enter_notify(e)?;
169169
             }
170170
             Event::RandrScreenChangeNotify(_) => {
171171
                 tracing::info!("RandR screen change detected, refreshing monitors");
@@ -438,6 +438,56 @@ impl WindowManager {
438438
         Ok(())
439439
     }
440440
 
441
+    fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> {
442
+        let window = event.event;
443
+
444
+        // Ignore if we're in a drag operation
445
+        if self.drag_state.is_some() {
446
+            return Ok(());
447
+        }
448
+
449
+        // Suppress EnterNotify events that happen shortly after a pointer warp
450
+        // This prevents feedback loops from mouse-follows-focus
451
+        if self.last_warp.elapsed() < std::time::Duration::from_millis(50) {
452
+            return Ok(());
453
+        }
454
+
455
+        // Ignore inferior (entering from a child window) and non-normal modes
456
+        // Only handle "Normal" mode enters (actual mouse movement)
457
+        if event.mode != NotifyMode::NORMAL {
458
+            return Ok(());
459
+        }
460
+
461
+        // Only focus windows we manage
462
+        if !self.windows.contains_key(&window) {
463
+            return Ok(());
464
+        }
465
+
466
+        // Don't focus if already focused
467
+        if self.focused_window == Some(window) {
468
+            return Ok(());
469
+        }
470
+
471
+        tracing::debug!("Focus follows mouse: focusing window {}", window);
472
+
473
+        // Regrab button on old focused window
474
+        if let Some(old) = self.focused_window {
475
+            self.conn.grab_button(old)?;
476
+        }
477
+
478
+        // Focus the new window (this will warp pointer back, but we'll suppress the resulting EnterNotify)
479
+        self.set_focus(window)?;
480
+        self.conn.ungrab_button(window)?;
481
+
482
+        // Raise floating windows on focus
483
+        if self.is_floating(window) {
484
+            self.raise_window(window)?;
485
+        }
486
+
487
+        self.conn.flush()?;
488
+        Ok(())
489
+    }
490
+
441491
     fn handle_key_press(&mut self, event: KeyPressEvent) -> Result<()> {
442492
         let keycode = event.detail;
443493
         let state = event.state;
@@ -484,6 +534,11 @@ impl WindowManager {
484534
                     self.close_window(window)?;
485535
                 }
486536
             }
537
+            Action::ForceCloseWindow => {
538
+                if let Some(window) = self.focused_window {
539
+                    self.force_close_window(window)?;
540
+                }
541
+            }
487542
             Action::Focus(direction) => {
488543
                 if let Some(dir) = parse_direction(&direction) {
489544
                     self.focus_direction(dir)?;
@@ -541,14 +596,20 @@ impl WindowManager {
541596
     }
542597
 
543598
     fn close_window(&mut self, window: u32) -> Result<()> {
599
+        // Verify we actually have a window to close
600
+        if !self.windows.contains_key(&window) {
601
+            tracing::warn!("close_window called on unmanaged window {}", window);
602
+            return Ok(());
603
+        }
604
+
544605
         tracing::info!("Closing window {}", window);
545606
 
546607
         // Try graceful ICCCM close first
547608
         if self.conn.supports_delete_window(window) {
548
-            tracing::debug!("Window {} supports WM_DELETE_WINDOW, sending graceful close", window);
609
+            tracing::info!("Window {} supports WM_DELETE_WINDOW, sending graceful close", window);
549610
             self.conn.send_delete_window(window)?;
550611
         } else {
551
-            tracing::debug!("Window {} doesn't support WM_DELETE_WINDOW, using kill_client", window);
612
+            tracing::info!("Window {} doesn't support WM_DELETE_WINDOW, using kill_client", window);
552613
             self.conn.conn.kill_client(window)?;
553614
         }
554615
 
@@ -556,9 +617,17 @@ impl WindowManager {
556617
         Ok(())
557618
     }
558619
 
620
+    fn force_close_window(&mut self, window: u32) -> Result<()> {
621
+        tracing::info!("Force closing window {}", window);
622
+        self.conn.conn.kill_client(window)?;
623
+        self.conn.flush()?;
624
+        Ok(())
625
+    }
626
+
559627
     fn focus_direction(&mut self, direction: Direction) -> Result<()> {
560628
         let Some(focused) = self.focused_window else {
561
-            return Ok(());
629
+            // No focused window - try to focus adjacent monitor
630
+            return self.focus_adjacent_monitor(direction);
562631
         };
563632
 
564633
         let screen = self.screen_rect();
@@ -574,8 +643,65 @@ impl WindowManager {
574643
             self.conn.flush()?;
575644
 
576645
             tracing::debug!("Focused {:?} to window {}", direction, target);
646
+        } else {
647
+            // No adjacent window on this workspace - try adjacent monitor
648
+            self.focus_adjacent_monitor(direction)?;
649
+        }
650
+
651
+        Ok(())
652
+    }
653
+
654
+    /// Focus the adjacent monitor in the given direction (does NOT wrap at edges)
655
+    fn focus_adjacent_monitor(&mut self, direction: Direction) -> Result<()> {
656
+        if self.monitors.len() <= 1 {
657
+            return Ok(());
658
+        }
659
+
660
+        // Calculate target index WITHOUT wrapping
661
+        let target_idx = match direction {
662
+            Direction::Left => {
663
+                if self.focused_monitor == 0 {
664
+                    // At leftmost monitor - do nothing
665
+                    return Ok(());
666
+                }
667
+                self.focused_monitor - 1
668
+            }
669
+            Direction::Right => {
670
+                if self.focused_monitor >= self.monitors.len() - 1 {
671
+                    // At rightmost monitor - do nothing
672
+                    return Ok(());
673
+                }
674
+                self.focused_monitor + 1
675
+            }
676
+            // Up/Down could navigate if monitors are stacked vertically
677
+            Direction::Up | Direction::Down => return Ok(()),
678
+        };
679
+
680
+        tracing::info!("Moving focus from monitor {} to {}", self.focused_monitor, target_idx);
681
+        self.focused_monitor = target_idx;
682
+
683
+        // Focus the active workspace on that monitor
684
+        let workspace_idx = self.monitors[target_idx].active_workspace;
685
+        self.focused_workspace = workspace_idx;
686
+
687
+        // Focus a window on that workspace if any, or just warp to monitor center
688
+        if let Some(window) = self.workspaces[workspace_idx].focused
689
+            .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
690
+            .or_else(|| self.workspaces[workspace_idx].tree.first_window())
691
+        {
692
+            if let Some(old) = self.focused_window {
693
+                self.conn.grab_button(old)?;
694
+            }
695
+            self.set_focus(window)?;
696
+            self.conn.ungrab_button(window)?;
697
+        } else {
698
+            // No windows on target monitor - clear focus and warp to monitor center
699
+            self.focused_window = None;
700
+            self.warp_to_monitor(target_idx)?;
701
+            tracing::debug!("No windows on monitor {}, warped to center", target_idx);
577702
         }
578703
 
704
+        self.conn.flush()?;
579705
         Ok(())
580706
     }
581707
 
@@ -632,66 +758,49 @@ impl WindowManager {
632758
         }
633759
 
634760
         // Find which monitor owns this workspace
635
-        let target_monitor = self.monitor_idx_for_workspace(idx);
636
-
637
-        // If the workspace is on a different monitor, just focus that monitor
638
-        if let Some(mon_idx) = target_monitor {
639
-            if mon_idx != self.focused_monitor {
640
-                tracing::info!("Workspace {} is on monitor {}, switching focus", idx + 1, mon_idx);
641
-                self.focused_monitor = mon_idx;
642
-                self.focused_workspace = idx;
643
-                self.monitors[mon_idx].active_workspace = idx;
644
-                self.conn.set_current_desktop(idx as u32)?;
645
-
646
-                // Focus a window on the target workspace
647
-                let ws = &self.workspaces[idx];
648
-                if let Some(window) = ws.focused
649
-                    .or_else(|| ws.floating.last().copied())
650
-                    .or_else(|| ws.tree.first_window())
651
-                {
652
-                    self.set_focus(window)?;
653
-                } else {
654
-                    self.focused_window = None;
655
-                }
656
-                self.conn.flush()?;
657
-                return Ok(());
658
-            }
659
-        }
761
+        let target_monitor_idx = self.monitor_idx_for_workspace(idx).unwrap_or(0);
762
+        let old_ws_on_target = self.monitors[target_monitor_idx].active_workspace;
660763
 
661
-        // Same monitor - switch its active workspace
662
-        if idx == self.focused_workspace {
764
+        // Already on this workspace?
765
+        if idx == old_ws_on_target && target_monitor_idx == self.focused_monitor {
663766
             return Ok(());
664767
         }
665768
 
666
-        tracing::info!("Switching to workspace {}", idx + 1);
769
+        tracing::info!(
770
+            "Switching to workspace {} on monitor {} (was ws {})",
771
+            idx + 1, target_monitor_idx, old_ws_on_target + 1
772
+        );
773
+
774
+        // If switching to a different workspace on the target monitor, hide old/show new
775
+        if idx != old_ws_on_target {
776
+            // Hide windows on old workspace
777
+            for window in self.workspaces[old_ws_on_target].all_windows() {
778
+                self.conn.unmap_window(window)?;
779
+            }
667780
 
668
-        // Hide all windows on current workspace (tiled + floating)
669
-        for window in self.current_workspace().all_windows() {
670
-            self.conn.unmap_window(window)?;
781
+            // Update monitor's active workspace
782
+            self.monitors[target_monitor_idx].active_workspace = idx;
783
+
784
+            // Show windows on new workspace
785
+            for window in self.workspaces[idx].all_windows() {
786
+                self.conn.map_window(window)?;
787
+            }
671788
         }
672789
 
673
-        // Update monitor's active workspace
674
-        let old_ws = self.focused_workspace;
790
+        // Update focused state
791
+        self.focused_monitor = target_monitor_idx;
675792
         self.focused_workspace = idx;
676
-        self.monitors[self.focused_monitor].active_workspace = idx;
677793
 
678794
         // Update EWMH _NET_CURRENT_DESKTOP
679795
         self.conn.set_current_desktop(idx as u32)?;
680796
 
681
-        // Show all windows on new workspace (tiled + floating)
682
-        for window in self.current_workspace().all_windows() {
683
-            self.conn.map_window(window)?;
684
-        }
685
-
686
-        // Apply layout and update focus
797
+        // Apply layout
687798
         self.apply_layout()?;
688799
 
689
-        // Focus the workspace's focused window, preferring floating on top
690
-        if let Some(window) = self
691
-            .current_workspace()
692
-            .focused
693
-            .or_else(|| self.current_workspace().floating.last().copied())
694
-            .or_else(|| self.current_workspace().tree.first_window())
800
+        // Focus a window on the target workspace
801
+        if let Some(window) = self.workspaces[idx].focused
802
+            .or_else(|| self.workspaces[idx].floating.last().copied())
803
+            .or_else(|| self.workspaces[idx].tree.first_window())
695804
         {
696805
             self.set_focus(window)?;
697806
             self.conn.ungrab_button(window)?;
@@ -699,7 +808,6 @@ impl WindowManager {
699808
             self.focused_window = None;
700809
         }
701810
 
702
-        tracing::debug!("Switched from workspace {} to {}", old_ws + 1, idx + 1);
703811
         self.conn.flush()?;
704812
         Ok(())
705813
     }
@@ -1130,7 +1238,7 @@ impl WindowManager {
11301238
         let workspace_idx = self.monitors[target_idx].active_workspace;
11311239
         self.focused_workspace = workspace_idx;
11321240
 
1133
-        // Focus a window on that workspace if any
1241
+        // Focus a window on that workspace if any, or warp to monitor center
11341242
         if let Some(window) = self.workspaces[workspace_idx].focused
11351243
             .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
11361244
             .or_else(|| self.workspaces[workspace_idx].tree.first_window())
@@ -1141,7 +1249,9 @@ impl WindowManager {
11411249
             self.set_focus(window)?;
11421250
             self.conn.ungrab_button(window)?;
11431251
         } else {
1252
+            // No windows - warp to monitor center
11441253
             self.focused_window = None;
1254
+            self.warp_to_monitor(target_idx)?;
11451255
         }
11461256
 
11471257
         self.conn.flush()?;