srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/core/src/placement.rs105
-rw-r--r--crates/wayland/src/state/mod.rs6
-rw-r--r--crates/wayland/src/udev/platform.rs1
-rw-r--r--crates/wayland/src/udev/render.rs11
-rw-r--r--crates/wayland/src/winit/capture.rs27
-rw-r--r--crates/wayland/src/winit/connect.rs1
6 files changed, 136 insertions, 15 deletions
diff --git a/crates/core/src/placement.rs b/crates/core/src/placement.rs
index 91c317c..cd3c29c 100644
--- a/crates/core/src/placement.rs
+++ b/crates/core/src/placement.rs
@@ -142,6 +142,20 @@ pub fn centered_in(area: Rect, width: u32, height: u32) -> Rect {
Rect::new(x.max(area.x), y.max(area.y), width, height)
}
+/// Moves `rect` so it sits inside `area` where it can, shrinking it only if
+/// it is genuinely larger than `area`.
+///
+/// Position is corrected before size on purpose: a window that merely
+/// overhangs an edge should slide back on screen at the size it asked for,
+/// not be cut down to fit where it happened to land.
+pub fn clamp_into(rect: Rect, area: Rect) -> Rect {
+ let width = rect.width.min(area.width);
+ let height = rect.height.min(area.height);
+ let x = rect.x.clamp(area.x, (area.right() - width as i32).max(area.x));
+ let y = rect.y.clamp(area.y, (area.bottom() - height as i32).max(area.y));
+ Rect::new(x, y, width, height)
+}
+
pub struct SmartPlacement;
impl SmartPlacement {
@@ -166,10 +180,10 @@ impl SmartPlacement {
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))
+ Self::grid(monitor, existing, size, cfg, cascade_step).unwrap_or_else(|| Self::cascade(monitor, size, cfg, cascade_step))
}
- fn grid(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig) -> Option<Rect> {
+ fn grid(monitor: &Monitor, existing: &[Rect], size: (u32, u32), cfg: &PlacementConfig, cascade_step: u32) -> Option<Rect> {
let count = existing.len() + 1;
let grid_size = (count as f64).sqrt().ceil() as u32;
let grid_size = grid_size.clamp(1, cfg.max_grid);
@@ -185,14 +199,33 @@ impl SmartPlacement {
return None;
}
- for gy in 0..grid_size {
- for gx in 0..grid_size {
- let x = area.x + cfg.grid_margin as i32 + (gx * (cell_w + cfg.grid_margin)) as i32;
- let y = area.y + cfg.grid_margin as i32 + (gy * (cell_h + cfg.grid_margin)) as i32;
- let candidate = Rect::new(x, y, cell_w, cell_h);
- if !existing.iter().any(|w| w.overlaps(&candidate)) {
- return Some(Rect::new(x, y, size.0.min(cell_w), size.1.min(cell_h)));
- }
+ // Cells are visited starting from a different one each time rather
+ // than always from the top-left, so consecutive windows do not all
+ // pile into the same corner. Reported as windows spawning
+ // "predominantly left side": the scan returned the first free cell
+ // in reading order, which is the leftmost one that happens to be
+ // free, over and over.
+ let cells = grid_size * grid_size;
+ let start = cascade_step % cells.max(1);
+ for offset in 0..cells {
+ let cell = (start + offset) % cells;
+ let (gx, gy) = (cell % grid_size, cell / grid_size);
+ let x = area.x + cfg.grid_margin as i32 + (gx * (cell_w + cfg.grid_margin)) as i32;
+ let y = area.y + cfg.grid_margin as i32 + (gy * (cell_h + cfg.grid_margin)) as i32;
+ let candidate = Rect::new(x, y, cell_w, cell_h);
+ if !existing.iter().any(|w| w.overlaps(&candidate)) {
+ // The window keeps the size it actually asked for. Shrinking
+ // it to the cell is what made every window come out the same
+ // boxy shape regardless of what it wanted - reported as
+ // windows spawning "as squares". The cell decides *where* a
+ // window goes, not how big it is.
+ //
+ // Clamped into the usable area afterwards so a window bigger
+ // than its cell still lands fully on screen rather than
+ // hanging off the edge with its border out of view --
+ // reported in the same breath as spawning "a little bit out
+ // of view, ie i can't see a border".
+ return Some(clamp_into(Rect::new(x, y, size.0, size.1), area));
}
}
None
@@ -228,9 +261,9 @@ impl SmartPlacement {
let max_steps = max_steps_x.min(max_steps_y).max(1);
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)
+ let x = area.x + cfg.cascade_offset + step * cfg.cascade_offset;
+ let y = area.y + cfg.cascade_offset + step * cfg.cascade_offset;
+ clamp_into(Rect::new(x, y, width, height), area)
}
/// Given a window being dragged (its live geometry) and the monitor it's
@@ -372,6 +405,52 @@ mod tests {
}
#[test]
+ fn grid_placement_keeps_the_size_the_window_asked_for() {
+ // The reported "windows spawn as squares": every window used to be
+ // shrunk to its grid cell, so they all came out the same shape no
+ // matter what size they wanted.
+ let cfg = PlacementConfig::default();
+ let existing = [Rect::new(0, 0, 100, 100)];
+ let r = SmartPlacement::place(&monitor(), &existing, (1200, 400), &cfg, 0);
+ assert_eq!((r.width, r.height), (1200, 400), "the requested size must survive placement");
+ }
+
+ #[test]
+ fn a_window_never_lands_partly_off_screen() {
+ // "sometimes a little bit out of view, ie i can't see a border".
+ let cfg = PlacementConfig::default();
+ let area = monitor().geometry;
+ for step in 0..40u32 {
+ for size in [(400, 300), (1600, 900), (1900, 1000)] {
+ let r = SmartPlacement::place(&monitor(), &[Rect::new(0, 0, 50, 50)], size, &cfg, step);
+ assert!(
+ r.x >= area.x && r.y >= area.y && r.right() <= area.right() && r.bottom() <= area.bottom(),
+ "step {step} size {size:?} landed at {r:?}, outside {area:?}"
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn consecutive_windows_do_not_all_pile_into_the_same_corner() {
+ // "windows predominately spawn left side": the grid scan always
+ // returned the first free cell in reading order.
+ let cfg = PlacementConfig::default();
+ let existing = [Rect::new(900, 500, 80, 80)];
+ let xs: Vec<i32> = (0..4).map(|step| SmartPlacement::place(&monitor(), &existing, (300, 200), &cfg, step).x).collect();
+ assert!(xs.iter().any(|&x| x != xs[0]), "every placement started at the same x: {xs:?}");
+ }
+
+ #[test]
+ fn a_window_larger_than_the_whole_screen_is_cut_down_to_it() {
+ let cfg = PlacementConfig::default();
+ let r = SmartPlacement::place(&monitor(), &[Rect::new(0, 0, 50, 50)], (4000, 3000), &cfg, 0);
+ let area = monitor().geometry;
+ assert_eq!((r.width, r.height), (area.width, area.height));
+ assert_eq!((r.x, r.y), (area.x, area.y));
+ }
+
+ #[test]
fn snap_zone_kind_halves_split_the_area_down_the_middle() {
let area = monitor().geometry;
assert_eq!(SnapZoneKind::LeftHalf.rect(area), Rect::new(0, 0, 960, 1080));
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index fee7af3..5c1b02e 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -589,6 +589,12 @@ pub(crate) struct CompState {
/// one drag is in progress at a time. See
/// `elements::snap_preview_elements`.
pub(crate) snap_preview_buffers: Vec<SolidColorBuffer>,
+ /// Persistent solid-colour buffers for the border strips drawn into the
+ /// winit backend's screencopy pass - same stable-`Id` reasoning as
+ /// `border_side_buffers`. One shared pool rather than one per window:
+ /// a capture pass runs to completion in a single frame, so nothing
+ /// needs to persist per window between windows.
+ pub(crate) capture_border_buffers: Vec<SolidColorBuffer>,
/// Persistent solid-colour buffer backing the whole-output night-light/
/// reading-mode overlay, one per output name - same "reuse the buffer
/// so its `Id` stays stable across frames" reasoning as `border_side_
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index ea2260e..8dad7c3 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -268,6 +268,7 @@ impl UdevPlatform {
rounded_content_buffers: HashMap::new(),
border_side_buffers: HashMap::new(),
snap_preview_buffers: Vec::new(),
+ capture_border_buffers: Vec::new(),
color_filter_buffers: HashMap::new(),
last_synced_size: HashMap::new(),
provisional_size: HashSet::new(),
diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs
index 3bd9a52..9992b2c 100644
--- a/crates/wayland/src/udev/render.rs
+++ b/crates/wayland/src/udev/render.rs
@@ -668,7 +668,14 @@ impl CompState {
// instead, since `custom_elements` composites earlier-
// pushed entries over later ones - exactly backwards
// from what this overlap needs.
- if w.border_width > 0 {
+ // No border on a maximized window - see the matching
+ // gate on the GPU path above. This is the Pixman path,
+ // the one a real DRM session actually runs: the earlier
+ // fix landed only on the GPU branch, so the border kept
+ // being drawn on real hardware and was reported again as
+ // "i still see the border or at least the top border
+ // when maximized".
+ if w.border_width > 0 && !w.maximized {
let strips = decoration::border_strips(frame, w.border_width);
// Strip 0 (top) rounded on its own two corners - see
// `render_border_top`'s own doc comment - so it's a
@@ -773,7 +780,7 @@ impl CompState {
// (unlike the bottom strip) *do* need cropping against
// this same top/bottom-strip overlap, a real bug this
// comment used to claim didn't exist here at all.
- if w.border_width > 0 {
+ if w.border_width > 0 && !w.maximized {
let color = crate::state::effective_border_color(w.border_color, focused == Some(id), self.wm.borrow().theme.border_inactive_dim);
let strips = decoration::border_strips(frame, w.border_width);
// Strip 1 (bottom), the top strip's own mirror --
diff --git a/crates/wayland/src/winit/capture.rs b/crates/wayland/src/winit/capture.rs
index 095b56b..3813559 100644
--- a/crates/wayland/src/winit/capture.rs
+++ b/crates/wayland/src/winit/capture.rs
@@ -96,6 +96,7 @@ impl WaylandPlatform {
// as both on-screen render loops do it.
let monitor_bounds: Vec<srdwm_core::Rect> = self.wm.borrow().monitors().iter().map(|m| m.full_geometry).collect();
let mut occluders: Vec<srdwm_core::Rect> = Vec::new();
+ let focused = self.wm.borrow().focused_id();
for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::<Vec<_>>() {
let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
if let Some(deco) = self.state.decorations.get(&id) {
@@ -158,6 +159,32 @@ impl WaylandPlatform {
}
}
}
+ // Border strips, as plain solid fills.
+ //
+ // The on-screen loop draws the top and bottom strips from
+ // cached bitmaps so their corners round into the titlebar's
+ // curve, and only the sides as fills. This pass approximates
+ // all four with fills: corner rounding is not reproduced, so a
+ // capture is not pixel-exact at the four corners. Everything
+ // that matters for checking a border - whether one is drawn at
+ // all, where, how thick, and in what colour - is faithful.
+ //
+ // Without this a screenshot could not answer "is there a border
+ // here", which is exactly the question a border bug asks. That
+ // gap was documented and then walked into twice: a maximize
+ // border fix was checked against a capture that never draws
+ // borders, and the control silently passed for the wrong
+ // reason both times.
+ if w.border_width > 0 && !w.maximized {
+ let color = crate::state::effective_border_color(w.border_color, focused == Some(id), self.wm.borrow().theme.border_inactive_dim);
+ let frame = self.state.effective_frame(id, w.geometry);
+ for (index, strip) in crate::decoration::border_strips(frame, w.border_width).into_iter().enumerate() {
+ for fragment in crate::elements::visible_border_fragments(strip, &occluders) {
+ let buf = crate::elements::border_fragment_buffer(&mut self.state.capture_border_buffers, index);
+ custom_elements.push(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (0, 0))));
+ }
+ }
+ }
occluders.push(w.geometry);
}
custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Background | Layer::Bottom)));
diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs
index 1648826..aa59d9d 100644
--- a/crates/wayland/src/winit/connect.rs
+++ b/crates/wayland/src/winit/connect.rs
@@ -180,6 +180,7 @@ impl WaylandPlatform {
rounded_content_buffers: HashMap::new(),
border_side_buffers: HashMap::new(),
snap_preview_buffers: Vec::new(),
+ capture_border_buffers: Vec::new(),
color_filter_buffers: HashMap::new(),
last_synced_size: HashMap::new(),
provisional_size: HashSet::new(),