gardesk/tarmac / ae35f6c

Browse files

Stabilize workspace visibility and floating order

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
ae35f6c9130a460a3a63b4d6c94b9cb4c20a3f65
Parents
b736bef
Tree
fecca57

3 changed files

StatusFile+-
M tarmac/src/core/state.rs 197 209
M tarmac/src/core/workspace.rs 10 0
M tarmac/src/platform/observer.rs 1 0
tarmac/src/core/state.rsmodified
@@ -209,6 +209,24 @@ impl WmState {
209
             .position(|m| m.active_workspace == ws_idx)
209
             .position(|m| m.active_workspace == ws_idx)
210
     }
210
     }
211
 
211
 
212
+    fn special_showing_workspace(&self, ws_idx: usize) -> bool {
213
+        self.active_specials
214
+            .iter()
215
+            .flatten()
216
+            .any(|&idx| idx == ws_idx)
217
+    }
218
+
219
+    fn workspace_is_effectively_visible(&self, ws_idx: usize) -> bool {
220
+        self.monitor_showing_workspace(ws_idx).is_some() || self.special_showing_workspace(ws_idx)
221
+    }
222
+
223
+    fn sync_workspace_visibility(&mut self) {
224
+        for ws_idx in 0..self.workspaces.count() {
225
+            let visible = self.workspace_is_effectively_visible(ws_idx);
226
+            self.workspaces.get_mut(ws_idx).visible = visible;
227
+        }
228
+    }
229
+
212
     pub fn discover_and_observe(&mut self) {
230
     pub fn discover_and_observe(&mut self) {
213
         // Discover all displays
231
         // Discover all displays
214
         let mut displays = crate::platform::display::discover_displays();
232
         let mut displays = crate::platform::display::discover_displays();
@@ -274,10 +292,10 @@ impl WmState {
274
         for (mi, ws_idx) in monitor_assignments.into_iter().enumerate() {
292
         for (mi, ws_idx) in monitor_assignments.into_iter().enumerate() {
275
             let ws_idx = ws_idx.unwrap_or(0);
293
             let ws_idx = ws_idx.unwrap_or(0);
276
             self.monitors[mi].active_workspace = ws_idx;
294
             self.monitors[mi].active_workspace = ws_idx;
277
-            self.workspaces.get_mut(ws_idx).visible = true;
278
             self.workspaces.get_mut(ws_idx).last_monitor = Some(mi);
295
             self.workspaces.get_mut(ws_idx).last_monitor = Some(mi);
279
             self.workspaces.get_mut(ws_idx).last_display_id = Some(self.monitors[mi].id);
296
             self.workspaces.get_mut(ws_idx).last_display_id = Some(self.monitors[mi].id);
280
         }
297
         }
298
+        self.sync_workspace_visibility();
281
 
299
 
282
         tracing::info!(
300
         tracing::info!(
283
             monitors = self.monitors.len(),
301
             monitors = self.monitors.len(),
@@ -375,9 +393,8 @@ impl WmState {
375
                     "show floating"
393
                     "show floating"
376
                 );
394
                 );
377
                 self.show_window(fw.id, fw.geometry);
395
                 self.show_window(fw.id, fw.geometry);
378
-                use crate::platform::skylight::{K_CG_FLOATING_WINDOW_LEVEL, set_window_level};
379
-                set_window_level(fw.id, K_CG_FLOATING_WINDOW_LEVEL);
380
             }
396
             }
397
+            self.restack_floating_windows(ws_idx);
381
         }
398
         }
382
     }
399
     }
383
 
400
 
