294 lines
9.7 KiB
Rust
294 lines
9.7 KiB
Rust
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 chose in the overlay.
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum Choice {
|
|
/// A rectangle in the frame of output `.0`.
|
|
Region(usize, PixelRect),
|
|
/// Space before dragging: capture a window instead.
|
|
Window,
|
|
}
|
|
|
|
/// 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<Choice>>,
|
|
}
|
|
|
|
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.
|
|
/// Space before dragging returns `Choice::Window`.
|
|
pub fn select(wl: &mut Wayland, outputs: &[Output], frames: &[Frame]) -> Result<Choice> {
|
|
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);
|
|
}
|
|
|
|
pub fn switch_to_window(&mut self) {
|
|
if self.selection.allows_window_switch() {
|
|
tracing::info!("switching to window mode");
|
|
self.outcome.get_or_insert(Some(Choice::Window));
|
|
}
|
|
}
|
|
|
|
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(Choice::Region(a, rect)));
|
|
}
|
|
// A click, not a drag: clear it and keep waiting.
|
|
None => self.redraw(a, qh),
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|