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 capture;
|
||||||
mod config;
|
mod config;
|
||||||
mod detach;
|
mod detach;
|
||||||
@@ -11,6 +8,7 @@ mod lock;
|
|||||||
mod log;
|
mod log;
|
||||||
mod niri;
|
mod niri;
|
||||||
mod output;
|
mod output;
|
||||||
|
mod region;
|
||||||
mod render;
|
mod render;
|
||||||
mod selection;
|
mod selection;
|
||||||
mod wayland;
|
mod wayland;
|
||||||
@@ -48,6 +46,11 @@ enum Command {
|
|||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
common: Common,
|
common: Common,
|
||||||
},
|
},
|
||||||
|
/// Drag to select a region (Cmd+Shift+4).
|
||||||
|
Region {
|
||||||
|
#[command(flatten)]
|
||||||
|
common: Common,
|
||||||
|
},
|
||||||
/// Report what the compositor and system support.
|
/// Report what the compositor and system support.
|
||||||
Doctor,
|
Doctor,
|
||||||
}
|
}
|
||||||
@@ -76,6 +79,7 @@ fn parse_delay(s: &str) -> Result<Duration, String> {
|
|||||||
|
|
||||||
enum Mode {
|
enum Mode {
|
||||||
Screen { all: bool },
|
Screen { all: bool },
|
||||||
|
Region,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -106,6 +110,7 @@ fn run(cli: Cli) -> Result<()> {
|
|||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Doctor => doctor::run(),
|
Command::Doctor => doctor::run(),
|
||||||
Command::Screen { all, common } => capture(Mode::Screen { all }, common),
|
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)
|
(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);
|
drop(wl);
|
||||||
|
|
||||||
@@ -224,10 +234,10 @@ mod tests {
|
|||||||
fn flag_conflicts() {
|
fn flag_conflicts() {
|
||||||
assert!(parses(&["screen", "--all"]));
|
assert!(parses(&["screen", "--all"]));
|
||||||
assert!(parses(&["screen", "-o", "-"]));
|
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", "-o", "-"]));
|
||||||
assert!(!parses(&["screen", "--all", "--clipboard-only"]));
|
assert!(!parses(&["screen", "--all", "--clipboard-only"]));
|
||||||
assert!(!parses(&["screen", "--clipboard-only", "-o", "a.png"]));
|
assert!(!parses(&["region", "--clipboard-only", "-o", "a.png"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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::capture::Pending;
|
||||||
use crate::error::HintExt;
|
use crate::error::HintExt;
|
||||||
use crate::frame::OutputGeom;
|
use crate::frame::OutputGeom;
|
||||||
|
use crate::region::Overlay;
|
||||||
|
|
||||||
/// An output as valw sees it.
|
/// An output as valw sees it.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -61,6 +62,8 @@ pub struct State {
|
|||||||
pub pointer: Option<wl_pointer::WlPointer>,
|
pub pointer: Option<wl_pointer::WlPointer>,
|
||||||
/// Screencopy requests in flight.
|
/// Screencopy requests in flight.
|
||||||
pub captures: Vec<Pending>,
|
pub captures: Vec<Pending>,
|
||||||
|
/// The region selection overlay, while it is open.
|
||||||
|
pub overlay: Option<Overlay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wayland {
|
impl Wayland {
|
||||||
@@ -87,6 +90,7 @@ impl Wayland {
|
|||||||
keyboard: None,
|
keyboard: None,
|
||||||
pointer: None,
|
pointer: None,
|
||||||
captures: Vec::new(),
|
captures: Vec::new(),
|
||||||
|
overlay: None,
|
||||||
};
|
};
|
||||||
// Two round trips: one for wl_output, one for the xdg-output details.
|
// Two round trips: one for wl_output, one for the xdg-output details.
|
||||||
queue
|
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(
|
fn surface_enter(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -267,16 +281,23 @@ impl OutputHandler for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LayerShellHandler 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(
|
fn configure(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Connection,
|
_: &Connection,
|
||||||
_: &QueueHandle<Self>,
|
qh: &QueueHandle<Self>,
|
||||||
_: &LayerSurface,
|
layer: &LayerSurface,
|
||||||
_: LayerSurfaceConfigure,
|
configure: LayerSurfaceConfigure,
|
||||||
_: u32,
|
_: 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>,
|
_: &QueueHandle<Self>,
|
||||||
_: &wl_keyboard::WlKeyboard,
|
_: &wl_keyboard::WlKeyboard,
|
||||||
_: u32,
|
_: u32,
|
||||||
_: KeyEvent,
|
event: KeyEvent,
|
||||||
) {
|
) {
|
||||||
|
if event.keysym == Keysym::Escape
|
||||||
|
&& let Some(overlay) = &mut self.overlay
|
||||||
|
{
|
||||||
|
overlay.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repeat_key(
|
fn repeat_key(
|
||||||
@@ -404,10 +430,13 @@ impl PointerHandler for State {
|
|||||||
fn pointer_frame(
|
fn pointer_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Connection,
|
_: &Connection,
|
||||||
_: &QueueHandle<Self>,
|
qh: &QueueHandle<Self>,
|
||||||
_: &wl_pointer::WlPointer,
|
_: &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