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
+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),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user