Implement working screenshot capture with wlr-screencopy

- Fixed brace mismatch and duplicate code in screenshot.rs
- Increased SHM pool size to 256MB for high-res displays
- Corrected wlr-screencopy protocol usage: send copy request in Buffer event
- Fixed background handling: set_background now updates LockedSurface.background
- Removed dummy background creation; screenshots set when ready
- Added format conversion for Xbgr8888 and Xrgb8888 to ARGB32
- Proper Y-inversion handling based on flags

The lock screen now displays captured screenshots as background.
Screenshots are taken immediately after lock, before UI is shown.
Matches swaylock-effects behavior: lock appears first, then screenshot
applies when ready (no blocking).

Fixes: black/red screen issues, screenshot not displaying.
This commit is contained in:
2026-03-06 21:21:47 +01:00
parent bd6f1df391
commit 8aa84209a8
5 changed files with 660 additions and 113 deletions
Generated
+35
View File
@@ -645,6 +645,21 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.32" version = "0.3.32"
@@ -652,6 +667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@@ -671,6 +687,12 @@ dependencies = [
"futures-util", "futures-util",
] ]
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.32" version = "0.3.32"
@@ -682,6 +704,12 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.32" version = "0.3.32"
@@ -694,9 +722,13 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
] ]
@@ -2057,11 +2089,13 @@ dependencies = [
name = "wayrustlock" name = "wayrustlock"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"cairo-rs", "cairo-rs",
"chrono", "chrono",
"clap", "clap",
"env_logger", "env_logger",
"fastblur", "fastblur",
"futures",
"image", "image",
"log", "log",
"pam-client", "pam-client",
@@ -2072,6 +2106,7 @@ dependencies = [
"users", "users",
"wayland-client", "wayland-client",
"wayland-protocols", "wayland-protocols",
"wayland-protocols-wlr",
"xkbcommon", "xkbcommon",
"zeroize", "zeroize",
] ]
+3
View File
@@ -7,6 +7,9 @@ edition = "2021"
smithay-client-toolkit = { version = "0.19", features = ["calloop"] } smithay-client-toolkit = { version = "0.19", features = ["calloop"] }
wayland-client = "0.31" wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client", "unstable"] } wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
anyhow = "1.0"
futures = "0.3"
cairo-rs = { version = "0.20", features = ["png"] } cairo-rs = { version = "0.20", features = ["png"] }
image = "0.25" image = "0.25"
fastblur = "0.1" fastblur = "0.1"
+34 -46
View File
@@ -1,7 +1,8 @@
use cairo::ImageSurface; use cairo::ImageSurface;
use std::error::Error; use std::error::Error;
use std::time::Instant; use std::time::Instant;
use wayland_client::protocol::{wl_shm, wl_surface}; use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
use wayland_client::Proxy;
use crate::config::Config; use crate::config::Config;
use crate::input::{InputAction, InputHandler}; use crate::input::{InputAction, InputHandler};
@@ -23,11 +24,17 @@ pub struct LockedSurface {
temp_screenshot_shown: bool, temp_screenshot_shown: bool,
last_update: Instant, last_update: Instant,
wayland_surface: Option<wl_surface::WlSurface>, wayland_surface: Option<wl_surface::WlSurface>,
output: wl_output::WlOutput,
} }
impl LockedSurface { impl LockedSurface {
/// Create a new locked surface for an output /// Create a new locked surface for an output
pub fn new(width: i32, height: i32, config: &Config) -> Option<Self> { pub fn new(
width: i32,
height: i32,
config: &Config,
output: wl_output::WlOutput,
) -> Option<Self> {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return None; return None;
} }
@@ -35,38 +42,8 @@ impl LockedSurface {
let renderer = Renderer::new(width, height, config.clone()); let renderer = Renderer::new(width, height, config.clone());
let input_handler = InputHandler::new(config.clone()); let input_handler = InputHandler::new(config.clone());
// Create background if screenshots are enabled // Background will be set later when screenshot is captured (if screenshots enabled)
let background = if config.screenshots { let background = None;
// For now, create a dummy screenshot with the output dimensions
// In a real implementation, this would capture actual screenshots via Wayland
let mut screenshot = Screenshot {
width: width as u32,
height: height as u32,
data: vec![0u8; (width * height * 4) as usize],
};
// Fill with a dark gray color (similar to swaylock default)
for i in 0..(screenshot.width * screenshot.height) as usize {
let offset = i * 4;
screenshot.data[offset] = 40; // R
screenshot.data[offset + 1] = 44; // G
screenshot.data[offset + 2] = 52; // B
screenshot.data[offset + 3] = 255; // A
}
// Apply effects if configured
if let Some((blur_radius, blur_times)) = config.effect_blur {
screenshot.apply_blur(blur_radius, blur_times);
}
if let Some((vignette_base, vignette_factor)) = config.effect_vignette {
screenshot.apply_vignette(vignette_base, vignette_factor);
}
Some(screenshot.as_image_surface())
} else {
None
};
Some(Self { Some(Self {
width, width,
@@ -81,6 +58,7 @@ impl LockedSurface {
temp_screenshot_shown: false, temp_screenshot_shown: false,
last_update: Instant::now(), last_update: Instant::now(),
wayland_surface: None, wayland_surface: None,
output,
}) })
} }
@@ -334,10 +312,20 @@ impl LockedSurface {
self.wayland_surface.as_ref() self.wayland_surface.as_ref()
} }
/// Get the output associated with this locked surface
pub fn output(&self) -> &wl_output::WlOutput {
&self.output
}
/// Check if this surface has a Wayland surface attached /// Check if this surface has a Wayland surface attached
pub fn has_wayland_surface(&self) -> bool { pub fn has_wayland_surface(&self) -> bool {
self.wayland_surface.is_some() self.wayland_surface.is_some()
} }
/// Set the background image for this locked surface
pub fn set_background(&mut self, surface: ImageSurface) {
self.background = Some(surface);
}
} }
/// Manager for all locked surfaces (multiple outputs) /// Manager for all locked surfaces (multiple outputs)
@@ -358,8 +346,8 @@ impl LockManager {
} }
/// Add a locked surface for an output /// Add a locked surface for an output
pub fn add_surface(&mut self, width: i32, height: i32) -> bool { pub fn add_surface(&mut self, width: i32, height: i32, output: wl_output::WlOutput) -> bool {
match LockedSurface::new(width, height, &self.config) { match LockedSurface::new(width, height, &self.config, output) {
Some(surface) => { Some(surface) => {
self.surfaces.push(surface); self.surfaces.push(surface);
true true
@@ -423,16 +411,6 @@ impl LockManager {
self.surfaces.get_mut(index) self.surfaces.get_mut(index)
} }
/// Initialize lock surfaces for all outputs (called after session is locked)
pub fn initialize_lock_surfaces(&mut self) {
// In a real implementation, this would create Wayland surfaces for each output
// For now, we'll create dummy surfaces with default dimensions
if self.surfaces.is_empty() {
// Add a default surface (single monitor)
self.add_surface(1920, 1080);
}
}
/// Toggle temp screenshot peek mode /// Toggle temp screenshot peek mode
pub fn toggle_peek(&mut self) { pub fn toggle_peek(&mut self) {
for surface in &mut self.surfaces { for surface in &mut self.surfaces {
@@ -449,4 +427,14 @@ impl LockManager {
.iter_mut() .iter_mut()
.find(|surface| surface.matches_surface(wayland_surface)) .find(|surface| surface.matches_surface(wayland_surface))
} }
/// Find a locked surface by output
pub fn find_surface_by_output(
&mut self,
output: &wl_output::WlOutput,
) -> Option<&mut LockedSurface> {
self.surfaces
.iter_mut()
.find(|surface| surface.output().id() == output.id())
}
} }
+307 -16
View File
@@ -9,10 +9,21 @@ mod util;
use config::Config; use config::Config;
use lock::LockManager; use lock::LockManager;
use screenshot::{CaptureData, Screenshot, ScreenshotManager};
use std::collections::HashSet;
use std::error::Error; use std::error::Error;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use wayland_client::globals::GlobalList;
use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::Proxy;
use wayland_client::QueueHandle;
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{
Flags, ZwlrScreencopyFrameV1,
};
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use zeroize::Zeroizing; use zeroize::Zeroizing;
fn setup_file_logging() { fn setup_file_logging() {
@@ -119,6 +130,7 @@ fn lock_wayland_session(
compositor_state: CompositorState, compositor_state: CompositorState,
output_state: OutputState, output_state: OutputState,
registry_state: RegistryState, registry_state: RegistryState,
globals: GlobalList,
session_lock_state: SessionLockState, session_lock_state: SessionLockState,
seat_state: SeatState, seat_state: SeatState,
shm_state: Shm, shm_state: Shm,
@@ -131,6 +143,12 @@ fn lock_wayland_session(
exit: bool, exit: bool,
auth_tx: Option<channel::Sender<Zeroizing<String>>>, auth_tx: Option<channel::Sender<Zeroizing<String>>>,
unlocking: bool, unlocking: bool,
screenshot_manager: Option<ScreenshotManager>,
screenshot_frames: Vec<Option<ZwlrScreencopyFrameV1>>,
captured_backgrounds: Vec<Option<cairo::ImageSurface>>,
locked_outputs: HashSet<WlOutput>,
outputs: Vec<WlOutput>,
lock_surface_outputs: Vec<WlOutput>,
} }
impl SessionLockHandler for WaylandLock { impl SessionLockHandler for WaylandLock {
@@ -146,15 +164,22 @@ fn lock_wayland_session(
log_to_file("Session LOCKED - creating surfaces"); log_to_file("Session LOCKED - creating surfaces");
eprintln!("SESSION LOCKED - creating lock surfaces"); eprintln!("SESSION LOCKED - creating lock surfaces");
// Take ownership of session_lock
let lock_ref = &mut session_lock; let lock_ref = &mut session_lock;
// Create lock surfaces for all outputs let outputs: Vec<WlOutput> = self.output_state.outputs().collect();
for output in self.output_state.outputs() { let output_count = outputs.len();
self.outputs = outputs.clone();
self.screenshot_frames = vec![None; output_count];
self.captured_backgrounds = vec![None; output_count];
for output in &outputs {
log_to_file(&format!("Creating lock surface for output")); log_to_file(&format!("Creating lock surface for output"));
let surface = self.compositor_state.create_surface(qh); let surface = self.compositor_state.create_surface(qh);
let lock_surface = lock_ref.create_lock_surface(surface, &output, qh); let lock_surface = lock_ref.create_lock_surface(surface, output, qh);
self.lock_surfaces.push(lock_surface); self.lock_surfaces.push(lock_surface);
self.lock_surface_outputs.push(output.clone());
self.locked_outputs.insert(output.clone());
} }
log_to_file(&format!( log_to_file(&format!(
@@ -164,8 +189,35 @@ fn lock_wayland_session(
self.session_lock = Some(session_lock); self.session_lock = Some(session_lock);
if let Ok(mut lock_manager) = self.lock_manager.lock() { if self.config.screenshots {
lock_manager.initialize_lock_surfaces(); log::info!("Initializing screenshot capture...");
match ScreenshotManager::new(&self.globals, qh) {
Ok(manager) => {
self.screenshot_manager = Some(manager);
log::info!("Screenshot manager initialized");
for (idx, output) in self.outputs.iter().enumerate() {
log::info!("Starting screenshot capture for output {}...", idx);
match self.screenshot_manager.as_mut().unwrap().capture_output(
output,
qh,
CaptureData::new(idx),
) {
Ok(frame) => {
self.screenshot_frames[idx] = Some(frame);
log::debug!("Screenshot capture initiated for output {}", idx);
}
Err(e) => {
log::error!("Failed to capture output {}: {}", idx, e);
}
}
}
}
Err(e) => {
log::warn!("Screenshot capture not available: {}", e);
self.screenshot_manager = None;
}
}
} }
} }
@@ -204,7 +256,29 @@ fn lock_wayland_session(
eprintln!("CONFIGURE: {}x{}", width, height); eprintln!("CONFIGURE: {}x{}", width, height);
let surface_added = if let Ok(mut lock_manager) = self.lock_manager.lock() { let surface_added = if let Ok(mut lock_manager) = self.lock_manager.lock() {
lock_manager.add_surface(width as i32, height as i32) let surface_wl_id = session_lock_surface.wl_surface().id();
let output_idx = self
.lock_surfaces
.iter()
.position(|s| s.wl_surface().id() == surface_wl_id);
if let Some(idx) = output_idx {
if idx < self.lock_surface_outputs.len() {
let output = &self.lock_surface_outputs[idx];
let (width, height) = configure.new_size;
lock_manager.add_surface(width as i32, height as i32, output.clone())
} else {
log::error!(
"lock_surface_outputs index out of bounds: {} vs {}",
idx,
self.lock_surface_outputs.len()
);
false
}
} else {
log::error!("Could not find lock surface in self.lock_surfaces");
false
}
} else { } else {
false false
}; };
@@ -216,7 +290,6 @@ fn lock_wayland_session(
} }
log::debug!("Surface added to lock manager"); log::debug!("Surface added to lock manager");
self.lock_surfaces.push(session_lock_surface.clone());
if let Ok(mut lock_manager) = self.lock_manager.lock() { if let Ok(mut lock_manager) = self.lock_manager.lock() {
let surface_count = lock_manager.surface_count(); let surface_count = lock_manager.surface_count();
@@ -229,7 +302,36 @@ fn lock_wayland_session(
if let Some(locked_surface) = lock_manager.get_surface_mut(surface_count - 1) { if let Some(locked_surface) = lock_manager.get_surface_mut(surface_count - 1) {
locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone()); locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone());
// Render the surface first if self.config.screenshots {
let output = locked_surface.output();
let output_idx = self.outputs.iter().position(|o| o.id() == output.id());
if let Some(idx) = output_idx {
if let Some(background) = self
.captured_backgrounds
.get_mut(idx)
.and_then(|b| b.take())
{
log::info!(
"Using captured screenshot background for output {}",
idx
);
locked_surface.set_background(background);
} else {
log::warn!("No screenshot available for output {}, will render without background", idx);
}
} else {
log::warn!(
"Output not found in self.outputs, cannot assign screenshot"
);
}
}
}
if let Some(locked_surface) = lock_manager.get_surface_mut(surface_count - 1) {
locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone());
// Render the surface
locked_surface.renderer.render(); locked_surface.renderer.render();
match locked_surface.renderer.get_pixel_data() { match locked_surface.renderer.get_pixel_data() {
@@ -364,9 +466,15 @@ fn lock_wayland_session(
output: wl_output::WlOutput, output: wl_output::WlOutput,
) { ) {
log::info!("New output detected: creating lock surface"); log::info!("New output detected: creating lock surface");
if self.locked_outputs.contains(&output) {
log::debug!("Output already has lock surface, ignoring");
return;
}
if let Some(ref session_lock) = self.session_lock { if let Some(ref session_lock) = self.session_lock {
let surface = self.compositor_state.create_surface(qh); let surface = self.compositor_state.create_surface(qh);
let _lock_surface = session_lock.create_lock_surface(surface, &output, qh); let _lock_surface = session_lock.create_lock_surface(surface, &output, qh);
self.locked_outputs.insert(output.clone());
self.lock_surface_outputs.push(output.clone());
} }
} }
@@ -634,6 +742,184 @@ fn lock_wayland_session(
smithay_client_toolkit::delegate_keyboard!(WaylandLock); smithay_client_toolkit::delegate_keyboard!(WaylandLock);
smithay_client_toolkit::delegate_pointer!(WaylandLock); smithay_client_toolkit::delegate_pointer!(WaylandLock);
impl wayland_client::Dispatch<ZwlrScreencopyManagerV1, ()> for WaylandLock {
fn event(
state: &mut Self,
_manager: &ZwlrScreencopyManagerV1,
_event: <ZwlrScreencopyManagerV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
// No events expected from the manager
}
}
impl wayland_client::Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
fn event(
state: &mut Self,
frame: &ZwlrScreencopyFrameV1,
event: <ZwlrScreencopyFrameV1 as wayland_client::Proxy>::Event,
_data: &CaptureData,
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
let output_idx = _data.output_idx;
log::debug!("Screencopy event for output {}: {:?}", output_idx, event);
match event {
wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Buffer {
format,
width,
height,
stride,
} => {
log::debug!("Buffer event: {}x{}, stride={}, format={:?}", width, height, stride, format);
// Extract format value from the compositor's Buffer event
let format_value = match format {
wayland_client::WEnum::Value(f) => f,
wayland_client::WEnum::Unknown(_) => {
log::error!("Unknown format value");
return;
}
};
// Store buffer info including the actual format
if let Ok(mut info_guard) = _data.info.lock() {
*info_guard = Some(screenshot::BufferInfo {
width,
height,
stride,
format: format_value,
});
}
// Create SHM buffer with the EXACT format the compositor specified
if let Ok(mut buffer_guard) = _data.buffer.lock() {
if let Ok((buf, _canvas)) = state.pool.create_buffer(
i32::try_from(width).unwrap(),
i32::try_from(height).unwrap(),
i32::try_from(stride).unwrap(),
format_value,
) {
// Store the buffer for later use in Ready event
*buffer_guard = Some(buf);
log::debug!("Created SHM buffer for screencopy with format {:?}", format_value);
// Immediately send copy request (protocol requires this after buffer creation)
if let Some(buffer) = buffer_guard.as_ref() {
frame.copy(buffer.wl_buffer());
log::debug!("Sent copy request for screenshot");
}
} else {
log::error!("Failed to create SHM buffer");
}
}
}
wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Flags { flags } => {
log::debug!("Flags: {:?}", flags);
if let Ok(mut flags_guard) = _data.flags.lock() {
let flags_value = match flags {
wayland_client::WEnum::Value(f) => f,
wayland_client::WEnum::Unknown(_) => {
log::warn!("Unknown flags value");
return;
}
};
*flags_guard = Some(flags_value);
}
}
wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Ready {
tv_sec_hi: _,
tv_sec_lo: _,
tv_nsec: _
} => {
log::info!("Ready event: processing screenshot for output {}", output_idx);
// Extract all data from CaptureData
let info_opt = _data.info.lock().ok().and_then(|i| i.clone());
let buffer_opt = _data.buffer.lock().ok().and_then(|mut b| b.take());
let flags_opt = _data.flags.lock().ok().and_then(|f| f.clone());
if let (Some(info), Some(buffer), Some(flags)) = (info_opt, buffer_opt, flags_opt) {
// Determine if Y-inversion is needed (Y_INVERT flag is bit 0)
let y_invert = flags.bits() & 1 != 0;
let handle = screenshot::ScreencopyBufferHandle {
buffer,
info: screenshot::BufferInfo {
width: info.width,
height: info.height,
stride: info.stride,
format: info.format,
},
y_invert,
};
match state.screenshot_manager.as_ref().unwrap().buffer_to_surface(handle, &mut state.pool) {
Ok(surface) => {
// Apply effects if configured
let mut screenshot = Screenshot::new(surface);
if let Err(e) = screenshot.apply_effects(&state.config) {
log::error!("Failed to apply effects: {}", e);
}
let surface = screenshot.into_inner();
// Save screenshot to file for debugging if debug mode enabled
if state.config.debug {
let path = format!("/tmp/wayrustlock_output{}.png", output_idx);
match std::fs::File::create(&path) {
Ok(mut file) => {
if let Err(e) = surface.write_to_png(&mut file) {
log::error!("Failed to write PNG to {}: {}", path, e);
} else {
log::info!("Saved screenshot to {}", path);
}
}
Err(e) => {
log::error!("Failed to create file {}: {}", path, e);
}
}
}
if output_idx < state.outputs.len() {
let output = &state.outputs[output_idx];
if let Ok(mut lock_manager) = state.lock_manager.lock() {
if let Some(locked_surface) = lock_manager.find_surface_by_output(output) {
log::debug!("Setting background directly on lock surface for output {}", output_idx);
locked_surface.set_background(surface);
} else {
log::debug!("Storing background in captured_backgrounds for output {}", output_idx);
state.captured_backgrounds[output_idx] = Some(surface);
}
} else {
log::debug!("Failed to lock manager, storing background");
state.captured_backgrounds[output_idx] = Some(surface);
}
} else {
log::error!("output_idx {} out of bounds (outputs len {})", output_idx, state.outputs.len());
}
}
Err(e) => {
log::error!("Failed to convert buffer to surface: {}", e);
}
}
} else {
log::error!("Missing buffer data for Ready event");
}
// Clean up frame
frame.destroy();
}
wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::Event::Failed => {
log::error!("Screenshot capture failed for output {}", output_idx);
frame.destroy();
}
_ => {}
}
}
}
let conn = Connection::connect_to_env()?; let conn = Connection::connect_to_env()?;
let (globals, event_queue) = registry_queue_init(&conn)?; let (globals, event_queue) = registry_queue_init(&conn)?;
let qh: QueueHandle<WaylandLock> = event_queue.handle(); let qh: QueueHandle<WaylandLock> = event_queue.handle();
@@ -645,26 +931,32 @@ fn lock_wayland_session(
let mut state = WaylandLock { let mut state = WaylandLock {
loop_handle: event_loop.handle(), loop_handle: event_loop.handle(),
conn: conn.clone(), conn: conn.clone(),
lock_manager,
config,
ctrlc_exit: ctrlc_exit.clone(),
auth_tx: Some(auth_tx),
compositor_state: CompositorState::bind(&globals, &qh)?, compositor_state: CompositorState::bind(&globals, &qh)?,
output_state: OutputState::new(&globals, &qh), output_state: OutputState::new(&globals, &qh),
registry_state: RegistryState::new(&globals), registry_state: RegistryState::new(&globals),
session_lock_state: SessionLockState::new(&globals, &qh), session_lock_state: SessionLockState::new(&globals, &qh),
seat_state: SeatState::new(&globals, &qh), seat_state: SeatState::new(&globals, &qh),
shm_state: Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?, shm_state: Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?,
pool: SlotPool::new( pool: SlotPool::new(
1920 * 1080 * 4 * 3, // triple buffering for smooth rendering 256 * 1024 * 1024, // 256 MB pool to support high-resolution displays and screenshots
&Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?, &Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?,
) )
.map_err(|e| format!("Failed to create slot pool: {:?}", e))?, .map_err(|e| format!("Failed to create slot pool: {:?}", e))?,
globals,
session_lock: None, session_lock: None,
lock_surfaces: Vec::new(), lock_surfaces: Vec::new(),
lock_manager,
config,
ctrlc_exit: ctrlc_exit.clone(),
exit: false, exit: false,
auth_tx: Some(auth_tx),
unlocking: false, unlocking: false,
screenshot_manager: None,
screenshot_frames: Vec::new(),
captured_backgrounds: Vec::new(),
locked_outputs: HashSet::new(),
outputs: Vec::new(),
lock_surface_outputs: Vec::new(),
}; };
state.session_lock = Some( state.session_lock = Some(
@@ -747,7 +1039,6 @@ fn run_demonstration_mode(
{ {
let mut lock_manager = lock_manager.lock().unwrap(); let mut lock_manager = lock_manager.lock().unwrap();
lock_manager.initialize_lock_surfaces();
log::info!( log::info!(
"Initialized {} lock surface(s)", "Initialized {} lock surface(s)",
lock_manager.surface_count() lock_manager.surface_count()
+278 -48
View File
@@ -1,55 +1,85 @@
//!
//! This module provides functionality to capture the current screen contents
//! and apply visual effects like blur and vignette, similar to swaylock-effects.
use anyhow::{Context, Result};
use cairo::ImageSurface;
use log::debug;
use smithay_client_toolkit::shm::{slot::Buffer, slot::SlotPool};
use std::sync::Mutex;
use wayland_client::globals::GlobalList;
use wayland_client::protocol::{wl_output, wl_shm};
use wayland_client::{Dispatch, QueueHandle};
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{
Flags, ZwlrScreencopyFrameV1,
};
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
use crate::config::Config;
/// A captured screenshot with optional visual effects applied.
pub struct Screenshot { pub struct Screenshot {
pub width: u32, surface: ImageSurface,
pub height: u32,
pub data: Vec<u8>,
} }
impl Screenshot { impl Screenshot {
pub fn capture( /// Create a new screenshot from a Cairo surface.
_output: wayland_client::protocol::wl_output::WlOutput, pub fn new(surface: ImageSurface) -> Self {
width: i32, Self { surface }
height: i32,
) -> Result<Self, String> {
let width = width as u32;
let height = height as u32;
let size = (width * height * 4) as usize;
let mut data = vec![0u8; size];
for i in 0..(width * height) as usize {
let offset = i * 4;
data[offset] = 40;
data[offset + 1] = 44;
data[offset + 2] = 52;
data[offset + 3] = 255;
} }
Ok(Self { /// Get a reference to the underlying surface.
width, pub fn surface(&self) -> &ImageSurface {
height, &self.surface
data,
})
} }
pub fn apply_blur(&mut self, radius: u32, times: u32) { /// Consume the screenshot and return the underlying Cairo surface.
pub fn into_inner(self) -> ImageSurface {
self.surface
}
/// Apply configured visual effects to the screenshot.
pub fn apply_effects(&mut self, config: &Config) -> Result<()> {
if let Some((radius, times)) = config.effect_blur {
self.apply_blur(radius, times)?;
}
if let Some((base, factor)) = config.effect_vignette {
self.apply_vignette(base, factor);
}
Ok(())
}
/// Apply a Gaussian blur effect.
pub fn apply_blur(&mut self, radius: u32, times: u32) -> Result<()> {
if radius == 0 || times == 0 { if radius == 0 || times == 0 {
return; return Ok(());
} }
let width = self.surface.width();
let height = self.surface.height();
let stride = self.surface.stride() as usize;
let mut data = vec![0u8; stride * height as usize];
self.surface
.with_data(|src| data.copy_from_slice(src))
.context("Failed to get surface data")?;
// Convert to image::RgbaImage for processing
let mut img: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> = let mut img: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =
image::ImageBuffer::from_raw(self.width, self.height, self.data.clone()) image::ImageBuffer::from_raw(width as u32, height as u32, data)
.expect("Failed to create image buffer"); .context("Failed to create image buffer")?;
for _ in 0..times { for _ in 0..times {
let mut rgb_data: Vec<[u8; 3]> = let mut rgb_data: Vec<[u8; 3]> =
Vec::with_capacity((self.width * self.height) as usize); Vec::with_capacity((width as usize) * (height as usize));
for pixel in img.pixels() { for pixel in img.pixels() {
rgb_data.push([pixel[0], pixel[1], pixel[2]]); rgb_data.push([pixel[0], pixel[1], pixel[2]]);
} }
fastblur::gaussian_blur( fastblur::gaussian_blur(
&mut rgb_data, &mut rgb_data,
self.width as usize, width as usize,
self.height as usize, height as usize,
radius as f32, radius as f32,
); );
@@ -60,40 +90,240 @@ impl Screenshot {
} }
} }
self.data = img.into_raw(); // Copy back to surface
let new_data = img.into_raw();
let mut surface_data = self.surface.data()?;
surface_data.copy_from_slice(&new_data);
Ok(())
} }
/// Apply a vignette effect (darken edges).
pub fn apply_vignette(&mut self, base: f32, factor: f32) { pub fn apply_vignette(&mut self, base: f32, factor: f32) {
let center_x = self.width as f32 / 2.0; let width = self.surface.width();
let center_y = self.height as f32 / 2.0; let height = self.surface.height();
let center_x = width as f32 / 2.0;
let center_y = height as f32 / 2.0;
let max_distance = (center_x * center_x + center_y * center_y).sqrt(); let max_distance = (center_x * center_x + center_y * center_y).sqrt();
for y in 0..self.height { let stride = self.surface.stride() as usize;
for x in 0..self.width { let mut data = vec![0u8; stride * height as usize];
self.surface
.with_data(|src| data.copy_from_slice(src))
.unwrap();
for y in 0..height {
for x in 0..width {
let dx = x as f32 - center_x; let dx = x as f32 - center_x;
let dy = y as f32 - center_y; let dy = y as f32 - center_y;
let distance = (dx * dx + dy * dy).sqrt(); let distance = (dx * dx + dy * dy).sqrt();
let vignette_factor = base + (1.0 - base) * (distance / max_distance).powf(factor); let vignette_factor = base + (1.0 - base) * (distance / max_distance).powf(factor);
let index = ((y * self.width + x) * 4) as usize; let index = ((y * width + x) * 4) as usize;
for i in 0..3 { for i in 0..3 {
let value = self.data[index + i] as f32 * vignette_factor; let value = data[index + i] as f32 * vignette_factor;
self.data[index + i] = value.clamp(0.0, 255.0) as u8; data[index + i] = value.clamp(0.0, 255.0) as u8;
}
} }
} }
} }
pub fn as_image_surface(&self) -> cairo::ImageSurface { let mut surface_data = self.surface.data().unwrap();
let surface = cairo::ImageSurface::create( surface_data.copy_from_slice(&data);
}
}
#[derive(Clone)]
/// Information about a buffer from the screencopy protocol.
pub struct BufferInfo {
pub width: u32,
pub height: u32,
pub stride: u32,
pub format: wl_shm::Format,
}
/// Handle to a captured buffer that can be converted to a Cairo surface.
pub struct ScreencopyBufferHandle {
pub buffer: Buffer,
pub info: BufferInfo,
pub y_invert: bool,
}
/// Manager for the wlr-screencopy protocol.
pub struct ScreenshotManager {
manager: Option<ZwlrScreencopyManagerV1>,
}
impl ScreenshotManager {
/// Bind to the wlr-screencopy global and create a new manager.
///
/// Returns `Ok(Self)` if the protocol is available, otherwise `Err`.
pub fn new<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self>
where
D: Dispatch<ZwlrScreencopyManagerV1, ()> + 'static,
{
let manager = globals
.bind::<ZwlrScreencopyManagerV1, _, _>(qh, 1..=3, ())
.ok();
if manager.is_none() {
debug!("zwlr_screencopy_manager_v1 not available");
}
Ok(Self { manager })
}
/// Returns `true` if the wlr-screencopy protocol is available.
pub fn is_available(&self) -> bool {
self.manager.is_some()
}
/// Initiate a screencopy operation for the given output.
///
/// This method sends a screencopy request and returns the frame object.
/// The frame events will be dispatched to the provided queue's dispatcher
/// with the given user data.
pub fn capture_output<D>(
&self,
output: &wl_output::WlOutput,
qh: &QueueHandle<D>,
user_data: CaptureData,
) -> Result<ZwlrScreencopyFrameV1>
where
D: Dispatch<ZwlrScreencopyFrameV1, CaptureData> + 'static,
{
let manager = self.manager.as_ref().context("Screencopy not available")?;
let frame = manager.capture_output(0, output, qh, user_data);
Ok(frame)
}
/// Convert a captured buffer to a Cairo ImageSurface.
pub fn buffer_to_surface(
&self,
handle: ScreencopyBufferHandle,
pool: &mut SlotPool,
) -> Result<ImageSurface> {
let info = handle.info;
let y_invert = handle.y_invert;
let canvas = handle
.buffer
.canvas(pool)
.context("Failed to get buffer canvas")?;
let pixel_width = (info.width * 4) as usize;
let stride = info.stride as usize;
let height = info.height as usize;
if stride < pixel_width {
anyhow::bail!("Stride smaller than pixel width");
}
let raw_data = {
let mut data = vec![0u8; (info.width * info.height * 4) as usize];
for row in 0..height {
let src_offset = row * stride;
let dst_offset = row * pixel_width;
data[dst_offset..dst_offset + pixel_width]
.copy_from_slice(&canvas[src_offset..src_offset + pixel_width]);
}
data
};
let converted_data = match info.format {
wayland_client::protocol::wl_shm::Format::Argb8888 => raw_data,
wayland_client::protocol::wl_shm::Format::Xbgr8888 => {
convert_xbgr8888_to_argb32(&raw_data, info.width as usize, info.height as usize)
}
wayland_client::protocol::wl_shm::Format::Xrgb8888 => {
convert_xrgb8888_to_argb32(&raw_data, info.width as usize, info.height as usize)
}
_ => {
log::warn!("Unsupported format {:?}, using raw data as-is", info.format);
raw_data
}
};
if y_invert {
let mut flipped = vec![0u8; (info.width * info.height * 4) as usize];
let src_stride = (info.width * 4) as usize;
for row in 0..height {
let src_row = height - 1 - row;
let src_offset = src_row * src_stride;
let dst_offset = row * src_stride;
flipped[dst_offset..dst_offset + src_stride]
.copy_from_slice(&converted_data[src_offset..src_offset + src_stride]);
}
return Ok(ImageSurface::create_for_data(
flipped,
cairo::Format::ARgb32, cairo::Format::ARgb32,
self.width as i32, info.width as i32,
self.height as i32, info.height as i32,
src_stride as i32,
) )
.expect("Failed to create image surface"); .context("Failed to create flipped Cairo surface")?);
}
// TODO: Properly copy pixel data to surface using cairo API Ok(ImageSurface::create_for_data(
// For now, return empty surface converted_data,
surface cairo::Format::ARgb32,
info.width as i32,
info.height as i32,
pixel_width as i32,
)
.context("Failed to create Cairo surface")?)
}
}
/// Convert Xbgr8888 buffer data to ARGB32 format.
fn convert_xbgr8888_to_argb32(data: &[u8], width: usize, height: usize) -> Vec<u8> {
let mut result = vec![0u8; width * height * 4];
for y in 0..height {
for x in 0..width {
let src_idx = (y * width + x) * 4;
let dst_idx = (y * width + x) * 4;
result[dst_idx] = 255;
result[dst_idx + 1] = data[src_idx + 2];
result[dst_idx + 2] = data[src_idx + 1];
result[dst_idx + 3] = data[src_idx];
}
}
result
}
/// Convert Xrgb8888 buffer data to ARGB32 format.
fn convert_xrgb8888_to_argb32(data: &[u8], width: usize, height: usize) -> Vec<u8> {
let mut result = vec![0u8; width * height * 4];
for y in 0..height {
for x in 0..width {
let src_idx = (y * width + x) * 4;
let dst_idx = (y * width + x) * 4;
result[dst_idx] = 255;
result[dst_idx + 1] = data[src_idx + 1];
result[dst_idx + 2] = data[src_idx + 2];
result[dst_idx + 3] = data[src_idx + 3];
}
}
result
}
/// User data associated with a screencopy frame request.
///
/// Stores intermediate data needed to assemble the final screenshot once
/// all frame events are received.
pub struct CaptureData {
pub output_idx: usize,
pub info: Mutex<Option<BufferInfo>>,
pub flags: Mutex<Option<Flags>>,
pub buffer: Mutex<Option<Buffer>>,
}
impl CaptureData {
/// Create new capture data for the given output index.
pub fn new(output_idx: usize) -> Self {
Self {
output_idx,
info: Mutex::new(None),
flags: Mutex::new(None),
buffer: Mutex::new(None),
}
} }
} }