srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/src/manager')
-rw-r--r--crates/core/src/manager/fake_monitor.rs69
-rw-r--r--crates/core/src/manager/mod.rs23
-rw-r--r--crates/core/src/manager/tests.rs36
-rw-r--r--crates/core/src/manager/windows.rs11
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) {