diff options
Diffstat (limited to 'crates/core/src/manager')
| -rw-r--r-- | crates/core/src/manager/fake_monitor.rs | 69 | ||||
| -rw-r--r-- | crates/core/src/manager/mod.rs | 23 | ||||
| -rw-r--r-- | crates/core/src/manager/tests.rs | 36 | ||||
| -rw-r--r-- | crates/core/src/manager/windows.rs | 11 |
4 files changed, 131 insertions, 8 deletions
diff --git a/crates/core/src/manager/fake_monitor.rs b/crates/core/src/manager/fake_monitor.rs new file mode 100644 index 0000000..2533e83 --- /dev/null +++ b/crates/core/src/manager/fake_monitor.rs @@ -0,0 +1,69 @@ +//! Requesting a fake (fully virtual, no real hardware) monitor be created +//! or removed - see `crates/wayland/src/udev/virtual_heads.rs`'s own +//! module doc comment for the full design. Split out the same way +//! `lock.rs`/`input_pin.rs` are: everything here is plain `impl +//! WindowManager` methods; see `super` (`mod.rs`) for `WindowManager`'s +//! field definitions. + +use super::*; + +impl WindowManager { + /// Queues a request to create a fake monitor named `name` at + /// `width`x`height` - the only caller today is the IPC + /// `"create_fake_monitor"` dispatch, the compositor-agnostic side of + /// `srd dispatch create fake-monitor`. Core has no real `wl_output` + /// to create itself (backend-owned, same as every other cross- + /// boundary request here); the Wayland backend drains and applies + /// this on its own next poll. + pub fn request_create_fake_monitor(&mut self, name: String, width: u32, height: u32) { + self.create_fake_monitor_requests.push((name, width, height)); + } + + pub fn drain_create_fake_monitor_requests(&mut self) -> Vec<(String, u32, u32)> { + std::mem::take(&mut self.create_fake_monitor_requests) + } + + /// Same cross-boundary-request pattern, for removing a fake monitor + /// by name - the IPC `"remove_fake_monitor"` dispatch. + pub fn request_remove_fake_monitor(&mut self, name: String) { + self.remove_fake_monitor_requests.push(name); + } + + pub fn drain_remove_fake_monitor_requests(&mut self) -> Vec<String> { + std::mem::take(&mut self.remove_fake_monitor_requests) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_create_request_is_reported_once_then_the_queue_is_empty() { + let mut wm = WindowManager::new(); + assert!(wm.drain_create_fake_monitor_requests().is_empty()); + wm.request_create_fake_monitor("FAKE-1".into(), 1920, 1080); + assert_eq!(wm.drain_create_fake_monitor_requests(), vec![("FAKE-1".to_string(), 1920, 1080)]); + assert!(wm.drain_create_fake_monitor_requests().is_empty()); + } + + #[test] + fn a_remove_request_is_reported_once_then_the_queue_is_empty() { + let mut wm = WindowManager::new(); + wm.request_remove_fake_monitor("FAKE-1".into()); + assert_eq!(wm.drain_remove_fake_monitor_requests(), vec!["FAKE-1".to_string()]); + assert!(wm.drain_remove_fake_monitor_requests().is_empty()); + } + + #[test] + fn multiple_create_requests_before_a_drain_are_all_preserved() { + // Unlike `request_pin_input`'s own "replace, don't accumulate" + // semantics (one pid can only ever have one current pin target), + // two different fake-monitor names are two genuinely independent + // creations - both must survive to the next drain. + let mut wm = WindowManager::new(); + wm.request_create_fake_monitor("FAKE-1".into(), 1920, 1080); + wm.request_create_fake_monitor("FAKE-2".into(), 1280, 720); + assert_eq!(wm.drain_create_fake_monitor_requests(), vec![("FAKE-1".to_string(), 1920, 1080), ("FAKE-2".to_string(), 1280, 720)]); + } +} diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs index a5cde36..c82bced 100644 --- a/crates/core/src/manager/mod.rs +++ b/crates/core/src/manager/mod.rs @@ -80,6 +80,12 @@ pub struct WindowManager { /// ever learn) to a specific window. See `input_pin.rs`'s own doc /// comment. pin_input_requests: Vec<(i32, Option<WindowId>)>, + /// Same cross-boundary-request pattern, for fake (fully virtual, no + /// real hardware) monitors - see `fake_monitor.rs`'s own doc comment + /// and `crates/wayland/src/udev/virtual_heads.rs`'s module doc + /// comment for the full design. + create_fake_monitor_requests: Vec<(String, u32, u32)>, + remove_fake_monitor_requests: Vec<String>, /// Same cross-boundary-request pattern as `output_position_requests` /// just above, for enable/disable - see `request_output_enabled`'s /// own doc comment for why this is keyed by name, not `MonitorId`. @@ -170,6 +176,19 @@ pub struct WindowManager { layouts: HashMap<String, Box<dyn Layout>>, pub tiling: TilingConfig, pub placement: PlacementConfig, + /// Feeds `SmartPlacement::place`'s own `cascade_step` - advances on + /// every real cascade/grid placement, session-long, never reset by a + /// window closing. See `SmartPlacement::cascade`'s own doc comment + /// for the reported "every window opens in the same spot" bug this + /// exists to fix. A `Cell`, not a plain field: `add_window`'s own + /// `target_monitor` is a `&Monitor` borrowed from `self.monitors` and + /// stays alive across the same call that needs to bump this counter, + /// so a plain `&mut self` write there would conflict with that live + /// immutable borrow - interior mutability sidesteps it without + /// restructuring the borrow, the same reasoning any of this + /// compositor's other `Rc<RefCell<...>>`-style shared-mutation points + /// already accept. + next_cascade_step: std::cell::Cell<u32>, /// Whether geometry changes made via `toggle_maximize`/`toggle_fullscreen` /// should be animated. Read from `general.animations`; a backend's open /// animation is gated on this too, since core has no notion of "open". @@ -410,6 +429,8 @@ impl WindowManager { monitors: Vec::new(), output_position_requests: Vec::new(), pin_input_requests: Vec::new(), + create_fake_monitor_requests: Vec::new(), + remove_fake_monitor_requests: Vec::new(), output_enable_requests: Vec::new(), disabled_monitors: HashMap::new(), monitor_splits: HashMap::new(), @@ -460,6 +481,7 @@ impl WindowManager { layouts, tiling: TilingConfig::default(), placement: PlacementConfig::default(), + next_cascade_step: std::cell::Cell::new(0), animations_enabled: true, animation_duration_ms: 200, shadows_enabled: true, @@ -510,6 +532,7 @@ impl WindowManager { mod capture; mod dragresize; +mod fake_monitor; mod focus; mod hittest; mod input_pin; diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs index 725af1f..c82d3dd 100644 --- a/crates/core/src/manager/tests.rs +++ b/crates/core/src/manager/tests.rs @@ -18,8 +18,11 @@ w.geometry = Rect::new(0, 0, 400, 300); wm.add_window(w); let placed = wm.window(id).unwrap().geometry; - // Grid placement starts at grid_margin, not (0,0). - assert_eq!(placed.x, wm.placement.grid_margin as i32); + // The first window on an empty workspace cascades (see + // `SmartPlacement::place`'s own doc comment on why grid is + // skipped entirely when nothing else is open), starting at + // `cascade_offset`, not (0,0). + assert_eq!(placed.x, wm.placement.cascade_offset); } #[test] @@ -569,16 +572,35 @@ let mut wa = Window::new(a, "a"); wa.geometry = Rect::new(0, 0, 400, 300); wm.add_window(wa); + // `a`'s own real, auto-placed geometry - read back rather than + // assumed, since `SmartPlacement` (not the `Rect` set above, + // which `add_window` overwrites) decides where it actually lands. + let a_geom = wm.window(a).unwrap().geometry; let b = wm.alloc_window_id(); - let mut wb = Window::new(b, "b"); - wb.geometry = Rect::new(0, 0, 400, 300); // identical geometry to `a` - wm.add_window(wb); + wm.add_window(Window::new(b, "b")); + // Forced to genuinely identical geometry to `a` *after* placement + // (`add_window`'s own `SmartPlacement` would otherwise place `b` + // to avoid overlapping `a`, defeating this test's actual point -- + // the overlap here needs to be real, not incidental). + wm.window_mut(b).unwrap().geometry = a_geom; let other_workspace = wm.add_workspace("2", "dynamic"); wm.move_window_to_workspace(b, other_workspace); // b is now off-screen, not minimized - let (hit_id, _) = wm.hit_test(200, 10).unwrap(); + // `hit_test` specifically means titlebar/border/resize-margin hits + // (see its own doc comment) - a point in the window's plain + // content area always resolves `None` there by design (`w`'s own + // opaque content is in the way, `hit_test_with`'s own comment). + // A few pixels below the top edge, horizontally centered, is + // safely inside the titlebar band without landing in a corner + // resize zone. + let (px, py) = (a_geom.x + a_geom.width as i32 / 2, a_geom.y + 5); + let (hit_id, _) = wm.hit_test(px, py).unwrap(); assert_eq!(hit_id, a, "a click must land on the visible window, not one hidden on another workspace"); - assert_eq!(wm.window_at(200, 10), Some(a)); + // `window_at` (content-inclusive) is checked at the window's + // actual center instead - unlike `hit_test`, it has no titlebar- + // only restriction to work around. + let center = (a_geom.x + a_geom.width as i32 / 2, a_geom.y + a_geom.height as i32 / 2); + assert_eq!(wm.window_at(center.0, center.1), Some(a)); } #[test] diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs index ff6c4c8..b2edb6b 100644 --- a/crates/core/src/manager/windows.rs +++ b/crates/core/src/manager/windows.rs @@ -149,7 +149,16 @@ impl WindowManager { if layout_name != "tiling" { let existing: Vec<Rect> = self.windows_on_workspace(workspace).map(|w| w.geometry).collect(); let size = (window.geometry.width, window.geometry.height); - window.geometry = SmartPlacement::place(monitor, &existing, size, &self.placement); + // `next_cascade_step` advances on every real placement, + // never reset by a window closing - see `SmartPlacement:: + // cascade`'s own doc comment for the reported bug this + // fixes ("every window opens in the same spot" when + // opening one app at a time, closing each before the + // next, which kept `existing` empty at the moment of + // every single placement). + let step = self.next_cascade_step.get(); + self.next_cascade_step.set(step.wrapping_add(1)); + window.geometry = SmartPlacement::place(monitor, &existing, size, &self.placement, step); } } if let Some(geometry) = actions.as_ref().and_then(|a| a.geometry) { |