From c660001a6e3ceefb72d9594aae63cfaad78fa4f1 Mon Sep 17 00:00:00 2001 From: elwrcl Date: Fri, 2 Oct 2026 14:57:12 +0300 Subject: [PATCH] feat: drag the preview out --- src/host.rs | 165 ++++++++++++++++++++++++++++++++++---------- src/main.rs | 3 - src/thumbnail.rs | 176 ++++++++++++++++++++++++++++++++++++++++------- src/wayland.rs | 107 +++++++++++++++++++++++++++- 4 files changed, 385 insertions(+), 66 deletions(-) diff --git a/src/host.rs b/src/host.rs index 55bc62f..7a0bd7e 100644 --- a/src/host.rs +++ b/src/host.rs @@ -5,9 +5,12 @@ use std::collections::BTreeSet; use std::io::{ErrorKind, Read, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Duration, Instant}; use anyhow::{Context, Result}; +use smithay_client_toolkit::data_device_manager::{WritePipe, data_source::DragSource}; use smithay_client_toolkit::reexports::calloop::{ EventLoop, Interest, LoopHandle, Mode, PostAction, generic::Generic, @@ -16,15 +19,16 @@ use smithay_client_toolkit::reexports::calloop::{ use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource; use smithay_client_toolkit::seat::pointer::{BTN_LEFT, PointerEvent, PointerEventKind}; use smithay_client_toolkit::shell::wlr_layer::LayerSurface; +use wayland_client::protocol::{wl_data_device_manager::DndAction, wl_data_source::WlDataSource}; use wayland_client::{Connection, QueueHandle, protocol::wl_surface::WlSurface}; -use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{ - Shape, WpCursorShapeDeviceV1, -}; +use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use crate::dnd::{self, Outcome}; use crate::ipc::{self, Reply, Request}; use crate::lock::Lock; use crate::stack; -use crate::thumbnail::{Action, Parts, Thumbnail}; +use crate::thumbnail::{Action, DragStart, Parts, Thumbnail}; use crate::wayland::{State, Wayland}; /// A host nobody connects to within this time gives up. @@ -89,6 +93,19 @@ pub struct Host { conn: Connection, handle: LoopHandle<'static, State>, qh: QueueHandle, + drag: Option, + /// Threads still writing dropped data; the host waits for them, or the + /// receiving app would get a cut-off file. + writers: Arc, + warned_no_dnd: bool, +} + +/// The drag-out in progress. Dropping the source cancels the drag. +struct Drag { + thumb: u64, + source: DragSource, + icon: WlSurface, + icon_viewport: WpViewport, } /// Runs the host until its last thumbnail closes. A second host exits @@ -128,6 +145,9 @@ pub fn run() -> Result<()> { conn, handle: handle.clone(), qh, + drag: None, + writers: Arc::new(AtomicUsize::new(0)), + warned_no_dnd: false, }); let mut next_client = 0u64; @@ -152,7 +172,8 @@ pub fn run() -> Result<()> { .run(Duration::from_millis(500), &mut state, |state| { let host = state.preview.as_mut().expect("host state"); host.tidy(); - if host.core.should_exit(host.thumbs.len(), Instant::now()) { + let busy = host.thumbs.len() + host.writers.load(Ordering::SeqCst); + if host.core.should_exit(busy, Instant::now()) { signal.stop(); } }) @@ -253,38 +274,40 @@ impl Host { self.thumbs.retain(|t| !t.is_layer(layer)); } - pub fn pointer( - &mut self, - events: &[PointerEvent], - cursor: Option<&WpCursorShapeDeviceV1>, - qh: &QueueHandle, - ) { - for event in events { - let Some(t) = self.thumb(&event.surface) else { - continue; - }; - let (x, y) = event.position; - match event.kind { - PointerEventKind::Enter { serial } => { - if let Some(cursor) = cursor { - cursor.set_shape(serial, Shape::Pointer); - } - } - PointerEventKind::Press { - button: BTN_LEFT, .. - } => t.press(x, y), - PointerEventKind::Motion { .. } => t.motion(x, qh), - PointerEventKind::Release { - button: BTN_LEFT, .. - } => { - let action = t.release(x, y, qh); - if action == Some(Action::OpenEditor) { - open_editor(&t.path); - } - } - _ => {} + /// Writes the dragged screenshot for the receiving app, on its own + /// thread: a large PNG fills the pipe, and blocking here would freeze + /// every thumbnail. + pub fn send(&self, source: &WlDataSource, mime: String, fd: WritePipe) { + let Some(drag) = self.drag.as_ref().filter(|d| d.source.inner() == source) else { + return; + }; + let Some(thumb) = self.thumbs.iter().find(|t| t.id == drag.thumb) else { + return; + }; + let path = thumb.path.clone(); + let writers = Arc::clone(&self.writers); + writers.fetch_add(1, Ordering::SeqCst); + std::thread::spawn(move || { + let mut fd = fd; + if let Err(e) = dnd::write_offer(&mime, &path, &mut fd) { + tracing::warn!("could not send the screenshot as {mime}: {e:#}"); } + drop(fd); + writers.fetch_sub(1, Ordering::SeqCst); + }); + } + + pub fn drag_ended(&mut self, source: &WlDataSource, outcome: Outcome, qh: &QueueHandle) { + let Some(drag) = self.drag.take_if(|d| d.source.inner() == source) else { + return; + }; + tracing::info!("drag-out {outcome:?}"); + drag.icon_viewport.destroy(); + drag.icon.destroy(); + if let Some(t) = self.thumbs.iter_mut().find(|t| t.id == drag.thumb) { + t.end_drag(outcome, qh); } + let _ = self.conn.flush(); } fn expire(&mut self, id: u64) { @@ -306,6 +329,78 @@ impl Host { } } +/// Pointer events on thumbnails. A free function: starting a drag needs +/// more of `State` than the host. +pub fn pointer(state: &mut State, events: &[PointerEvent], qh: &QueueHandle) { + for event in events { + let Some(host) = state.preview.as_mut() else { + return; + }; + let Some(i) = host.thumbs.iter().position(|t| t.is(&event.surface)) else { + continue; + }; + let (x, y) = event.position; + match event.kind { + PointerEventKind::Enter { serial } => { + if let Some(cursor) = &state.cursor_device { + cursor.set_shape(serial, Shape::Pointer); + } + } + PointerEventKind::Press { + button: BTN_LEFT, + serial, + .. + } => host.thumbs[i].press(x, y, serial), + PointerEventKind::Motion { .. } => { + let start = host.thumbs[i].motion(x, y, qh); + if let Some(start) = start { + start_drag(state, i, start, qh); + } + } + PointerEventKind::Release { + button: BTN_LEFT, .. + } => { + let t = &mut host.thumbs[i]; + if t.release(x, y, qh) == Some(Action::OpenEditor) { + open_editor(&t.path); + } + } + _ => {} + } + } +} + +fn start_drag(state: &mut State, i: usize, start: DragStart, qh: &QueueHandle) { + let host = state.preview.as_mut().expect("host state"); + let (Some(manager), Some(device), Some(viewporter)) = + (&state.data_devices, &state.data_device, &state.viewporter) + else { + if !host.warned_no_dnd { + host.warned_no_dnd = true; + tracing::warn!("the compositor has no wl_data_device_manager; drag-out is off"); + } + return; + }; + if host.drag.is_some() { + return; + } + let source = manager.create_drag_and_drop_source(qh, dnd::MIME_TYPES, DndAction::Copy); + let icon = state.compositor.create_surface(qh); + let icon_viewport = viewporter.get_viewport(&icon, qh, ()); + let thumb = &mut host.thumbs[i]; + source.start_drag(device, thumb.surface(), Some(&icon), start.serial); + thumb.draw_icon(&icon, &icon_viewport, start.grab); + thumb.begin_drag(qh); + tracing::info!("drag-out started for {}", thumb.path.display()); + host.drag = Some(Drag { + thumb: thumb.id, + source, + icon, + icon_viewport, + }); + let _ = host.conn.flush(); +} + fn disconnect(state: &mut State, id: u64) { let Some(host) = &mut state.preview else { return; diff --git a/src/main.rs b/src/main.rs index 59fa3ba..e7f514a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,3 @@ -// Drag-out modules land before the host uses them; Task 3 removes this. -#![allow(dead_code)] - mod capture; mod config; mod detach; diff --git a/src/thumbnail.rs b/src/thumbnail.rs index eed9eca..332e5ea 100644 --- a/src/thumbnail.rs +++ b/src/thumbnail.rs @@ -16,12 +16,16 @@ use smithay_client_toolkit::{ slot::{Buffer, SlotPool}, }, }; -use wayland_client::{QueueHandle, protocol::wl_shm}; +use wayland_client::{ + Proxy, QueueHandle, + protocol::{wl_shm, wl_surface::WlSurface}, +}; use wayland_protocols::wp::viewporter::client::{ wp_viewport::WpViewport, wp_viewporter::WpViewporter, }; -use crate::stack::{self, Anim, EDGE_MARGIN, Release}; +use crate::dnd::{self, AfterDrag, Outcome}; +use crate::stack::{self, Anim, EDGE_MARGIN, Gesture, Release}; use crate::wayland::{Output, State}; pub const NAMESPACE: &str = "valw-preview"; @@ -93,6 +97,24 @@ pub enum Action { OpenEditor, } +/// A press turned into a drag-out: the host starts the drag with these. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct DragStart { + /// Serial of the press; the compositor only accepts a drag for it. + pub serial: u32, + /// Where on the image the press was, so it stays under the pointer. + pub grab: (f64, f64), +} + +/// A press in progress and what it turned into once the pointer moved. +#[derive(Debug, Clone, Copy)] +struct Press { + x: f64, + y: f64, + serial: u32, + gesture: Option, +} + pub struct Thumbnail { pub id: u64, pub output: String, @@ -108,13 +130,17 @@ pub struct Thumbnail { /// How far right of its resting place the image is drawn, logical px. offset: f64, anim: Option<(Anim, Instant)>, - drag: Option<(f64, f64)>, + press: Option, margin: u32, /// Input region while visible (the image) and while hidden (nothing). input: Region, no_input: Region, /// Hidden for a capture: drawn fully transparent, clicks pass through. hidden: bool, + /// Being dragged out: the drag icon shows it, so this one hides too. + /// Separate from `hidden` so a capture ending mid-drag can't bring it back. + dragging: bool, + icon_buffer: Option, configured: bool, frame_pending: bool, started: bool, @@ -178,11 +204,13 @@ impl Thumbnail { scale: output.scale, offset: travel(size.0), anim: None, - drag: None, + press: None, margin: EDGE_MARGIN, input, no_input, hidden: false, + dragging: false, + icon_buffer: None, configured: false, frame_pending: false, started: false, @@ -191,10 +219,14 @@ impl Thumbnail { }) } - pub fn is(&self, surface: &wayland_client::protocol::wl_surface::WlSurface) -> bool { + pub fn is(&self, surface: &WlSurface) -> bool { self.layer.wl_surface() == surface } + pub fn surface(&self) -> &WlSurface { + self.layer.wl_surface() + } + pub fn is_layer(&self, layer: &LayerSurface) -> bool { self.layer.wl_surface() == layer.wl_surface() } @@ -205,7 +237,23 @@ impl Thumbnail { /// The slide-out has finished (or is invisible anyway). pub fn is_finished(&self) -> bool { - self.closing && (self.hidden || anim_over(self.anim, Instant::now())) + self.closing && (!self.visible() || anim_over(self.anim, Instant::now())) + } + + fn visible(&self) -> bool { + !self.hidden && !self.dragging + } + + /// Clicks reach the image only while it is visible. + fn apply_input(&self) { + let region = if self.visible() { + &self.input + } else { + &self.no_input + }; + self.layer + .wl_surface() + .set_input_region(Some(region.wl_region())); } /// Makes the thumbnail invisible and click-through. The surface stays @@ -215,9 +263,7 @@ impl Thumbnail { return; } self.hidden = true; - self.layer - .wl_surface() - .set_input_region(Some(self.no_input.wl_region())); + self.apply_input(); self.draw(qh, true); } @@ -226,9 +272,7 @@ impl Thumbnail { return; } self.hidden = false; - self.layer - .wl_surface() - .set_input_region(Some(self.input.wl_region())); + self.apply_input(); self.draw(qh, true); } @@ -252,15 +296,16 @@ impl Thumbnail { pub fn frame_done(&mut self, qh: &QueueHandle) { self.frame_pending = false; - if self.anim.is_some() || self.drag.is_some() { + if self.anim.is_some() || self.press.is_some() { self.draw(qh, false); } } - /// The timeout fired: slide out, unless the user is dragging it. + /// The timeout fired: slide out, unless the user is holding or + /// dragging it; then it is decided when they let go. pub fn expire(&mut self, qh: &QueueHandle) { self.expired = true; - if self.drag.is_none() { + if self.press.is_none() && !self.dragging { self.slide_out(qh); } } @@ -277,23 +322,46 @@ impl Thumbnail { self.draw(qh, false); } - pub fn press(&mut self, x: f64, y: f64) { - if !self.closing { + pub fn press(&mut self, x: f64, y: f64, serial: u32) { + if !self.closing && !self.dragging { self.anim = None; - self.drag = Some((x, y)); + self.press = Some(Press { + x, + y, + serial, + gesture: None, + }); } } - pub fn motion(&mut self, x: f64, qh: &QueueHandle) { - if let Some((x0, _)) = self.drag { - self.offset = stack::drag_offset(x - x0); + /// Follows a swipe, or returns a `DragStart` the moment the press turns + /// into a drag-out. + pub fn motion(&mut self, x: f64, y: f64, qh: &QueueHandle) -> Option { + let press = self.press.as_mut()?; + let (dx, dy) = (x - press.x, y - press.y); + if press.gesture.is_none() { + press.gesture = stack::classify(dx, dy); + if press.gesture == Some(Gesture::DragOut) { + return Some(DragStart { + serial: press.serial, + grab: (press.x, press.y), + }); + } + } + if press.gesture == Some(Gesture::Swipe) { + self.offset = stack::drag_offset(dx); self.draw(qh, false); } + None } pub fn release(&mut self, x: f64, y: f64, qh: &QueueHandle) -> Option { - let (x0, y0) = self.drag.take()?; - match stack::release(x - x0, y - y0, self.size.0 as f64) { + let press = self.press.take()?; + if press.gesture == Some(Gesture::DragOut) { + // The drag never started (no data device); nothing moved. + return None; + } + match stack::release(x - press.x, y - press.y, self.size.0 as f64) { Release::Click => { self.slide_out(qh); Some(Action::OpenEditor) @@ -314,6 +382,63 @@ impl Thumbnail { } } + /// The drag started: the icon carries the image, this one hides. + pub fn begin_drag(&mut self, qh: &QueueHandle) { + self.press = None; + self.dragging = true; + self.anim = None; + self.offset = 0.0; + self.apply_input(); + self.draw(qh, true); + } + + pub fn end_drag(&mut self, outcome: Outcome, qh: &QueueHandle) { + self.dragging = false; + self.icon_buffer = None; + self.apply_input(); + match dnd::after_drag(outcome, self.expired) { + AfterDrag::Close => { + // Already invisible; nothing to animate. + self.closing = true; + self.anim = None; + } + AfterDrag::ComeBack => { + self.anim = Some((Anim::slide_in(travel(self.size.0)), Instant::now())); + self.draw(qh, true); + } + AfterDrag::SlideOut => self.slide_out(qh), + } + } + + /// Draws the drag icon: the image alone, sharp at the output's scale, + /// placed so the grabbed point stays under the pointer. + pub fn draw_icon(&mut self, icon: &WlSurface, viewport: &WpViewport, grab: (f64, f64)) { + let (width, height) = self.small.dimensions(); + let Ok((buffer, canvas)) = self.pool.create_buffer( + width as i32, + height as i32, + width as i32 * 4, + wl_shm::Format::Argb8888, + ) else { + tracing::warn!("could not allocate a drag icon buffer"); + return; + }; + paint(canvas, width, &self.small, 0); + viewport.set_destination(self.size.0 as i32, self.size.1 as i32); + // wl_surface.offset needs version 5; older compositors put the + // icon's corner at the pointer instead. + if icon.version() >= 5 { + icon.offset(-(grab.0.round() as i32), -(grab.1.round() as i32)); + } + icon.damage_buffer(0, 0, width as i32, height as i32); + if buffer.attach_to(icon).is_err() { + tracing::warn!("could not attach the drag icon"); + return; + } + icon.commit(); + self.icon_buffer = Some(buffer); + } + fn draw(&mut self, qh: &QueueHandle, force: bool) { if let Some((anim, start)) = self.anim { let (offset, done) = anim.at(start.elapsed().as_secs_f64() * 1000.0); @@ -328,6 +453,7 @@ impl Thumbnail { } let width = canvas_width(self.size.0, self.scale); let height = self.small.height(); + let visible = self.visible(); let Ok((buffer, canvas)) = self.pool.create_buffer( width as i32, height as i32, @@ -337,7 +463,7 @@ impl Thumbnail { tracing::warn!("could not allocate a thumbnail buffer"); return; }; - if self.hidden { + if !visible { canvas.fill(0); } else { paint( @@ -351,7 +477,7 @@ impl Thumbnail { surface.damage_buffer(0, 0, width as i32, height as i32); // Keep drawing while something moves; motion that arrives while a // frame is pending is picked up by the next frame callback. - if self.anim.is_some() || self.drag.is_some() { + if self.anim.is_some() || self.press.is_some() { surface.frame(qh, FrameCallbackData(surface.clone())); self.frame_pending = true; } diff --git a/src/wayland.rs b/src/wayland.rs index ac93707..934d022 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -4,6 +4,12 @@ use anyhow::{Context, Result, bail}; use rustix::event::{PollFd, PollFlags, Timespec, poll}; use smithay_client_toolkit::{ compositor::{CompositorHandler, CompositorState}, + data_device_manager::{ + DataDeviceManagerState, WritePipe, + data_device::{DataDevice, DataDeviceHandler}, + data_offer::{DataOfferHandler, DragOffer}, + data_source::DataSourceHandler, + }, delegate_registry, output::{OutputHandler, OutputState}, registry::{ProvidesRegistryState, RegistryState}, @@ -19,7 +25,10 @@ use smithay_client_toolkit::{ use wayland_client::{ Connection, EventQueue, QueueHandle, globals::{GlobalList, registry_queue_init}, - protocol::{wl_buffer::WlBuffer, wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface}, + protocol::{ + wl_buffer::WlBuffer, wl_data_device::WlDataDevice, wl_data_device_manager::DndAction, + wl_data_source::WlDataSource, wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface, + }, }; use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1; use wayland_protocols::wp::viewporter::client::{ @@ -28,6 +37,7 @@ use wayland_protocols::wp::viewporter::client::{ use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; use crate::capture::Pending; +use crate::dnd::Outcome; use crate::error::HintExt; use crate::frame::OutputGeom; use crate::host::Host; @@ -61,6 +71,9 @@ pub struct State { pub viewporter: Option, pub cursor_shape: Option, pub cursor_device: Option, + /// For dragging a preview out; the host never receives drops. + pub data_devices: Option, + pub data_device: Option, pub keyboard: Option, pub pointer: Option, /// Screencopy requests in flight. @@ -92,6 +105,8 @@ impl Wayland { viewporter: globals.bind(&qh, 1..=1, ()).ok(), cursor_shape: CursorShapeManager::bind(&globals, &qh).ok(), cursor_device: None, + data_devices: DataDeviceManagerState::bind(&globals, &qh).ok(), + data_device: None, keyboard: None, pointer: None, captures: Vec::new(), @@ -370,6 +385,10 @@ impl SeatHandler for State { .cursor_shape .as_ref() .map(|m| m.get_shape_device(&pointer, qh)); + self.data_device = self + .data_devices + .as_ref() + .map(|m| m.get_data_device(qh, &seat)); self.pointer = Some(pointer); } } @@ -481,12 +500,94 @@ impl PointerHandler for State { if let Some(overlay) = &mut self.overlay { overlay.pointer(events, self.cursor_device.as_ref(), qh); } - if let Some(preview) = &mut self.preview { - preview.pointer(events, self.cursor_device.as_ref(), qh); + if self.preview.is_some() { + crate::host::pointer(self, events, qh); } } } +impl DataSourceHandler for State { + fn accept_mime( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + _: Option, + ) { + } + + fn send_request( + &mut self, + _: &Connection, + _: &QueueHandle, + source: &WlDataSource, + mime: String, + fd: WritePipe, + ) { + if let Some(preview) = &self.preview { + preview.send(source, mime, fd); + } + } + + fn cancelled(&mut self, _: &Connection, qh: &QueueHandle, source: &WlDataSource) { + if let Some(preview) = &mut self.preview { + preview.drag_ended(source, Outcome::Cancelled, qh); + } + } + + fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) {} + + fn dnd_finished(&mut self, _: &Connection, qh: &QueueHandle, source: &WlDataSource) { + if let Some(preview) = &mut self.preview { + preview.drag_ended(source, Outcome::Dropped, qh); + } + } + + fn action(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource, _: DndAction) {} +} + +// valw only ever starts drags; incoming offers are ignored. +impl DataDeviceHandler for State { + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataDevice, + _: f64, + _: f64, + _: &wl_surface::WlSurface, + ) { + } + + fn leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + + fn motion(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice, _: f64, _: f64) {} + + fn selection(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} + + fn drop_performed(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) {} +} + +impl DataOfferHandler for State { + fn source_actions( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &mut DragOffer, + _: DndAction, + ) { + } + + fn selected_action( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &mut DragOffer, + _: DndAction, + ) { + } +} + impl ShmHandler for State { fn shm_state(&mut self) -> &mut Shm { &mut self.shm