feat: frozen-frame region selection overlay
This commit is contained in:
1 parent
2512fdd430
commit
4a89b77179
3 files changed
+331
-13
No files matched your search
+15
-5
@@ -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<Duration, String> {
|
||||
|
||||
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]
|
||||
|
||||
+279
@@ -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<Surface>,
|
||||
selection: Selection,
|
||||
/// Output where the current drag started; the capture is clipped to it.
|
||||
anchor: Option<usize>,
|
||||
outcome: Option<Option<Picked>>,
|
||||
}
|
||||
|
||||
struct Surface {
|
||||
layer: LayerSurface,
|
||||
viewport: WpViewport,
|
||||
geom: OutputGeom,
|
||||
width: u32,
|
||||
height: u32,
|
||||
dark: Vec<u8>,
|
||||
bright: Arc<Bgrx>,
|
||||
pool: SlotPool,
|
||||
buffer: Option<Buffer>,
|
||||
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<Picked> {
|
||||
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<Surface> {
|
||||
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<PixelRect>, qh: &QueueHandle<State>) {
|
||||
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<usize> {
|
||||
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<PixelRect> {
|
||||
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<State>) {
|
||||
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<State>) {
|
||||
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<State>) {
|
||||
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<State>,
|
||||
) {
|
||||
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),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-8
@@ -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<wl_pointer::WlPointer>,
|
||||
/// Screencopy requests in flight.
|
||||
pub captures: Vec<Pending>,
|
||||
/// The region selection overlay, while it is open.
|
||||
pub overlay: Option<Overlay>,
|
||||
}
|
||||
|
||||
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<Self>, _: &wl_surface::WlSurface, _: u32) {}
|
||||
fn frame(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
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<Self>, _: &LayerSurface) {}
|
||||
fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
|
||||
if let Some(overlay) = &mut self.overlay {
|
||||
overlay.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
fn configure(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &LayerSurface,
|
||||
_: LayerSurfaceConfigure,
|
||||
qh: &QueueHandle<Self>,
|
||||
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<Self>,
|
||||
_: &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<Self>,
|
||||
qh: &QueueHandle<Self>,
|
||||
_: &wl_pointer::WlPointer,
|
||||
_: &[PointerEvent],
|
||||
events: &[PointerEvent],
|
||||
) {
|
||||
if let Some(overlay) = &mut self.overlay {
|
||||
overlay.pointer(events, self.cursor_device.as_ref(), qh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user