@@ -467,9 +484,6 @@ impl WmState {
467
             .collect();
484
             .collect();
468
         // Track workspaces we've already fully processed to avoid infinite loops.
485
         // Track workspaces we've already fully processed to avoid infinite loops.
469
         let mut processed: Vec<usize> = Vec::new();
486
         let mut processed: Vec<usize> = Vec::new();
470
-        // Track the first overflow workspace we created (for wrap-around).
471
-        let mut first_overflow_ws: Option<usize> = None;
472
-
473
         while let Some(ws_idx) = pending.pop() {
487
         while let Some(ws_idx) = pending.pop() {
474
             if processed.contains(&ws_idx) {
488
             if processed.contains(&ws_idx) {
475
                 continue;
489
                 continue;
@@ -480,6 +494,7 @@ impl WmState {
480
                 .monitor_showing_workspace(ws_idx)
494
                 .monitor_showing_workspace(ws_idx)
481
                 .map(|mi| self.monitor_rect(mi))
495
                 .map(|mi| self.monitor_rect(mi))
482
                 .unwrap_or_else(|| self.focused_rect());
496
                 .unwrap_or_else(|| self.focused_rect());
497
+            let (gap_inner, gap_outer) = self.workspace_gaps(ws_idx);
483
 
498
 
484
             // Phase 1: Swap oversized windows into larger tiles.
499
             // Phase 1: Swap oversized windows into larger tiles.
485
             // Batch all swaps using BSP geometry (no AX calls), then apply layout
500
             // Batch all swaps using BSP geometry (no AX calls), then apply layout
@@ -499,12 +514,7 @@ impl WmState {
499
                         .workspaces
514
                         .workspaces
500
                         .get(ws_idx)
515
                         .get(ws_idx)
501
                         .tree
516
                         .tree
502
-                        .calculate_geometries_with_gaps(
517
+                        .calculate_geometries_with_gaps(screen_rect, gap_inner, gap_outer, true);
503
-                            screen_rect,
504
-                            self.gap_inner,
505
-                            self.gap_outer,
506
-                            true,
507
-                        );
508
                     if geometries.is_empty() {
518
                     if geometries.is_empty() {
509
                         break;
519
                         break;
510
                     }
520
                     }
@@ -568,21 +578,15 @@ impl WmState {
568
                 // settling. The settled set prevents ping-pong.
578
                 // settling. The settled set prevents ping-pong.
569
             }
579
             }
570
 
580
 
571
-            // Phase 2: Evict remaining oversized windows that couldn't be fixed by swaps.
581
+            // Phase 2: Float remaining oversized windows locally.
572
-            // Try all workspaces in order (including ones with existing windows).
582
+            // Oversized windows should not silently migrate across workspaces;
573
-            // The recursive pass will handle any overflow on the target workspace.
583
+            // keep workspace membership stable and remediate in place.
574
-            let mut evict_search_from = ws_idx + 1;
575
             loop {
584
             loop {
576
                 let geometries = self
585
                 let geometries = self
577
                     .workspaces
586
                     .workspaces
578
                     .get(ws_idx)
587
                     .get(ws_idx)
579
                     .tree
588
                     .tree
580
-                    .calculate_geometries_with_gaps(
589
+                    .calculate_geometries_with_gaps(screen_rect, gap_inner, gap_outer, true);
581
-                        screen_rect,
582
-                        self.gap_inner,
583
-                        self.gap_outer,
584
-                        true,
585
-                    );
586
                 if geometries.is_empty() {
590
                 if geometries.is_empty() {
587
                     break;
591
                     break;
588
                 }
592
                 }
@@ -610,88 +614,24 @@ impl WmState {
610
                     None => break,
614
                     None => break,
611
                 };
615
                 };
612
 
616
 
613
-                // Find next non-visible workspace to evict to.
614
-                // Only evict to non-visible workspaces — visible ones would
615
-                // need immediate overflow checking with potentially stale sizes.
616
-                let next_ws = (evict_search_from..self.workspaces.count())
617
-                    .find(|&i| i != ws_idx && !self.workspaces.get(i).visible);
618
-                let next_ws = match next_ws {
619
-                    Some(ws) => ws,
620
-                    None => {
621
-                        // All workspaces visible or exhausted — create one
622
-                        let idx = self.workspaces.count();
623
-                        self.workspaces.get_or_create(idx);
624
-                        idx
625
-                    }
626
-                };
627
-                evict_search_from = next_ws + 1;
628
-
629
-                // Check if we've wrapped all the way around.
630
-                let hit_limit = first_overflow_ws
631
-                    .is_some_and(|first| processed.contains(&next_ws) || next_ws == first);
632
-
633
-                if hit_limit {
634
-                    self.float_oversized_on_workspace(ws_idx, screen_rect);
635
-                    break;
636
-                }
637
-
638
-                if first_overflow_ws.is_none() {
639
-                    first_overflow_ws = Some(next_ws);
640
-                }
641
-
642
                 tracing::info!(
617
                 tracing::info!(
643
                     id = oversized_wid,
618
                     id = oversized_wid,
644
                     min_w,
619
                     min_w,
645
                     min_h,
620
                     min_h,
646
-                    from_ws = ws_idx + 1,
621
+                    ws = ws_idx + 1,
647
-                    to_ws = next_ws + 1,
622
+                    "floating oversized window locally"
648
-                    "evicting oversized window to next workspace"
649
                 );
623
                 );
650
-
624
+                self.workspaces
651
-                // Remove from current workspace
625
+                    .get_mut(ws_idx)
652
-                let ws = self.workspaces.get_mut(ws_idx);
626
+                    .toggle_float(oversized_wid, screen_rect);
653
-                ws.tree.remove(oversized_wid);
627
+                if let Some(ax_ref) = self.ax_refs.get(&oversized_wid) {
654
-                if ws.focused == Some(oversized_wid) {
628
+                    let fx = screen_rect.x + (screen_rect.width - min_w) / 2.0;
655
-                    ws.pop_focus();
629
+                    let fy = screen_rect.y + (screen_rect.height - min_h) / 2.0;
630
+                    let _ = ax_set_position(ax_ref, fx, fy);
631
+                    let _ = ax_set_size(ax_ref, min_w, min_h);
656
                 }
632
                 }
657
-                ws.focus_history.retain(|id| *id != oversized_wid);
658
-
659
-                // Hide and re-layout the SOURCE workspace (without the evicted window).
660
-                // Do this BEFORE inserting into the target so apply_layout
661
-                // can't accidentally restore alpha=1.0 on the evicted window.
662
-                let source_monitor = self.monitor_showing_workspace(ws_idx).unwrap_or(
663
-                    self.focused_monitor
664
-                        .min(self.monitors.len().saturating_sub(1)),
665
-                );
666
-                let source_frame = self.monitors[source_monitor].frame;
667
-                self.hide_window_on_frame(oversized_wid, source_frame, Some(source_monitor));
668
-                self.log_hidden_window_diagnostics(oversized_wid, "post-evict-hide");
669
                 self.apply_layout();
633
                 self.apply_layout();
670
-
634
+                self.restack_floating_windows(ws_idx);
671
-                // Now insert into target workspace (already hidden)
672
-                let target_rect = self
673
-                    .monitor_showing_workspace(next_ws)
674
-                    .map(|tmi| self.monitor_rect(tmi))
675
-                    .unwrap_or(screen_rect);
676
-                let target_ws = self.workspaces.get_or_create(next_ws);
677
-                target_ws.last_monitor = Some(source_monitor);
678
-                target_ws.last_display_id = Some(self.monitors[source_monitor].id);
679
-                target_ws
680
-                    .tree
681
-                    .insert_with_rect(oversized_wid, target_ws.focused, target_rect);
682
-                target_ws.record_focus(oversized_wid);
683
-
684
-                // Only queue VISIBLE target workspaces for overflow checking.
685
-                // Non-visible workspaces have stale ax_get_size values and will
686
-                // be checked when the user switches to them (switch_workspace
687
-                // calls fix_oversized_windows). This prevents runaway eviction
688
-                // chains caused by stale sizes.
689
-                if self.monitor_showing_workspace(next_ws).is_some()
690
-                    && !processed.contains(&next_ws)
691
-                    && !pending.contains(&next_ws)
692
-                {
693
-                    pending.push(next_ws);
694
-                }
695
             }
635
             }
696
 
636
 
697
             processed.push(ws_idx);
637
             processed.push(ws_idx);
@@ -720,56 +660,6 @@ impl WmState {
720
         }
660
         }
721
     }
661
     }
722
 
662
 
723
-    /// Float all oversized windows on a workspace as an absolute last resort.
724
-    /// Only called when every workspace has been tried and overflow persists.
725
-    fn float_oversized_on_workspace(&mut self, ws_idx: usize, screen_rect: Rect) {
726
-        loop {
727
-            let geometries = self
728
-                .workspaces
729
-                .get(ws_idx)
730
-                .tree
731
-                .calculate_geometries_with_gaps(screen_rect, self.gap_inner, self.gap_outer, true);
732
-            if geometries.is_empty() {
733
-                break;
734
-            }
735
-
736
-            let oversized = geometries.iter().find_map(|(wid, rect)| {
737
-                let ax_ref = self.ax_refs.get(wid)?;
738
-                let (aw, ah) = ax_get_size(ax_ref).ok()?;
739
-                if aw > rect.width + 1.0 || ah > rect.height + 1.0 {
740
-                    Some((*wid, aw, ah))
741
-                } else {
742
-                    None
743
-                }
744
-            });
745
-
746
-            let (oversized_wid, min_w, min_h) = match oversized {
747
-                Some(v) => v,
748
-                None => break,
749
-            };
750
-
751
-            tracing::warn!(
752
-                id = oversized_wid,
753
-                min_w,
754
-                min_h,
755
-                ws = ws_idx + 1,
756
-                "last-resort float: all workspaces exhausted"
757
-            );
758
-            self.workspaces
759
-                .get_mut(ws_idx)
760
-                .toggle_float(oversized_wid, screen_rect);
761
-            if let Some(ax_ref) = self.ax_refs.get(&oversized_wid) {
762
-                let fx = screen_rect.x + (screen_rect.width - min_w) / 2.0;
763
-                let fy = screen_rect.y + (screen_rect.height - min_h) / 2.0;
764
-                let _ = ax_set_position(ax_ref, fx, fy);
765
-                let _ = ax_set_size(ax_ref, min_w, min_h);
766
-            }
767
-            use crate::platform::skylight::{K_CG_FLOATING_WINDOW_LEVEL, set_window_level};
768
-            set_window_level(oversized_wid, K_CG_FLOATING_WINDOW_LEVEL);
769
-            self.apply_layout();
770
-        }
771
-    }
772
-
773
     // --- Window operations ---
663
     // --- Window operations ---
774
 
664
 
775
     pub fn focus_direction(&mut self, direction: super::tree::Direction) {
665
     pub fn focus_direction(&mut self, direction: super::tree::Direction) {
@@ -778,12 +668,13 @@ impl WmState {
778
         let ws = self.active_workspace();
668
         let ws = self.active_workspace();
779
         let focused = ws.focused;
669
         let focused = ws.focused;
780
         let sr = self.focused_rect();
670
         let sr = self.focused_rect();
671
+        let (gap_inner, gap_outer) = self.workspace_gaps(self.active_ws_idx());
781
 
672
 
782
         // Try intra-workspace navigation first (only if we have a focused window)
673
         // Try intra-workspace navigation first (only if we have a focused window)
783
         if let Some(from) = focused {
674
         if let Some(from) = focused {
784
-            let geoms =
675
+            let geoms = ws
785
-                ws.tree
676
+                .tree
786
-                    .calculate_geometries_with_gaps(sr, self.gap_inner, self.gap_outer, true);
677
+                .calculate_geometries_with_gaps(sr, gap_inner, gap_outer, true);
787
 
678
 
788
             // Check if focused window is at the monitor edge in the requested
679
             // Check if focused window is at the monitor edge in the requested
789
             // direction. If so, cross monitors instead of spiraling into the BSP tree.
680
             // direction. If so, cross monitors instead of spiraling into the BSP tree.
@@ -829,12 +720,10 @@ impl WmState {
829
         if let Some(new_mi) = new_mi {
720
         if let Some(new_mi) = new_mi {
830
             self.focused_monitor = new_mi;
721
             self.focused_monitor = new_mi;
831
             let target_sr = self.focused_rect();
722
             let target_sr = self.focused_rect();
832
-            let target_geoms = self.active_workspace().tree.calculate_geometries_with_gaps(
723
+            let target_geoms = self
833
-                target_sr,
724
+                .active_workspace()
834
-                self.gap_inner,
725
+                .tree
835
-                self.gap_outer,
726
+                .calculate_geometries_with_gaps(target_sr, gap_inner, gap_outer, true);
836
-                true,
837
-            );
838
 
727
 
839
             if let Some(wid) =
728
             if let Some(wid) =
840
                 Node::nearest_to_edge(&target_geoms, direction).or(self.active_workspace().focused)
729
                 Node::nearest_to_edge(&target_geoms, direction).or(self.active_workspace().focused)
@@ -873,9 +762,10 @@ impl WmState {
873
             None => return,
762
             None => return,
874
         };
763
         };
875
         let sr = self.focused_rect();
764
         let sr = self.focused_rect();
876
-        let geoms =
765
+        let (gap_inner, gap_outer) = self.workspace_gaps(self.active_ws_idx());
877
-            ws.tree
766
+        let geoms = ws
878
-                .calculate_geometries_with_gaps(sr, self.gap_inner, self.gap_outer, true);
767
+            .tree
768
+            .calculate_geometries_with_gaps(sr, gap_inner, gap_outer, true);
879
 
769
 
880
         // Check if the focused window touches the monitor edge in the
770
         // Check if the focused window touches the monitor edge in the
881
         // requested direction. If so, skip intra-workspace swap and move
771
         // requested direction. If so, skip intra-workspace swap and move
@@ -986,14 +876,16 @@ impl WmState {
986
         // workspace might be different from the window's workspace.
876
         // workspace might be different from the window's workspace.
987
         let old_focused = if let Some(ws_idx) = self.workspaces.find_window(id) {
877
         let old_focused = if let Some(ws_idx) = self.workspaces.find_window(id) {
988
             let old = self.workspaces.get(ws_idx).focused;
878
             let old = self.workspaces.get(ws_idx).focused;
879
+            self.workspaces.get_mut(ws_idx).raise_floating(id);
989
             self.workspaces.get_mut(ws_idx).record_focus(id);
880
             self.workspaces.get_mut(ws_idx).record_focus(id);
990
             old
881
             old
991
         } else {
882
         } else {
992
             let old = self.active_workspace().focused;
883
             let old = self.active_workspace().focused;
884
+            self.active_workspace_mut().raise_floating(id);
993
             self.active_workspace_mut().record_focus(id);
885
             self.active_workspace_mut().record_focus(id);
994
             old
886
             old
995
         };
887
         };
996
-        self.enforce_floating_levels();
888
+        self.enforce_floating_levels(id);
997
 
889
 
998
         // Update border colors on focus change
890
         // Update border colors on focus change
999
         if self.borders.is_enabled() && old_focused != Some(id) {
891
         if self.borders.is_enabled() && old_focused != Some(id) {
@@ -1126,10 +1018,23 @@ impl WmState {
1126
 
1018
 
1127
     /// Re-apply SkyLight window levels for all floating windows.
1019
     /// Re-apply SkyLight window levels for all floating windows.
1128
     /// Called after every focus change since app activation can reset ordering.
1020
     /// Called after every focus change since app activation can reset ordering.
1129
-    fn enforce_floating_levels(&self) {
1021
+    fn enforce_floating_levels(&self, focused_id: WindowId) {
1022
+        if let Some(ws_idx) = self.workspaces.find_window(focused_id) {
1023
+            self.restack_floating_windows(ws_idx);
1024
+        } else {
1025
+            self.restack_floating_windows(self.active_ws_idx());
1026
+        }
1027
+    }
1028
+
1029
+    fn restack_floating_windows(&self, ws_idx: usize) {
1130
         use crate::platform::skylight::{K_CG_FLOATING_WINDOW_LEVEL, set_window_level};
1030
         use crate::platform::skylight::{K_CG_FLOATING_WINDOW_LEVEL, set_window_level};
1131
-        for fw in &self.active_workspace().floating {
1031
+
1032
+        let ws = self.workspaces.get(ws_idx);
1033
+        for fw in &ws.floating {
1132
             set_window_level(fw.id, K_CG_FLOATING_WINDOW_LEVEL);
1034
             set_window_level(fw.id, K_CG_FLOATING_WINDOW_LEVEL);
1035
+            if let Some(ax_ref) = self.ax_refs.get(&fw.id) {
1036
+                let _ = ax_perform_action(ax_ref, "AXRaise");
1037
+            }
1133
         }
1038
         }
1134
     }
1039
     }
1135
 
1040
 
@@ -1221,6 +1126,7 @@ impl WmState {
1221
         let window_under = if let Some(special_idx) = special_ws_idx {
1126
         let window_under = if let Some(special_idx) = special_ws_idx {
1222
             let sr = self.monitor_rect(mi);
1127
             let sr = self.monitor_rect(mi);
1223
             let ws = self.workspaces.get(special_idx);
1128
             let ws = self.workspaces.get(special_idx);
1129
+            let (gap_inner, gap_outer) = self.workspace_gaps(special_idx);
1224
 
1130
 
1225
             // Look up config for this special workspace's overlay rect
1131
             // Look up config for this special workspace's overlay rect
1226
             let special_name = match &ws.id {
1132
             let special_name = match &ws.id {
@@ -1265,8 +1171,8 @@ impl WmState {
1265
                 // Check tiled windows within the overlay rect
1171
                 // Check tiled windows within the overlay rect
1266
                 let geoms = ws.tree.calculate_geometries_with_gaps(
1172
                 let geoms = ws.tree.calculate_geometries_with_gaps(
1267
                     overlay_rect,
1173
                     overlay_rect,
1268
-                    self.gap_inner,
1174
+                    gap_inner,
1269
-                    self.gap_outer,
1175
+                    gap_outer,
1270
                     true,
1176
                     true,
1271
                 );
1177
                 );
1272
                 geoms
1178
                 geoms
@@ -1276,6 +1182,7 @@ impl WmState {
1276
             }
1182
             }
1277
         } else {
1183
         } else {
1278
             let ws = self.active_workspace();
1184
             let ws = self.active_workspace();
1185
+            let (gap_inner, gap_outer) = self.workspace_gaps(self.active_ws_idx());
1279
 
1186
 
1280
             // Check floating windows first -- they're visually on top
1187
             // Check floating windows first -- they're visually on top
1281
             let floating_under = ws
1188
             let floating_under = ws
@@ -1291,8 +1198,8 @@ impl WmState {
1291
                 // Check tiled windows using gap-aware geometry matching actual layout
1198
                 // Check tiled windows using gap-aware geometry matching actual layout
1292
                 let geoms = ws.tree.calculate_geometries_with_gaps(
1199
                 let geoms = ws.tree.calculate_geometries_with_gaps(
1293
                     self.focused_rect(),
1200
                     self.focused_rect(),
1294
-                    self.gap_inner,
1201
+                    gap_inner,
1295
-                    self.gap_outer,
1202
+                    gap_outer,
1296
                     true,
1203
                     true,
1297
                 );
1204
                 );
1298
                 geoms
1205
                 geoms
@@ -1334,7 +1241,7 @@ impl WmState {
1334
                 // Set window level to floating so it stays above all normal windows
1241
                 // Set window level to floating so it stays above all normal windows
1335
                 use crate::platform::skylight::{K_CG_FLOATING_WINDOW_LEVEL, set_window_level};
1242
                 use crate::platform::skylight::{K_CG_FLOATING_WINDOW_LEVEL, set_window_level};
1336
                 set_window_level(focused, K_CG_FLOATING_WINDOW_LEVEL);
1243
                 set_window_level(focused, K_CG_FLOATING_WINDOW_LEVEL);
1337
-                self.enforce_floating_levels();
1244
+                self.enforce_floating_levels(focused);
1338
                 tracing::info!(id = focused, "window floated (level=floating)");
1245
                 tracing::info!(id = focused, "window floated (level=floating)");
1339
             } else {
1246
             } else {
1340
                 // Restore to normal window level
1247
                 // Restore to normal window level
@@ -1347,6 +1254,7 @@ impl WmState {
1347
 
1254
 
1348
     pub fn click_to_focus(&mut self, x: f64, y: f64) {
1255
     pub fn click_to_focus(&mut self, x: f64, y: f64) {
1349
         let ws = self.active_workspace();
1256
         let ws = self.active_workspace();
1257
+        let (gap_inner, gap_outer) = self.workspace_gaps(self.active_ws_idx());
1350
 
1258
 
1351
         // Check floating first
1259
         // Check floating first
1352
         let floating_hit = ws
1260
         let floating_hit = ws
@@ -1361,8 +1269,8 @@ impl WmState {
1361
         } else {
1269
         } else {
1362
             let geoms = ws.tree.calculate_geometries_with_gaps(
1270
             let geoms = ws.tree.calculate_geometries_with_gaps(
1363
                 self.focused_rect(),
1271
                 self.focused_rect(),
1364
-                self.gap_inner,
1272
+                gap_inner,
1365
-                self.gap_outer,
1273
+                gap_outer,
1366
                 true,
1274
                 true,
1367
             );
1275
             );
1368
             geoms
1276
             geoms
@@ -1401,11 +1309,12 @@ impl WmState {
1401
                 // Warp to the focused WINDOW center (not monitor center)
1309
                 // Warp to the focused WINDOW center (not monitor center)
1402
                 if self.mouse_follows_focus {
1310
                 if self.mouse_follows_focus {
1403
                     let sr = self.focused_rect();
1311
                     let sr = self.focused_rect();
1312
+                    let (gap_inner, gap_outer) = self.workspace_gaps(target_idx);
1404
                     let geoms = self
1313
                     let geoms = self
1405
                         .workspaces
1314
                         .workspaces
1406
                         .get(target_idx)
1315
                         .get(target_idx)
1407
                         .tree
1316
                         .tree
1408
-                        .calculate_geometries_with_gaps(sr, self.gap_inner, self.gap_outer, true);
1317
+                        .calculate_geometries_with_gaps(sr, gap_inner, gap_outer, true);
1409
                     if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1318
                     if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1410
                         warp_mouse_to_center(rect);
1319
                         warp_mouse_to_center(rect);
1411
                     } else {
1320
                     } else {
@@ -1444,7 +1353,6 @@ impl WmState {
1444
         // Hide the workspace currently on the target monitor
1353
         // Hide the workspace currently on the target monitor
1445
         let displaced_idx = self.monitors[show_on_monitor].active_workspace;
1354
         let displaced_idx = self.monitors[show_on_monitor].active_workspace;
1446
         self.hide_workspace_windows(displaced_idx, show_on_monitor);
1355
         self.hide_workspace_windows(displaced_idx, show_on_monitor);
1447
-        self.workspaces.get_mut(displaced_idx).visible = false;
1448
         // If the displaced workspace is empty, unassign it from the monitor
1356
         // If the displaced workspace is empty, unassign it from the monitor
1449
         if self.workspaces.get(displaced_idx).is_empty() {
1357
         if self.workspaces.get(displaced_idx).is_empty() {
1450
             self.workspaces.get_mut(displaced_idx).last_monitor = None;
1358
             self.workspaces.get_mut(displaced_idx).last_monitor = None;
@@ -1453,11 +1361,11 @@ impl WmState {
1453
 
1361
 
1454
         // Show target workspace on the chosen monitor
1362
         // Show target workspace on the chosen monitor
1455
         self.monitors[show_on_monitor].active_workspace = target_idx;
1363
         self.monitors[show_on_monitor].active_workspace = target_idx;
1456
-        self.workspaces.get_mut(target_idx).visible = true;
1457
         self.workspaces.get_mut(target_idx).last_monitor = Some(show_on_monitor);
1364
         self.workspaces.get_mut(target_idx).last_monitor = Some(show_on_monitor);
1458
         self.workspaces.get_mut(target_idx).last_display_id =
1365
         self.workspaces.get_mut(target_idx).last_display_id =
1459
             Some(self.monitors[show_on_monitor].id);
1366
             Some(self.monitors[show_on_monitor].id);
1460
         self.focused_monitor = show_on_monitor;
1367
         self.focused_monitor = show_on_monitor;
1368
+        self.sync_workspace_visibility();
1461
 
1369
 
1462
         // Apply layout with double-apply for cross-monitor moves
1370
         // Apply layout with double-apply for cross-monitor moves
1463
         self.apply_layout();
1371
         self.apply_layout();
@@ -1469,11 +1377,12 @@ impl WmState {
1469
             self.focus_window(wid);
1377
             self.focus_window(wid);
1470
             if self.mouse_follows_focus {
1378
             if self.mouse_follows_focus {
1471
                 let sr = self.focused_rect();
1379
                 let sr = self.focused_rect();
1380
+                let (gap_inner, gap_outer) = self.workspace_gaps(target_idx);
1472
                 let geoms = self
1381
                 let geoms = self
1473
                     .workspaces
1382
                     .workspaces
1474
                     .get(target_idx)
1383
                     .get(target_idx)
1475
                     .tree
1384
                     .tree
1476
-                    .calculate_geometries_with_gaps(sr, self.gap_inner, self.gap_outer, true);
1385
+                    .calculate_geometries_with_gaps(sr, gap_inner, gap_outer, true);
1477
                 if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1386
                 if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1478
                     warp_mouse_to_center(rect);
1387
                     warp_mouse_to_center(rect);
1479
                 } else {
1388
                 } else {
@@ -1597,7 +1506,7 @@ impl WmState {
1597
 
1506
 
1598
     fn enforce_hidden_workspaces(&self) {
1507
     fn enforce_hidden_workspaces(&self) {
1599
         for ws_idx in 0..self.workspaces.count() {
1508
         for ws_idx in 0..self.workspaces.count() {
1600
-            if self.workspaces.get(ws_idx).visible {
1509
+            if self.workspace_is_effectively_visible(ws_idx) {
1601
                 continue;
1510
                 continue;
1602
             }
1511
             }
1603
             for wid in self.workspaces.get(ws_idx).all_window_ids() {
1512
             for wid in self.workspaces.get(ws_idx).all_window_ids() {
@@ -1627,7 +1536,7 @@ impl WmState {
1627
             pid = window.app_pid,
1536
             pid = window.app_pid,
1628
             app = %window.app_name,
1537
             app = %window.app_name,
1629
             workspace = ws_idx.map(|idx| idx + 1),
1538
             workspace = ws_idx.map(|idx| idx + 1),
1630
-            workspace_visible = ws_idx.map(|idx| self.workspaces.get(idx).visible),
1539
+            workspace_visible = ws_idx.map(|idx| self.workspace_is_effectively_visible(idx)),
1631
             group_ids = ?group_ids,
1540
             group_ids = ?group_ids,
1632
             on_screen_count = on_screen.len(),
1541
             on_screen_count = on_screen.len(),
1633
             "hidden window diagnostics"
1542
             "hidden window diagnostics"
@@ -1713,7 +1622,7 @@ impl WmState {
1713
             // Currently shown → hide
1622
             // Currently shown → hide
1714
             self.active_specials[mi] = None;
1623
             self.active_specials[mi] = None;
1715
             let wids = self.workspaces.get(special_idx).all_window_ids();
1624
             let wids = self.workspaces.get(special_idx).all_window_ids();
1716
-            self.workspaces.get_mut(special_idx).visible = false;
1625
+            self.sync_workspace_visibility();
1717
             for wid in wids {
1626
             for wid in wids {
1718
                 self.hide_window(wid);
1627
                 self.hide_window(wid);
1719
             }
1628
             }
@@ -1726,7 +1635,8 @@ impl WmState {
1726
             // Dismiss any other active special on this monitor first
1635
             // Dismiss any other active special on this monitor first
1727
             if let Some(old_idx) = self.active_specials[mi] {
1636
             if let Some(old_idx) = self.active_specials[mi] {
1728
                 let old_wids = self.workspaces.get(old_idx).all_window_ids();
1637
                 let old_wids = self.workspaces.get(old_idx).all_window_ids();
1729
-                self.workspaces.get_mut(old_idx).visible = false;
1638
+                self.active_specials[mi] = None;
1639
+                self.sync_workspace_visibility();
1730
                 for wid in old_wids {
1640
                 for wid in old_wids {
1731
                     self.hide_window(wid);
1641
                     self.hide_window(wid);
1732
                 }
1642
                 }
@@ -1736,9 +1646,9 @@ impl WmState {
1736
             self.active_specials[mi] = Some(special_idx);
1646
             self.active_specials[mi] = Some(special_idx);
1737
             {
1647
             {
1738
                 let ws = self.workspaces.get_mut(special_idx);
1648
                 let ws = self.workspaces.get_mut(special_idx);
1739
-                ws.visible = true;
1740
                 ws.last_monitor = Some(mi);
1649
                 ws.last_monitor = Some(mi);
1741
             }
1650
             }
1651
+            self.sync_workspace_visibility();
1742
 
1652
 
1743
             let sr = self.monitor_rect(mi);
1653
             let sr = self.monitor_rect(mi);
1744
             let ws = self.workspaces.get(special_idx);
1654
             let ws = self.workspaces.get(special_idx);
@@ -1773,12 +1683,10 @@ impl WmState {
1773
             let overlay_rect = Rect::new(overlay_x, overlay_y, overlay_w, overlay_h);
1683
             let overlay_rect = Rect::new(overlay_x, overlay_y, overlay_w, overlay_h);
1774
 
1684
 
1775
             // Compute geometries and collect data before calling self methods
1685
             // Compute geometries and collect data before calling self methods
1776
-            let geoms = ws.tree.calculate_geometries_with_gaps(
1686
+            let (gap_inner, gap_outer) = self.workspace_gaps(special_idx);
1777
-                overlay_rect,
1687
+            let geoms =
1778
-                self.gap_inner,
1688
+                ws.tree
1779
-                self.gap_outer,
1689
+                    .calculate_geometries_with_gaps(overlay_rect, gap_inner, gap_outer, true);
1780
-                true,
1781
-            );
1782
             let floating_data: Vec<(WindowId, Rect)> =
1690
             let floating_data: Vec<(WindowId, Rect)> =
1783
                 ws.floating.iter().map(|fw| (fw.id, fw.geometry)).collect();
1691
                 ws.floating.iter().map(|fw| (fw.id, fw.geometry)).collect();
1784
             let focus_target = ws.focused.or_else(|| geoms.first().map(|(id, _)| *id));
1692
             let focus_target = ws.focused.or_else(|| geoms.first().map(|(id, _)| *id));
@@ -1864,6 +1772,8 @@ impl WmState {
1864
         let is_visible = self.active_specials[self.focused_monitor] == Some(special_idx);
1772
         let is_visible = self.active_specials[self.focused_monitor] == Some(special_idx);
1865
         if !is_visible {
1773
         if !is_visible {
1866
             self.hide_window(focused);
1774
             self.hide_window(focused);
1775
+        } else {
1776
+            self.restack_floating_windows(special_idx);
1867
         }
1777
         }
1868
 
1778
 
1869
         // Retile the source workspace
1779
         // Retile the source workspace
@@ -1892,12 +1802,11 @@ impl WmState {
1892
             self.focus_window(wid);
1802
             self.focus_window(wid);
1893
             if self.mouse_follows_focus {
1803
             if self.mouse_follows_focus {
1894
                 let sr = self.focused_rect();
1804
                 let sr = self.focused_rect();
1895
-                let geoms = self.active_workspace().tree.calculate_geometries_with_gaps(
1805
+                let (gap_inner, gap_outer) = self.workspace_gaps(self.active_ws_idx());
1896
-                    sr,
1806
+                let geoms = self
1897
-                    self.gap_inner,
1807
+                    .active_workspace()
1898
-                    self.gap_outer,
1808
+                    .tree
1899
-                    true,
1809
+                    .calculate_geometries_with_gaps(sr, gap_inner, gap_outer, true);
1900
-                );
1901
                 if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1810
                 if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1902
                     warp_mouse_to_center(rect);
1811
                     warp_mouse_to_center(rect);
1903
                 } else {
1812
                 } else {
@@ -1927,12 +1836,11 @@ impl WmState {
1927
             self.focus_window(wid);
1836
             self.focus_window(wid);
1928
             if self.mouse_follows_focus {
1837
             if self.mouse_follows_focus {
1929
                 let sr = self.focused_rect();
1838
                 let sr = self.focused_rect();
1930
-                let geoms = self.active_workspace().tree.calculate_geometries_with_gaps(
1839
+                let (gap_inner, gap_outer) = self.workspace_gaps(self.active_ws_idx());
1931
-                    sr,
1840
+                let geoms = self
1932
-                    self.gap_inner,
1841
+                    .active_workspace()
1933
-                    self.gap_outer,
1842
+                    .tree
1934
-                    true,
1843
+                    .calculate_geometries_with_gaps(sr, gap_inner, gap_outer, true);
1935
-                );
1936
                 if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1844
                 if let Some((_, rect)) = geoms.iter().find(|(id, _)| *id == wid) {
1937
                     warp_mouse_to_center(rect);
1845
                     warp_mouse_to_center(rect);
1938
                 } else {
1846
                 } else {
@@ -1983,6 +1891,7 @@ impl WmState {
1983
 
1891
 
1984
         // Get target monitor's screen rect for layout
1892
         // Get target monitor's screen rect for layout
1985
         let target_rect = self.monitor_rect(target_mi);
1893
         let target_rect = self.monitor_rect(target_mi);
1894
+        let (target_gap_inner, target_gap_outer) = self.workspace_gaps(target_ws_idx);
1986
 
1895
 
1987
         // Find which workspace the window is actually on
1896
         // Find which workspace the window is actually on
1988
         let current_idx = match self.workspaces.find_window(focused) {
1897
         let current_idx = match self.workspaces.find_window(focused) {
@@ -2032,8 +1941,8 @@ impl WmState {
2032
         // Apply layout on target monitor using its screen rect
1941
         // Apply layout on target monitor using its screen rect
2033
         let geoms = target.tree.calculate_geometries_with_gaps(
1942
         let geoms = target.tree.calculate_geometries_with_gaps(
2034
             target_rect,
1943
             target_rect,
2035
-            self.gap_inner,
1944
+            target_gap_inner,
2036
-            self.gap_outer,
1945
+            target_gap_outer,
2037
             true,
1946
             true,
2038
         );
1947
         );
2039
         for (wid, rect) in &geoms {
1948
         for (wid, rect) in &geoms {
@@ -2063,6 +1972,7 @@ impl WmState {
2063
         });
1972
         });
2064
 
1973
 
2065
         let old_count = self.monitors.len();
1974
         let old_count = self.monitors.len();
1975
+        let old_active_specials = self.active_specials.clone();
2066
         let old_focused_display_id = self.monitors.get(self.focused_monitor).map(|m| m.id);
1976
         let old_focused_display_id = self.monitors.get(self.focused_monitor).map(|m| m.id);
2067
 
1977
 
2068
         // Collect old display IDs for orphan detection
1978
         // Collect old display IDs for orphan detection
@@ -2103,7 +2013,6 @@ impl WmState {
2103
                 );
2013
                 );
2104
                 self.hide_workspace_windows_immediate(ws_idx);
2014
                 self.hide_workspace_windows_immediate(ws_idx);
2105
                 let ws = self.workspaces.get_mut(ws_idx);
2015
                 let ws = self.workspaces.get_mut(ws_idx);
2106
-                ws.visible = false;
2107
                 ws.last_monitor = None;
2016
                 ws.last_monitor = None;
2108
                 // Keep last_display_id intact for reconnection
2017
                 // Keep last_display_id intact for reconnection
2109
             }
2018
             }
@@ -2126,7 +2035,7 @@ impl WmState {
2126
             let ws_idx = (0..self.workspaces.count())
2035
             let ws_idx = (0..self.workspaces.count())
2127
                 .find(|&i| {
2036
                 .find(|&i| {
2128
                     !used_ws.get(i).copied().unwrap_or(false)
2037
                     !used_ws.get(i).copied().unwrap_or(false)
2129
-                        && !self.workspaces.get(i).visible
2038
+                        && !self.workspace_is_effectively_visible(i)
2130
                         && self.workspace_prefs(i).and_then(|prefs| {
2039
                         && self.workspace_prefs(i).and_then(|prefs| {
2131
                             prefs.monitor.as_ref().map(|monitor| monitor.display_id)
2040
                             prefs.monitor.as_ref().map(|monitor| monitor.display_id)
2132
                         }) == Some(nd.id)
2041
                         }) == Some(nd.id)
@@ -2134,7 +2043,7 @@ impl WmState {
2134
                 .or_else(|| {
2043
                 .or_else(|| {
2135
                     (0..self.workspaces.count()).find(|&i| {
2044
                     (0..self.workspaces.count()).find(|&i| {
2136
                         !used_ws.get(i).copied().unwrap_or(false)
2045
                         !used_ws.get(i).copied().unwrap_or(false)
2137
-                            && !self.workspaces.get(i).visible
2046
+                            && !self.workspace_is_effectively_visible(i)
2138
                             && self.workspaces.get(i).last_display_id == Some(nd.id)
2047
                             && self.workspaces.get(i).last_display_id == Some(nd.id)
2139
                     })
2048
                     })
2140
                 })
2049
                 })
@@ -2142,7 +2051,7 @@ impl WmState {
2142
                     // Next preference: first hidden workspace assigned to no specific monitor.
2051
                     // Next preference: first hidden workspace assigned to no specific monitor.
2143
                     (0..self.workspaces.count()).find(|&i| {
2052
                     (0..self.workspaces.count()).find(|&i| {
2144
                         !used_ws.get(i).copied().unwrap_or(false)
2053
                         !used_ws.get(i).copied().unwrap_or(false)
2145
-                            && !self.workspaces.get(i).visible
2054
+                            && !self.workspace_is_effectively_visible(i)
2146
                             && self
2055
                             && self
2147
                                 .workspace_prefs(i)
2056
                                 .workspace_prefs(i)
2148
                                 .and_then(|prefs| {
2057
                                 .and_then(|prefs| {
@@ -2153,7 +2062,8 @@ impl WmState {
2153
                 })
2062
                 })
2154
                 .or_else(|| {
2063
                 .or_else(|| {
2155
                     (0..self.workspaces.count()).find(|&i| {
2064
                     (0..self.workspaces.count()).find(|&i| {
2156
-                        !used_ws.get(i).copied().unwrap_or(false) && !self.workspaces.get(i).visible
2065
+                        !used_ws.get(i).copied().unwrap_or(false)
2066
+                            && !self.workspace_is_effectively_visible(i)
2157
                     })
2067
                     })
2158
                 })
2068
                 })
2159
                 .unwrap_or_else(|| {
2069
                 .unwrap_or_else(|| {
@@ -2171,7 +2081,6 @@ impl WmState {
2171
             m.active_workspace = ws_idx;
2081
             m.active_workspace = ws_idx;
2172
 
2082
 
2173
             let ws = self.workspaces.get_mut(ws_idx);
2083
             let ws = self.workspaces.get_mut(ws_idx);
2174
-            ws.visible = true;
2175
             ws.last_monitor = Some(new_idx);
2084
             ws.last_monitor = Some(new_idx);
2176
             ws.last_display_id = Some(nd.id);
2085
             ws.last_display_id = Some(nd.id);
2177
 
2086
 
@@ -2184,7 +2093,19 @@ impl WmState {
2184
         }
2093
         }
2185
 
2094
 
2186
         // --- Phase 4: Replace monitors, recover focus ---
2095
         // --- Phase 4: Replace monitors, recover focus ---
2096
+        let remapped_specials: Vec<Option<usize>> = new_monitors
2097
+            .iter()
2098
+            .map(|monitor| {
2099
+                self.monitors
2100
+                    .iter()
2101
+                    .position(|old_monitor| old_monitor.id == monitor.id)
2102
+                    .and_then(|old_idx| old_active_specials.get(old_idx).copied().flatten())
2103
+            })
2104
+            .collect();
2105
+
2187
         self.monitors = new_monitors;
2106
         self.monitors = new_monitors;
2107
+        self.active_specials = remapped_specials;
2108
+        self.sync_workspace_visibility();
2188
 
2109
 
2189
         // Map old focused_monitor through display ID → new index
2110
         // Map old focused_monitor through display ID → new index
2190
         if let Some(old_did) = old_focused_display_id {
2111
         if let Some(old_did) = old_focused_display_id {
@@ -2248,7 +2169,7 @@ impl WmState {
2248
         {
2169
         {
2249
             // Dismiss the special workspace since we're taking its window
2170
             // Dismiss the special workspace since we're taking its window
2250
             self.active_specials[mi] = None;
2171
             self.active_specials[mi] = None;
2251
-            self.workspaces.get_mut(special_idx).visible = false;
2172
+            self.sync_workspace_visibility();
2252
             // Hide any remaining windows on the special workspace
2173
             // Hide any remaining windows on the special workspace
2253
             let remaining = self.workspaces.get(special_idx).all_window_ids();
2174
             let remaining = self.workspaces.get(special_idx).all_window_ids();
2254
             for wid in remaining {
2175
             for wid in remaining {
@@ -2301,7 +2222,7 @@ impl WmState {
2301
         target_ws.record_focus(focused);
2222
         target_ws.record_focus(focused);
2302
 
2223
 
2303
         // If target is not visible, hide the window
2224
         // If target is not visible, hide the window
2304
-        if !self.workspaces.get(target_idx).visible {
2225
+        if !self.workspace_is_effectively_visible(target_idx) {
2305
             self.hide_window(focused);
2226
             self.hide_window(focused);
2306
         }
2227
         }
2307
 
2228
 
@@ -2340,7 +2261,12 @@ impl WmState {
2340
             .workspaces
2261
             .workspaces
2341
             .get(ws_idx)
2262
             .get(ws_idx)
2342
             .tree
2263
             .tree
2343
-            .calculate_geometries_with_gaps(screen_rect, self.gap_inner, self.gap_outer, true);
2264
+            .calculate_geometries_with_gaps(
2265
+                screen_rect,
2266
+                self.workspace_gaps(ws_idx).0,
2267
+                self.workspace_gaps(ws_idx).1,
2268
+                true,
2269
+            );
2344
 
2270
 
2345
         let (_min_w, _min_h) = match geometries.iter().find(|(wid, _)| *wid == moved_wid) {
2271
         let (_min_w, _min_h) = match geometries.iter().find(|(wid, _)| *wid == moved_wid) {
2346
             Some((_, rect)) => {
2272
             Some((_, rect)) => {
@@ -2364,7 +2290,12 @@ impl WmState {
2364
                 .workspaces
2290
                 .workspaces
2365
                 .get(ws_idx)
2291
                 .get(ws_idx)
2366
                 .tree
2292
                 .tree
2367
-                .calculate_geometries_with_gaps(screen_rect, self.gap_inner, self.gap_outer, true);
2293
+                .calculate_geometries_with_gaps(
2294
+                    screen_rect,
2295
+                    self.workspace_gaps(ws_idx).0,
2296
+                    self.workspace_gaps(ws_idx).1,
2297
+                    true,
2298
+                );
2368
             if geoms.is_empty() {
2299
             if geoms.is_empty() {
2369
                 break;
2300
                 break;
2370
             }
2301
             }
@@ -2575,7 +2506,7 @@ impl WmState {
2575
     pub fn is_window_hidden(&self, wid: u32) -> bool {
2506
     pub fn is_window_hidden(&self, wid: u32) -> bool {
2576
         let id = wid as WindowId;
2507
         let id = wid as WindowId;
2577
         if let Some(ws_idx) = self.workspaces.find_window(id) {
2508
         if let Some(ws_idx) = self.workspaces.find_window(id) {
2578
-            !self.workspaces.get(ws_idx).visible
2509
+            !self.workspace_is_effectively_visible(ws_idx)
2579
         } else {
2510
         } else {
2580
             false
2511
             false
2581
         }
2512
         }
@@ -3163,4 +3094,61 @@ mod tests {
3163
         assert_eq!(hide_x, -979.0);
3094
         assert_eq!(hide_x, -979.0);
3164
         assert_eq!(hide_y, 6107.0);
3095
         assert_eq!(hide_y, 6107.0);
3165
     }
3096
     }
3097
+
3098
+    #[test]
3099
+    fn special_workspace_counts_as_effectively_visible() {
3100
+        let mut state = WmState::new();
3101
+        state.monitors = vec![Monitor {
3102
+            id: 42,
3103
+            frame: Rect::new(0.0, 0.0, 1710.0, 1107.0),
3104
+            usable_frame: Rect::new(0.0, 33.0, 1710.0, 1074.0),
3105
+            is_primary: true,
3106
+            active_workspace: 0,
3107
+        }];
3108
+        let special_idx = state.workspaces.special_index("scratch");
3109
+        state.active_specials = vec![Some(special_idx)];
3110
+        state.sync_workspace_visibility();
3111
+
3112
+        assert!(state.workspace_is_effectively_visible(special_idx));
3113
+        assert!(state.workspaces.get(special_idx).visible);
3114
+    }
3115
+
3116
+    #[test]
3117
+    fn is_window_hidden_uses_effective_visibility_not_cached_flag() {
3118
+        let mut state = WmState::new();
3119
+        state.monitors = vec![Monitor {
3120
+            id: 42,
3121
+            frame: Rect::new(0.0, 0.0, 1710.0, 1107.0),
3122
+            usable_frame: Rect::new(0.0, 33.0, 1710.0, 1074.0),
3123
+            is_primary: true,
3124
+            active_workspace: 0,
3125
+        }];
3126
+        state.registry.add(WindowState {
3127
+            id: 123,
3128
+            app_pid: 1,
3129
+            app_name: "Test".to_string(),
3130
+            app_bundle_id: "test.bundle".to_string(),
3131
+            title: String::new(),
3132
+            role: "AXWindow".to_string(),
3133
+            subrole: "AXStandardWindow".to_string(),
3134
+            x: 0.0,
3135
+            y: 0.0,
3136
+            width: 100.0,
3137
+            height: 100.0,
3138
+            floating: false,
3139
+            minimized: false,
3140
+        });
3141
+        state.workspaces.get_mut(1).tree.insert_with_rect(
3142
+            123,
3143
+            None,
3144
+            state.monitors[0].usable_frame,
3145
+        );
3146
+        state.workspaces.get_mut(1).visible = true; // Stale cached state.
3147
+
3148
+        assert!(state.is_window_hidden(123));
3149
+
3150
+        state.monitors[0].active_workspace = 1;
3151
+        state.sync_workspace_visibility();
3152
+        assert!(!state.is_window_hidden(123));
3153
+    }
3166
 }
3154
 }
tarmac/src/core/workspace.rsmodified
@@ -237,6 +237,16 @@ impl Workspace {
237
         self.focused = Some(window_id);
237
         self.focused = Some(window_id);
238
     }
238
     }
239
 
239
 
240
+    pub fn raise_floating(&mut self, id: WindowId) -> bool {
241
+        if let Some(idx) = self.floating.iter().position(|f| f.id == id) {
242
+            let floating = self.floating.remove(idx);
243
+            self.floating.push(floating);
244
+            true
245
+        } else {
246
+            false
247
+        }
248
+    }
249
+
240
     /// Pop the most recent focus and return the new focused window.
250
     /// Pop the most recent focus and return the new focused window.
241
     pub fn pop_focus(&mut self) -> Option<WindowId> {
251
     pub fn pop_focus(&mut self) -> Option<WindowId> {
242
         self.focus_history.pop();
252
         self.focus_history.pop();
tarmac/src/platform/observer.rsmodified
@@ -93,6 +93,7 @@ impl AppObserver {
93
         // Subscribe to all notification types on the app element
93
         // Subscribe to all notification types on the app element
94
         let notifications = [
94
         let notifications = [
95
             AX_WINDOW_CREATED,
95
             AX_WINDOW_CREATED,
96
+            AX_UI_ELEMENT_DESTROYED,
96
             AX_FOCUSED_WINDOW_CHANGED,
97
             AX_FOCUSED_WINDOW_CHANGED,
97
             AX_WINDOW_MOVED,
98
             AX_WINDOW_MOVED,
98
             AX_WINDOW_RESIZED,
99
             AX_WINDOW_RESIZED,