@@ -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 | } |