srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-07-14 22:34:00 +0200
committersrdusr <[email protected]>2026-07-14 22:34:00 +0200
commitaa7b7278b9e758985197170bcee4f0d2d4cb4590 (patch)
treea7426dd4851afb3bf5960788baf0bc78070ba4b0 /crates/wayland/src/udev
parent5ef39bd041c64c66f629f9fac53ecc0cdd7aff11 (diff)
downloadsrdwm-aa7b7278b9e758985197170bcee4f0d2d4cb4590.tar.gz
srdwm-aa7b7278b9e758985197170bcee4f0d2d4cb4590.zip
Workspace capture writes a readable image, and left-edge resize holds its anchor
Two things, and the first is smaller than I told anyone. WORKSPACE CAPTURE. I said off-screen workspace capture did not exist and would need building. It already did: udev/capture.rs renders a workspace that is not on screen, at the target monitor's native size, downscaled to a requested size, wallpaper included. Verified on the live DRM session rather than from the source - capturing the active workspace and a non-visible one gave 320x180 images with mean luminance 0.067 and 0.137, so the second is genuinely a different render and not a copy of what is presented. The only thing missing was the container. It wrote PPM, which the shells that want thumbnails cannot decode, so the file was written successfully, returned successfully, and silently not drawn - the same failure class as a capture pass that omits a tier. encode_capture now picks the format from the destination's extension: .ppm still writes PPM so existing callers keep working, .jpg/.jpeg write JPEG, anything else writes PNG. Four tests check the actual magic bytes rather than trusting the call, plus the unfamiliar extension fallback and a size-mismatch error. LEFT-EDGE RESIZE. Reported as content resizing "from the right side even when i resize from left". The window's origin moves the instant the pointer does, but the client only commits a matching buffer some frames later, so its still-old content was being placed at the new origin - which slides the whole window rather than growing it, and leaves the edge that should be nailed down drifting. sync_geometry now derives the origin from the size the client has actually committed when the drag is from a left or top edge, so the opposite edge stays exactly where the drag started and the dragged edge catches up as commits arrive. A right or bottom drag is untouched: its origin never moves. 533 tests pass, clippy clean.
Diffstat (limited to 'crates/wayland/src/udev')
-rw-r--r--crates/wayland/src/udev/capture.rs72
1 files changed, 70 insertions, 2 deletions
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));
+ }
+}