srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/core/src/manager/dragresize.rs15
-rw-r--r--crates/wayland/src/state/geometry.rs45
-rw-r--r--crates/wayland/src/udev/capture.rs72
3 files changed, 129 insertions, 3 deletions
diff --git a/crates/core/src/manager/dragresize.rs b/crates/core/src/manager/dragresize.rs
index 16aa539..cb151bd 100644
--- a/crates/core/src/manager/dragresize.rs
+++ b/crates/core/src/manager/dragresize.rs
@@ -169,6 +169,21 @@ impl WindowManager {
self.drag.is_some()
}
+ /// The active resize as `(window, dragged edge, the window's rect when
+ /// the drag began)`, or `None`.
+ ///
+ /// The caller needs all three to keep the *opposite* edge still. A
+ /// resize from the left or top has to hold the right or bottom edge
+ /// exactly where it was: the compositor moves the window's origin the
+ /// instant the pointer moves, but the client only commits a new buffer
+ /// some frames later, so positioning its still-old content at the new
+ /// origin drags the whole window sideways instead of growing it.
+ /// Reported as content resizing "from the right side even when i resize
+ /// from left".
+ pub fn resize_anchor(&self) -> Option<(WindowId, ResizeEdge, Rect)> {
+ self.resize.as_ref().map(|r| (r.window, r.edge, r.orig))
+ }
+
/// The window the current drag is moving, if any.
pub fn dragged_window(&self) -> Option<WindowId> {
self.drag.as_ref().map(|d| d.window)
diff --git a/crates/wayland/src/state/geometry.rs b/crates/wayland/src/state/geometry.rs
index 1910334..b8f610e 100644
--- a/crates/wayland/src/state/geometry.rs
+++ b/crates/wayland/src/state/geometry.rs
@@ -241,6 +241,37 @@ impl CompState {
/// reading `Window.geometry` unchanged - those are about this
/// compositor's own bookkeeping staying self-consistent, not about
/// matching a client's real pixels.
+ /// `geom`'s origin, corrected so an in-progress left/top resize keeps
+ /// its opposite edge fixed. Returns `geom`'s own origin unchanged when
+ /// this window is not being resized, or is being resized from an edge
+ /// whose origin does not move.
+ ///
+ /// `committed` is the client's own last-committed content size, in
+ /// logical points; `scale` converts it to the physical space every rect
+ /// here uses.
+ fn anchor_resizing_origin(
+ wm: &Rc<RefCell<WindowManager>>,
+ id: WindowId,
+ geom: srdwm_core::Rect,
+ committed: smithay::utils::Size<i32, smithay::utils::Logical>,
+ scale: f64,
+ ) -> (i32, i32) {
+ let Some((resizing, edge, orig)) = wm.borrow().resize_anchor() else { return (geom.x, geom.y) };
+ if resizing != id {
+ return (geom.x, geom.y);
+ }
+ let (mut x, mut y) = (geom.x, geom.y);
+ let physical_w = (committed.w as f64 * scale).round() as i32;
+ let physical_h = (committed.h as f64 * scale).round() as i32;
+ if physical_w > 0 && matches!(edge, srdwm_core::ResizeEdge::Left | srdwm_core::ResizeEdge::TopLeft | srdwm_core::ResizeEdge::BottomLeft) {
+ x = orig.right() - physical_w;
+ }
+ if physical_h > 0 && matches!(edge, srdwm_core::ResizeEdge::Top | srdwm_core::ResizeEdge::TopLeft | srdwm_core::ResizeEdge::TopRight) {
+ y = orig.bottom() - physical_h;
+ }
+ (x, y)
+ }
+
pub(crate) fn effective_frame(&self, id: WindowId, geom: srdwm_core::Rect) -> srdwm_core::Rect {
Self::effective_frame_of(&self.wm, &self.id_to_window, &self.pending_size_configure, id, geom)
}
@@ -362,7 +393,19 @@ impl CompState {
// calls already never did this (X11 windows have no equivalent
// shadow-margin geometry), which in hindsight was the correct
// pattern being followed there all along.
- self.space.map_element(w.clone(), (geom.x, geom.y + band), false);
+ // While resizing from a left or top edge, hold the opposite
+ // edge still. `geom` moves the instant the pointer does, but
+ // the client only commits a matching buffer some frames later,
+ // so placing its still-old content at the new origin slides the
+ // whole window sideways rather than growing it - reported as
+ // content resizing "from the right side even when i resize from
+ // left". Deriving the origin from the size the client has
+ // actually committed keeps the anchored edge exactly where the
+ // drag started, and the dragged edge catches up as commits
+ // arrive. A right/bottom drag needs none of this: its origin
+ // does not move at all.
+ let placed = Self::anchor_resizing_origin(&self.wm, id, geom, w.geometry().size, scale);
+ self.space.map_element(w.clone(), (placed.0, placed.1 + band), false);
moved = true;
if let Some(top) = w.toplevel() {
// xdg-shell position is a purely compositor-side concept --
diff --git a/crates/wayland/src/udev/capture.rs b/crates/wayland/src/udev/capture.rs
index fb4c22f..a64a73c 100644
--- a/crates/wayland/src/udev/capture.rs
+++ b/crates/wayland/src/udev/capture.rs
@@ -116,6 +116,36 @@ impl CompState {
}
}
+/// Encodes packed RGB to whatever the destination's extension asks for.
+///
+/// PPM was the only format this ever wrote, which made the capture
+/// unreadable to its actual consumers: a shell drawing thumbnails decodes
+/// PNG/JPEG/WebP and not PPM, so the file was written successfully,
+/// returned successfully, and then silently not drawn. The render itself
+/// was never the problem - only the container.
+///
+/// `.ppm` still produces PPM, so any existing caller keeps working;
+/// anything else is chosen by extension, defaulting to PNG when the
+/// extension is unfamiliar. PNG is the safe default: it is lossless and
+/// universally decodable, and at thumbnail sizes the size difference
+/// against JPEG is tens of kilobytes.
+fn encode_capture(rgb: &[u8], width: u32, height: u32, path: &str) -> Result<Vec<u8>, String> {
+ let extension = std::path::Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or_default().to_ascii_lowercase();
+ if extension == "ppm" {
+ let mut out = format!("P6\n{width} {height}\n255\n").into_bytes();
+ out.extend_from_slice(rgb);
+ return Ok(out);
+ }
+ let format = match extension.as_str() {
+ "jpg" | "jpeg" => image::ImageFormat::Jpeg,
+ _ => image::ImageFormat::Png,
+ };
+ let buffer = image::RgbImage::from_raw(width, height, rgb.to_vec()).ok_or_else(|| format!("capture buffer is not {width}x{height} RGB"))?;
+ let mut out = std::io::Cursor::new(Vec::new());
+ image::DynamicImage::ImageRgb8(buffer).write_to(&mut out, format).map_err(|e| format!("encode {extension}: {e}"))?;
+ Ok(out.into_inner())
+}
+
/// `pixels` is `Xrgb8888` - 4 bytes per pixel, little-endian, so byte
/// order in memory is B, G, R, X. PPM (`P6`) wants tightly-packed R, G, B
/// with no pad byte, hence the reorder rather than a straight `memcpy`.
@@ -150,8 +180,7 @@ fn write_ppm(pixels: &[u8], native: (u32, u32), target: Option<(u32, u32)>, path
}
}
- let mut out = format!("P6\n{tw} {th}\n255\n").into_bytes();
- out.extend_from_slice(&rgb);
+ let out = encode_capture(&rgb, tw, th, path)?;
// Written to a `.tmp` sibling and renamed into place: a reader (AGS's
// wsPreview poller) racing a partial write is exactly the kind of
// flicker/corruption a debounced, event-driven cache is supposed to
@@ -161,3 +190,42 @@ fn write_ppm(pixels: &[u8], native: (u32, u32), target: Option<(u32, u32)>, path
std::fs::write(&tmp, &out).map_err(|e| format!("write {tmp}: {e}"))?;
std::fs::rename(&tmp, path).map_err(|e| format!("rename to {path}: {e}"))
}
+
+#[cfg(test)]
+mod tests {
+ use super::encode_capture;
+
+ fn rgb(w: u32, h: u32) -> Vec<u8> {
+ (0..w * h).flat_map(|i| [(i % 251) as u8, 0x40, 0x80]).collect()
+ }
+
+ #[test]
+ fn the_extension_picks_the_container() {
+ // Checked by magic bytes rather than by trusting the call: the whole
+ // point is that the file a consumer opens is the format it expects.
+ let px = rgb(8, 4);
+ assert!(encode_capture(&px, 8, 4, "/tmp/x.ppm").unwrap().starts_with(b"P6"), "ppm");
+ assert!(encode_capture(&px, 8, 4, "/tmp/x.png").unwrap().starts_with(&[0x89, b'P', b'N', b'G']), "png");
+ assert!(encode_capture(&px, 8, 4, "/tmp/x.jpg").unwrap().starts_with(&[0xff, 0xd8]), "jpg");
+ assert!(encode_capture(&px, 8, 4, "/tmp/x.jpeg").unwrap().starts_with(&[0xff, 0xd8]), "jpeg");
+ }
+
+ #[test]
+ fn an_unfamiliar_extension_falls_back_to_png_rather_than_failing() {
+ let px = rgb(4, 4);
+ let out = encode_capture(&px, 4, 4, "/tmp/thumb.thumbnail").unwrap();
+ assert!(out.starts_with(&[0x89, b'P', b'N', b'G']));
+ }
+
+ #[test]
+ fn a_buffer_that_does_not_match_the_size_is_an_error_not_a_panic() {
+ assert!(encode_capture(&rgb(4, 4), 8, 8, "/tmp/x.png").is_err());
+ }
+
+ #[test]
+ fn the_encoded_image_round_trips_at_the_requested_size() {
+ let out = encode_capture(&rgb(9, 5), 9, 5, "/tmp/x.png").unwrap();
+ let decoded = image::load_from_memory(&out).expect("our own png must decode");
+ assert_eq!((decoded.width(), decoded.height()), (9, 5));
+ }
+}