srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/placement.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/src/placement.rs')
-rw-r--r--crates/core/src/placement.rs105
1 files changed, 90 insertions, 15 deletions
diff --git a/crates/core/src/placement.rs b/crates/core/src/placement.rs
index 0247666..e2b3d04 100644
--- a/crates/core/src/placement.rs
+++ b/crates/core/src/placement.rs
@@ -122,8 +122,26 @@ pub struct SmartPlacement;
impl SmartPlacement {
/// Place a new window of `size` given the geometries of windows already
/// occupying `monitor`. Tries a grid cell first, falling back to cascade.
- pub fn place(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig) -> Rect {
- Self::grid(monitor, existing, size, cfg).unwrap_or_else(|| Self::cascade(monitor, existing, size, cfg))
+ /// `cascade_step` is a monotonically increasing counter the caller owns
+ /// (`WindowManager::next_cascade_step`) - see `cascade`'s own doc
+ /// comment for why this can't just be `existing.len()` the way `grid`'s
+ /// own cell count legitimately still is.
+ ///
+ /// Skips grid entirely when `existing` is empty, going straight to
+ /// cascade instead: grid's real job is dividing space fairly among
+ /// *concurrent* windows, and with nothing else open there is nothing
+ /// to divide against - a 1x1 grid is mathematically one single cell
+ /// no matter how it's rotated, so it can never vary by session
+ /// history the way this reported bug needs. This is the actual
+ /// overwhelmingly common case in practice (open one app, use it,
+ /// close it, open the next), which is exactly why the bug this fixes
+ /// ("every window opens in the same spot") was reported as the normal
+ /// experience, not an edge case.
+ pub fn place(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig, cascade_step: u32) -> Rect {
+ if existing.is_empty() {
+ return Self::cascade(monitor, size, cfg, cascade_step);
+ }
+ Self::grid(monitor, existing, size, cfg).unwrap_or_else(|| Self::cascade(monitor, size, cfg, cascade_step))
}
fn grid(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig) -> Option<Rect> {
@@ -155,10 +173,27 @@ impl SmartPlacement {
None
}
- /// Diagonal cascade, stepping by `cascade_offset` per already-placed
- /// window and wrapping back to the origin once it would run off the
+ /// Diagonal cascade, stepping by `cascade_offset` per window opened so
+ /// far and wrapping back to the origin once it would run off the
/// monitor.
- fn cascade(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig) -> Rect {
+ ///
+ /// Driven by `cascade_step` - a counter the caller keeps incrementing
+ /// across the whole session - rather than `existing.len()` (how many
+ /// windows happen to be open on this workspace *right now*), which is
+ /// what this used to take. That reads as reasonable ("cascade further
+ /// when more windows are open") but has a real, reported bug baked in:
+ /// the overwhelmingly common way people actually use a desktop is one
+ /// app at a time - open, use, close, open the next - and `existing`
+ /// is empty at the start of every single one of those opens, so `step`
+ /// was `0` every time regardless of how many windows had already been
+ /// opened-and-closed that session. Reported live as "every window
+ /// spawns in the exact same place and size, not at all like Windows" --
+ /// confirmed by reading this function, not guessed: real Windows
+ /// cascades the *next* window further even after you close the
+ /// previous one, which needs a counter that survives a window closing,
+ /// not one derived from whoever is still open at the moment of the
+ /// next placement.
+ fn cascade(monitor: &Monitor, size: (u32, u32), cfg: &PlacementConfig, cascade_step: u32) -> Rect {
let area = monitor.geometry;
let width = size.0.min(area.width);
let height = size.1.min(area.height);
@@ -167,7 +202,7 @@ impl SmartPlacement {
let max_steps_y = ((area.height as i32 - height as i32) / cfg.cascade_offset.max(1)).max(1);
let max_steps = max_steps_x.min(max_steps_y).max(1);
- let step = existing.len() as i32 % max_steps;
+ let step = (cascade_step as i32) % max_steps;
let x = (area.x + cfg.cascade_offset + step * cfg.cascade_offset).min(area.right() - width as i32).max(area.x);
let y = (area.y + cfg.cascade_offset + step * cfg.cascade_offset).min(area.bottom() - height as i32).max(area.y);
Rect::new(x, y, width, height)
@@ -210,18 +245,36 @@ mod tests {
}
#[test]
- fn first_window_goes_in_top_left_grid_cell() {
+ fn a_window_opened_alone_cascades_rather_than_using_a_pointless_1x1_grid() {
+ // `place` skips `grid` entirely when nothing else is open - see
+ // its own doc comment for why: a grid with nothing to divide space
+ // against is always exactly one cell, which can never vary by
+ // session history, and "one app open at a time" is the ordinary
+ // case, not an edge one.
+ let cfg = PlacementConfig::default();
+ let r = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg, 0);
+ assert_eq!(r.x, cfg.cascade_offset);
+ assert_eq!(r.y, cfg.cascade_offset);
+ }
+
+ #[test]
+ fn opening_the_same_app_alone_twice_in_a_row_lands_in_different_spots() {
+ // The concrete reported symptom, exercised through the real
+ // `place` entry point (not `cascade` directly, unlike the more
+ // targeted unit test below) - opening one window, closing it, and
+ // opening another must not silently collapse back to the exact
+ // same spot just because `existing` is empty again both times.
let cfg = PlacementConfig::default();
- let r = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg);
- assert_eq!(r.x, cfg.grid_margin as i32);
- assert_eq!(r.y, cfg.grid_margin as i32);
+ let first = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg, 0);
+ let second = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg, 1);
+ assert_ne!(first, second);
}
#[test]
fn grid_avoids_occupied_cells() {
let cfg = PlacementConfig::default();
- let first = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg);
- let second = SmartPlacement::place(&monitor(), &[first], (400, 300), &cfg);
+ let first = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg, 0);
+ let second = SmartPlacement::place(&monitor(), &[first], (400, 300), &cfg, 1);
assert!(!first.overlaps(&second), "second window must not overlap the first: {first:?} vs {second:?}");
}
@@ -230,17 +283,39 @@ mod tests {
let cfg = PlacementConfig { max_grid: 1, ..Default::default() };
// max_grid=1 means the grid is always a single cell, so a second
// window can never find a free grid cell and must cascade.
- let first = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg);
- let second = SmartPlacement::place(&monitor(), &[first], (400, 300), &cfg);
+ let first = SmartPlacement::place(&monitor(), &[], (400, 300), &cfg, 0);
+ let second = SmartPlacement::place(&monitor(), &[first], (400, 300), &cfg, 1);
assert_ne!(first, second);
// First window is grid-placed (offset by grid_margin); the second no
// longer fits any grid cell and falls back to cascade, which steps
- // from the monitor origin by `cascade_offset` per already-placed window.
+ // from the monitor origin by `cascade_offset` per window opened so
+ // far this session (the caller's own counter, passed in as `1` here).
assert_eq!(second.x, cfg.cascade_offset * 2);
assert_eq!(second.y, cfg.cascade_offset * 2);
}
#[test]
+ fn cascade_step_keeps_advancing_even_if_the_previous_window_closed() {
+ // The actual reported bug this counter exists to fix: opening one
+ // window at a time (closing each before the next) used to reset
+ // `existing` to empty every time, so `step` - driven by `existing.
+ // len()` - was always 0 regardless of how many windows had already
+ // been opened-and-closed. A cascade_step the caller keeps
+ // incrementing across the session, independent of what is
+ // currently open, is what actually fixes it. Calls `cascade`
+ // directly (not `place`): `place`'s own grid-first fallback would
+ // succeed for an empty `existing` regardless of this test's own
+ // point (a grid's cell *count* legitimately does depend on live
+ // occupancy - see `place`'s own doc comment on why only `cascade`
+ // takes this counter), so a `place`-level test couldn't actually
+ // isolate cascade's own behavior here.
+ let cfg = PlacementConfig::default();
+ let first = SmartPlacement::cascade(&monitor(), (400, 300), &cfg, 0);
+ let second = SmartPlacement::cascade(&monitor(), (400, 300), &cfg, 1);
+ assert_ne!(first, second, "an unchanged cascade_step of 0 vs 1 must not collapse to the same spot");
+ }
+
+ #[test]
fn snap_left_edge_yields_left_half() {
let cfg = PlacementConfig::default();
let dragged = Rect::new(2, 100, 400, 300); // x=2 is within threshold of left edge