diff --git a/src/main.rs b/src/main.rs index 50fc543..ec502a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,3 @@ -// Modules land before the CLI uses them; Task 11 removes this. -#![allow(dead_code)] - mod capture; mod config; mod detach; @@ -11,6 +8,7 @@ mod lock; mod log; mod niri; mod output; +mod region; mod render; mod selection; mod wayland; @@ -48,6 +46,11 @@ enum Command { #[command(flatten)] common: Common, }, + /// Drag to select a region (Cmd+Shift+4). + Region { + #[command(flatten)] + common: Common, + }, /// Report what the compositor and system support. Doctor, } @@ -76,6 +79,7 @@ fn parse_delay(s: &str) -> Result { enum Mode { Screen { all: bool }, + Region, } fn main() -> ExitCode { @@ -106,6 +110,7 @@ fn run(cli: Cli) -> Result<()> { match cli.command { Command::Doctor => doctor::run(), Command::Screen { all, common } => capture(Mode::Screen { all }, common), + Command::Region { common } => capture(Mode::Region, common), } } @@ -139,6 +144,11 @@ fn capture(mode: Mode, common: Common) -> Result<()> { (vec![(frame.to_rgba(frame.full()), None)], 0) } } + Mode::Region => { + let frames = wl.capture(&outputs, cursor)?; + let (i, r) = region::select(&mut wl, &outputs, &frames)?; + (vec![(frames[i].to_rgba(r), None)], 0) + } }; drop(wl); @@ -224,10 +234,10 @@ mod tests { fn flag_conflicts() { assert!(parses(&["screen", "--all"])); assert!(parses(&["screen", "-o", "-"])); - assert!(parses(&["screen", "--clipboard-only", "--cursor"])); + assert!(parses(&["region", "--clipboard-only", "--cursor"])); assert!(!parses(&["screen", "--all", "-o", "-"])); assert!(!parses(&["screen", "--all", "--clipboard-only"])); - assert!(!parses(&["screen", "--clipboard-only", "-o", "a.png"])); + assert!(!parses(&["region", "--clipboard-only", "-o", "a.png"])); } #[test] diff --git a/src/region.rs b/src/region.rs new file mode 100644 index 0000000..97f950f --- /dev/null +++ b/src/region.rs @@ -0,0 +1,279 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use smithay_client_toolkit::{ + compositor::FrameCallbackData, + seat::pointer::{BTN_LEFT, BTN_RIGHT, PointerEvent, PointerEventKind}, + shell::{ + WaylandSurface, + wlr_layer::{Anchor, KeyboardInteractivity, Layer, LayerSurface}, + }, + shm::{ + Shm, + slot::{Buffer, SlotPool}, + }, +}; +use wayland_client::{ + QueueHandle, + protocol::{wl_shm, wl_surface::WlSurface}, +}; +use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{ + Shape, WpCursorShapeDeviceV1, +}; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; + +use crate::error::{Cancelled, HintExt}; +use crate::frame::{Bgrx, Frame, OutputGeom, PixelRect}; +use crate::render; +use crate::selection::{self, Point, Selection}; +use crate::wayland::{Output, State, Wayland}; + +pub const NAMESPACE: &str = "valw-overlay"; + +/// What the user picked: an output index and a rectangle in its frame. +pub type Picked = (usize, PixelRect); + +/// One full-screen layer surface per output, showing the frozen frame. +pub struct Overlay { + surfaces: Vec, + selection: Selection, + /// Output where the current drag started; the capture is clipped to it. + anchor: Option, + outcome: Option>, +} + +struct Surface { + layer: LayerSurface, + viewport: WpViewport, + geom: OutputGeom, + width: u32, + height: u32, + dark: Vec, + bright: Arc, + pool: SlotPool, + buffer: Option, + configured: bool, + frame_pending: bool, + dirty: bool, +} + +/// Shows the overlay on every output and waits for a selection. +/// Returns `Cancelled` if the user presses Esc or right-clicks. +pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result { + let qh = wl.queue.handle(); + let s = &wl.state; + let layer_shell = s + .layer_shell + .as_ref() + .context("compositor does not support wlr-layer-shell") + .hint("run `valw doctor` to see what the compositor supports")?; + let viewporter = s + .viewporter + .as_ref() + .context("compositor does not support wp-viewporter") + .hint("run `valw doctor` to see what the compositor supports")?; + + let mut surfaces = Vec::new(); + for (output, frame) in outputs.iter().zip(frames) { + let surface = s.compositor.create_surface(&qh); + let layer = layer_shell.create_layer_surface( + &qh, + surface, + Layer::Overlay, + Some(NAMESPACE), + Some(&output.wl), + ); + layer.set_anchor(Anchor::all()); + layer.set_exclusive_zone(-1); + layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive); + layer.commit(); + let viewport = viewporter.get_viewport(layer.wl_surface(), &qh, ()); + surfaces.push(Surface::new(layer, viewport, frame, &s.shm)?); + } + + tracing::debug!( + "overlay buffers ready {:?} after start", + crate::log::since_start() + ); + wl.state.overlay = Some(Overlay { + surfaces, + selection: Selection::default(), + anchor: None, + outcome: None, + }); + let result = wl.dispatch_blocking(|s| s.overlay.as_ref().is_some_and(|o| o.outcome.is_some())); + let outcome = wl.state.overlay.take().and_then(|o| o.outcome); + // Dropping the overlay destroys the surfaces; make that visible right away. + let _ = wl.conn.flush(); + result?; + outcome.flatten().ok_or_else(|| Cancelled.into()) +} + +impl Surface { + fn new(layer: LayerSurface, viewport: WpViewport, frame: &Frame, shm: &Shm) -> Result { + let (width, height) = frame.pixels.dimensions(); + let len = (width * height * 4) as usize; + Ok(Surface { + layer, + viewport, + geom: frame.output.clone(), + width, + height, + dark: render::dim(frame.pixels.as_raw(), render::DIM), + bright: Arc::clone(&frame.pixels), + // Room for two buffers so one can be on screen while we draw the next. + pool: SlotPool::new(len * 2, shm).context("could not create shm pool")?, + buffer: None, + configured: false, + frame_pending: false, + dirty: true, + }) + } + + fn draw(&mut self, sel: Option, qh: &QueueHandle) { + let stride = self.width as i32 * 4; + let Ok((buffer, canvas)) = self.pool.create_buffer( + self.width as i32, + self.height as i32, + stride, + wl_shm::Format::Xrgb8888, + ) else { + tracing::warn!("could not allocate an overlay buffer"); + return; + }; + render::compose(canvas, &self.dark, self.bright.as_raw(), self.width, sel); + + let surface = self.layer.wl_surface(); + surface.damage_buffer(0, 0, self.width as i32, self.height as i32); + surface.frame(qh, FrameCallbackData(surface.clone())); + if buffer.attach_to(surface).is_err() { + tracing::warn!("could not attach the overlay buffer"); + return; + } + self.layer.commit(); + self.buffer = Some(buffer); + self.frame_pending = true; + self.dirty = false; + } +} + +impl Overlay { + pub fn cancel(&mut self) { + self.outcome.get_or_insert(None); + } + + fn index_of(&self, surface: &WlSurface) -> Option { + self.surfaces + .iter() + .position(|s| s.layer.wl_surface() == surface) + } + + /// The selection as drawn on surface `i`: only the anchor output shows it. + fn selection_on(&self, i: usize) -> Option { + let Selection::Dragging { start, end } = self.selection else { + return None; + }; + let s = &self.surfaces[i]; + (self.anchor == Some(i)) + .then(|| selection::clip(start, end, &s.geom, s.width, s.height)) + .flatten() + } + + fn redraw(&mut self, i: usize, qh: &QueueHandle) { + let sel = self.selection_on(i); + let s = &mut self.surfaces[i]; + s.dirty = true; + if s.configured && !s.frame_pending { + s.draw(sel, qh); + } + } + + pub fn configure(&mut self, layer: &LayerSurface, (w, h): (u32, u32), qh: &QueueHandle) { + let Some(i) = self.index_of(layer.wl_surface()) else { + return; + }; + let s = &mut self.surfaces[i]; + // Show the physical-size buffer 1:1 on the logical-size surface. + s.viewport.set_destination(w as i32, h as i32); + if !s.configured { + s.configured = true; + let name = s.geom.name.clone(); + self.redraw(i, qh); + tracing::info!( + "overlay on {name} committed {:?} after start", + crate::log::since_start() + ); + } + } + + pub fn frame_done(&mut self, surface: &WlSurface, qh: &QueueHandle) { + let Some(i) = self.index_of(surface) else { + return; + }; + self.surfaces[i].frame_pending = false; + if self.surfaces[i].dirty { + self.redraw(i, qh); + } + } + + pub fn pointer( + &mut self, + events: &[PointerEvent], + cursor: Option<&WpCursorShapeDeviceV1>, + qh: &QueueHandle, + ) { + for event in events { + let Some(i) = self.index_of(&event.surface) else { + continue; + }; + let g = &self.surfaces[i].geom; + let p = Point { + x: g.x as f64 + event.position.0, + y: g.y as f64 + event.position.1, + }; + match event.kind { + PointerEventKind::Enter { serial } => { + if let Some(cursor) = cursor { + cursor.set_shape(serial, Shape::Crosshair); + } + } + PointerEventKind::Press { + button: BTN_LEFT, .. + } => { + self.selection.press(p); + self.anchor = Some(i); + } + PointerEventKind::Press { + button: BTN_RIGHT, .. + } => self.cancel(), + PointerEventKind::Motion { .. } => { + self.selection.motion(p); + if let Some(a) = self.anchor { + self.redraw(a, qh); + } + } + PointerEventKind::Release { + button: BTN_LEFT, .. + } => { + let (Some((start, end)), Some(a)) = (self.selection.release(p), self.anchor) + else { + continue; + }; + let s = &self.surfaces[a]; + match selection::resolve(start, end, &s.geom, s.width, s.height) { + Some(rect) => { + tracing::info!( + "selected {rect:?} on {} (logical {start:?} to {end:?})", + s.geom.name + ); + self.outcome = Some(Some((a, rect))); + } + // A click, not a drag: clear it and keep waiting. + None => self.redraw(a, qh), + } + } + _ => {} + } + } + } +} diff --git a/src/wayland.rs b/src/wayland.rs index 1625327..ba2ad79 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -30,6 +30,7 @@ use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::Z use crate::capture::Pending; use crate::error::HintExt; use crate::frame::OutputGeom; +use crate::region::Overlay; /// An output as valw sees it. #[derive(Debug, Clone)] @@ -61,6 +62,8 @@ pub struct State { pub pointer: Option, /// Screencopy requests in flight. pub captures: Vec, + /// The region selection overlay, while it is open. + pub overlay: Option, } impl Wayland { @@ -87,6 +90,7 @@ impl Wayland { keyboard: None, pointer: None, captures: Vec::new(), + overlay: None, }; // Two round trips: one for wl_output, one for the xdg-output details. queue @@ -233,7 +237,17 @@ impl CompositorHandler for State { ) { } - fn frame(&mut self, _: &Connection, _: &QueueHandle, _: &wl_surface::WlSurface, _: u32) {} + fn frame( + &mut self, + _: &Connection, + qh: &QueueHandle, + surface: &wl_surface::WlSurface, + _: u32, + ) { + if let Some(overlay) = &mut self.overlay { + overlay.frame_done(surface, qh); + } + } fn surface_enter( &mut self, @@ -267,16 +281,23 @@ impl OutputHandler for State { } impl LayerShellHandler for State { - fn closed(&mut self, _: &Connection, _: &QueueHandle, _: &LayerSurface) {} + fn closed(&mut self, _: &Connection, _: &QueueHandle, _: &LayerSurface) { + if let Some(overlay) = &mut self.overlay { + overlay.cancel(); + } + } fn configure( &mut self, _: &Connection, - _: &QueueHandle, - _: &LayerSurface, - _: LayerSurfaceConfigure, + qh: &QueueHandle, + layer: &LayerSurface, + configure: LayerSurfaceConfigure, _: u32, ) { + if let Some(overlay) = &mut self.overlay { + overlay.configure(layer, configure.new_size, qh); + } } } @@ -363,8 +384,13 @@ impl KeyboardHandler for State { _: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, - _: KeyEvent, + event: KeyEvent, ) { + if event.keysym == Keysym::Escape + && let Some(overlay) = &mut self.overlay + { + overlay.cancel(); + } } fn repeat_key( @@ -404,10 +430,13 @@ impl PointerHandler for State { fn pointer_frame( &mut self, _: &Connection, - _: &QueueHandle, + qh: &QueueHandle, _: &wl_pointer::WlPointer, - _: &[PointerEvent], + events: &[PointerEvent], ) { + if let Some(overlay) = &mut self.overlay { + overlay.pointer(events, self.cursor_device.as_ref(), qh); + } } }