feat: dnd sources
This commit is contained in:
parent
90dc61bb57
commit
cc9ab6de69
6 changed files with 360 additions and 65 deletions
|
|
@ -8,15 +8,18 @@ use std::str::{FromStr, Utf8Error};
|
||||||
|
|
||||||
use sctk::compositor::{CompositorHandler, CompositorState};
|
use sctk::compositor::{CompositorHandler, CompositorState};
|
||||||
use sctk::output::{OutputHandler, OutputState};
|
use sctk::output::{OutputHandler, OutputState};
|
||||||
use sctk::reexports::calloop::{EventLoop, LoopHandle};
|
use sctk::reexports::calloop::{self, EventLoop, LoopHandle};
|
||||||
use sctk::reexports::calloop_wayland_source::WaylandSource;
|
use sctk::reexports::calloop_wayland_source::WaylandSource;
|
||||||
use sctk::reexports::client::globals::registry_queue_init;
|
use sctk::reexports::client::globals::registry_queue_init;
|
||||||
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
|
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
|
||||||
use sctk::reexports::client::protocol::wl_surface::WlSurface;
|
use sctk::reexports::client::protocol::wl_surface::WlSurface;
|
||||||
use sctk::reexports::client::protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface};
|
use sctk::reexports::client::protocol::{
|
||||||
|
wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface,
|
||||||
|
};
|
||||||
use sctk::reexports::client::{Connection, Proxy, QueueHandle};
|
use sctk::reexports::client::{Connection, Proxy, QueueHandle};
|
||||||
use sctk::registry::{ProvidesRegistryState, RegistryState};
|
use sctk::registry::{ProvidesRegistryState, RegistryState};
|
||||||
use sctk::seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers};
|
use sctk::seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers};
|
||||||
|
use sctk::seat::pointer::{PointerEventKind, PointerHandler, BTN_LEFT, BTN_RIGHT};
|
||||||
use sctk::seat::{Capability, SeatHandler, SeatState};
|
use sctk::seat::{Capability, SeatHandler, SeatState};
|
||||||
use sctk::shell::xdg::window::{Window, WindowConfigure, WindowDecorations, WindowHandler};
|
use sctk::shell::xdg::window::{Window, WindowConfigure, WindowDecorations, WindowHandler};
|
||||||
use sctk::shell::xdg::XdgShell;
|
use sctk::shell::xdg::XdgShell;
|
||||||
|
|
@ -24,10 +27,10 @@ use sctk::shell::WaylandSurface;
|
||||||
use sctk::shm::slot::{Buffer, SlotPool};
|
use sctk::shm::slot::{Buffer, SlotPool};
|
||||||
use sctk::shm::{Shm, ShmHandler};
|
use sctk::shm::{Shm, ShmHandler};
|
||||||
use sctk::{
|
use sctk::{
|
||||||
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
|
delegate_compositor, delegate_keyboard, delegate_output, delegate_pointer, delegate_registry,
|
||||||
delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers,
|
delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers,
|
||||||
};
|
};
|
||||||
use smithay_clipboard::dnd::{DndDestinationRectangle, Rectangle};
|
use smithay_clipboard::dnd::{DndDestinationRectangle, OfferEvent, Rectangle, SourceEvent};
|
||||||
use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType, ALLOWED_TEXT_MIME_TYPES};
|
use smithay_clipboard::mime::{AllowedMimeTypes, AsMimeTypes, MimeType, ALLOWED_TEXT_MIME_TYPES};
|
||||||
use smithay_clipboard::{Clipboard, SimpleClipboard};
|
use smithay_clipboard::{Clipboard, SimpleClipboard};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
@ -70,8 +73,38 @@ fn main() {
|
||||||
let (tx, rx) = sctk::reexports::calloop::channel::sync_channel(10);
|
let (tx, rx) = sctk::reexports::calloop::channel::sync_channel(10);
|
||||||
clipboard.init_dnd(Box::new(tx)).expect("Failed to set up DnD");
|
clipboard.init_dnd(Box::new(tx)).expect("Failed to set up DnD");
|
||||||
|
|
||||||
_ = event_loop.handle().insert_source(rx, |event, _, _state| {
|
_ = event_loop.handle().insert_source(rx, |event, _, state| {
|
||||||
dbg!(event);
|
let calloop::channel::Event::Msg(event) = event else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match event {
|
||||||
|
smithay_clipboard::dnd::DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) => {
|
||||||
|
let s = smithay_clipboard::text::Text::try_from((data, mime_type)).unwrap();
|
||||||
|
println!("Received DnD data for {}: {}", id.unwrap_or_default(), s.0);
|
||||||
|
},
|
||||||
|
smithay_clipboard::dnd::DndEvent::Offer(id, OfferEvent::Leave) => {
|
||||||
|
if state.internal_dnd {
|
||||||
|
if state.pointer_focus {
|
||||||
|
println!("Internal drop completed!");
|
||||||
|
} else {
|
||||||
|
// Internal DnD will be ignored after leaving the window in which it
|
||||||
|
// started. Another approach might be to allow it to
|
||||||
|
// re-enter before some time has passed.
|
||||||
|
state.internal_dnd = false;
|
||||||
|
state.clipboard.end_dnd();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Dnd offer left {id:?}.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
smithay_clipboard::dnd::DndEvent::Source(SourceEvent::Finished) => {
|
||||||
|
println!("Finished sending data.");
|
||||||
|
state.internal_dnd = false;
|
||||||
|
},
|
||||||
|
e => {
|
||||||
|
dbg!(e);
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
clipboard.register_dnd_destination(window.wl_surface().clone(), vec![
|
clipboard.register_dnd_destination(window.wl_surface().clone(), vec![
|
||||||
|
|
@ -85,6 +118,36 @@ fn main() {
|
||||||
actions: DndAction::all(),
|
actions: DndAction::all(),
|
||||||
preferred: DndAction::Copy,
|
preferred: DndAction::Copy,
|
||||||
},
|
},
|
||||||
|
DndDestinationRectangle {
|
||||||
|
id: 1,
|
||||||
|
rectangle: Rectangle { x: 256., y: 0., width: 256., height: 256. },
|
||||||
|
mime_types: ALLOWED_TEXT_MIME_TYPES
|
||||||
|
.iter()
|
||||||
|
.map(|m| MimeType::from(Cow::from(m.to_string())))
|
||||||
|
.collect(),
|
||||||
|
actions: DndAction::all(),
|
||||||
|
preferred: DndAction::Copy,
|
||||||
|
},
|
||||||
|
DndDestinationRectangle {
|
||||||
|
id: 2,
|
||||||
|
rectangle: Rectangle { x: 0., y: 256., width: 256., height: 256. },
|
||||||
|
mime_types: ALLOWED_TEXT_MIME_TYPES
|
||||||
|
.iter()
|
||||||
|
.map(|m| MimeType::from(Cow::from(m.to_string())))
|
||||||
|
.collect(),
|
||||||
|
actions: DndAction::Copy,
|
||||||
|
preferred: DndAction::Copy,
|
||||||
|
},
|
||||||
|
DndDestinationRectangle {
|
||||||
|
id: 3,
|
||||||
|
rectangle: Rectangle { x: 256., y: 256., width: 256., height: 256. },
|
||||||
|
mime_types: ALLOWED_TEXT_MIME_TYPES
|
||||||
|
.iter()
|
||||||
|
.map(|m| MimeType::from(Cow::from(m.to_string())))
|
||||||
|
.collect(),
|
||||||
|
actions: DndAction::Move,
|
||||||
|
preferred: DndAction::Move,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let mut simple_window = SimpleWindow {
|
let mut simple_window = SimpleWindow {
|
||||||
|
|
@ -102,7 +165,10 @@ fn main() {
|
||||||
buffer: None,
|
buffer: None,
|
||||||
window,
|
window,
|
||||||
keyboard: None,
|
keyboard: None,
|
||||||
|
pointer: None,
|
||||||
|
internal_dnd: false,
|
||||||
keyboard_focus: false,
|
keyboard_focus: false,
|
||||||
|
pointer_focus: false,
|
||||||
loop_handle: event_loop.handle(),
|
loop_handle: event_loop.handle(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -131,7 +197,10 @@ struct SimpleWindow {
|
||||||
buffer: Option<Buffer>,
|
buffer: Option<Buffer>,
|
||||||
window: Window,
|
window: Window,
|
||||||
keyboard: Option<wl_keyboard::WlKeyboard>,
|
keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||||
|
pointer: Option<wl_pointer::WlPointer>,
|
||||||
|
internal_dnd: bool,
|
||||||
keyboard_focus: bool,
|
keyboard_focus: bool,
|
||||||
|
pointer_focus: bool,
|
||||||
loop_handle: LoopHandle<'static, SimpleWindow>,
|
loop_handle: LoopHandle<'static, SimpleWindow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,6 +324,12 @@ impl SeatHandler for SimpleWindow {
|
||||||
|
|
||||||
self.keyboard = Some(keyboard);
|
self.keyboard = Some(keyboard);
|
||||||
}
|
}
|
||||||
|
if capability == Capability::Pointer && self.pointer.is_none() {
|
||||||
|
println!("Set pointer capability");
|
||||||
|
let pointer = self.seat_state.get_pointer(qh, &seat).expect("Failed to create pointer");
|
||||||
|
|
||||||
|
self.pointer = Some(pointer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_capability(
|
fn remove_capability(
|
||||||
|
|
@ -273,6 +348,51 @@ impl SeatHandler for SimpleWindow {
|
||||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PointerHandler for SimpleWindow {
|
||||||
|
fn pointer_frame(
|
||||||
|
&mut self,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
_pointer: &sctk::reexports::client::protocol::wl_pointer::WlPointer,
|
||||||
|
events: &[sctk::seat::pointer::PointerEvent],
|
||||||
|
) {
|
||||||
|
for e in events {
|
||||||
|
match &e.kind {
|
||||||
|
PointerEventKind::Press { button, .. } if *button == BTN_LEFT => {
|
||||||
|
println!("Starting a drag!");
|
||||||
|
self.clipboard.start_dnd(
|
||||||
|
false,
|
||||||
|
self.window.wl_surface().clone(),
|
||||||
|
None,
|
||||||
|
smithay_clipboard::text::Text("Clipboard Drag and Drop!".to_string()),
|
||||||
|
DndAction::all(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PointerEventKind::Press { button, .. } if *button == BTN_RIGHT => {
|
||||||
|
println!("Starting an internal drag!");
|
||||||
|
self.internal_dnd = true;
|
||||||
|
self.clipboard.start_dnd(
|
||||||
|
true,
|
||||||
|
self.window.wl_surface().clone(),
|
||||||
|
None,
|
||||||
|
smithay_clipboard::text::Text(
|
||||||
|
"Internal clipboard Drag and Drop!".to_string(),
|
||||||
|
),
|
||||||
|
DndAction::all(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PointerEventKind::Leave { .. } => {
|
||||||
|
self.pointer_focus = false;
|
||||||
|
},
|
||||||
|
PointerEventKind::Enter { .. } => {
|
||||||
|
self.pointer_focus = true;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl KeyboardHandler for SimpleWindow {
|
impl KeyboardHandler for SimpleWindow {
|
||||||
fn enter(
|
fn enter(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -509,6 +629,7 @@ delegate_shm!(SimpleWindow);
|
||||||
|
|
||||||
delegate_seat!(SimpleWindow);
|
delegate_seat!(SimpleWindow);
|
||||||
delegate_keyboard!(SimpleWindow);
|
delegate_keyboard!(SimpleWindow);
|
||||||
|
delegate_pointer!(SimpleWindow);
|
||||||
|
|
||||||
delegate_xdg_shell!(SimpleWindow);
|
delegate_xdg_shell!(SimpleWindow);
|
||||||
delegate_xdg_window!(SimpleWindow);
|
delegate_xdg_window!(SimpleWindow);
|
||||||
|
|
|
||||||
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
nightly
|
||||||
|
|
@ -61,7 +61,7 @@ pub enum SourceEvent {
|
||||||
Action(DndAction),
|
Action(DndAction),
|
||||||
/// Mime accepted by destination.
|
/// Mime accepted by destination.
|
||||||
/// If [`None`], no mime types are accepted.
|
/// If [`None`], no mime types are accepted.
|
||||||
Mime(Option<String>),
|
Mime(Option<MimeType>),
|
||||||
/// DnD Dropped. The operation is still ongoing until receiving a
|
/// DnD Dropped. The operation is still ongoing until receiving a
|
||||||
/// [`Finished`] event.
|
/// [`Finished`] event.
|
||||||
Dropped,
|
Dropped,
|
||||||
|
|
@ -72,7 +72,7 @@ pub enum OfferEvent<T> {
|
||||||
Enter {
|
Enter {
|
||||||
x: f64,
|
x: f64,
|
||||||
y: f64,
|
y: f64,
|
||||||
mime_types: Vec<String>,
|
mime_types: Vec<MimeType>,
|
||||||
surface: T,
|
surface: T,
|
||||||
},
|
},
|
||||||
Motion {
|
Motion {
|
||||||
|
|
@ -91,7 +91,7 @@ pub enum OfferEvent<T> {
|
||||||
SelectedAction(DndAction),
|
SelectedAction(DndAction),
|
||||||
Data {
|
Data {
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
mime_type: String,
|
mime_type: MimeType,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,12 +131,16 @@ pub enum DndRequest<T> {
|
||||||
Surface(DndSurface<T>, Vec<DndDestinationRectangle>),
|
Surface(DndSurface<T>, Vec<DndDestinationRectangle>),
|
||||||
/// Start a Dnd operation with the given source surface and data.
|
/// Start a Dnd operation with the given source surface and data.
|
||||||
StartDnd {
|
StartDnd {
|
||||||
|
internal: bool,
|
||||||
source: DndSurface<T>,
|
source: DndSurface<T>,
|
||||||
icon: Option<DndSurface<T>>,
|
icon: Option<DndSurface<T>>,
|
||||||
content: Box<dyn AsMimeTypes + Send>,
|
content: Box<dyn AsMimeTypes + Send>,
|
||||||
|
actions: DndAction,
|
||||||
},
|
},
|
||||||
/// Set the DnD action chosen by the user.
|
/// Set the DnD action chosen by the user.
|
||||||
SetAction(DndAction),
|
SetAction(DndAction),
|
||||||
|
/// End an active DnD Source
|
||||||
|
DndEnd,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -171,21 +175,27 @@ impl<T: RawSurface> Clipboard<T> {
|
||||||
/// Start a DnD operation on the given surface with some data
|
/// Start a DnD operation on the given surface with some data
|
||||||
pub fn start_dnd<D: AsMimeTypes + Send + 'static>(
|
pub fn start_dnd<D: AsMimeTypes + Send + 'static>(
|
||||||
&self,
|
&self,
|
||||||
|
internal: bool,
|
||||||
source_surface: T,
|
source_surface: T,
|
||||||
icon_surface: Option<T>,
|
icon_surface: Option<T>,
|
||||||
content: D,
|
content: D,
|
||||||
|
actions: DndAction,
|
||||||
) {
|
) {
|
||||||
let source = DndSurface::new(source_surface, &self.connection).unwrap();
|
let source = DndSurface::new(source_surface, &self.connection).unwrap();
|
||||||
let icon = icon_surface.map(|s| DndSurface::new(s, &self.connection).unwrap());
|
let icon = icon_surface.map(|s| DndSurface::new(s, &self.connection).unwrap());
|
||||||
_ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::StartDnd {
|
_ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::StartDnd {
|
||||||
|
internal,
|
||||||
source,
|
source,
|
||||||
icon,
|
icon,
|
||||||
content: Box::new(content),
|
content: Box::new(content),
|
||||||
|
actions,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// End the current DnD operation, if there is one
|
/// End the current DnD operation, if there is one
|
||||||
pub fn end_dnd() {}
|
pub fn end_dnd(&self) {
|
||||||
|
_ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::DndEnd));
|
||||||
|
}
|
||||||
|
|
||||||
/// Register a surface for receiving DnD offers
|
/// Register a surface for receiving DnD offers
|
||||||
/// Rectangles should be provided in order of decreasing priority.
|
/// Rectangles should be provided in order of decreasing priority.
|
||||||
|
|
@ -198,5 +208,9 @@ impl<T: RawSurface> Clipboard<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the final action after presenting the user with a choice
|
/// Set the final action after presenting the user with a choice
|
||||||
pub fn set_action(&self, action: DndAction) {}
|
pub fn set_action(&self, action: DndAction) {
|
||||||
|
_ = self
|
||||||
|
.request_sender
|
||||||
|
.send(crate::worker::Command::DndRequest(DndRequest::SetAction(action)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
169
src/dnd/state.rs
169
src/dnd/state.rs
|
|
@ -1,12 +1,13 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{Error, ErrorKind, Read};
|
use std::io::{Error, ErrorKind, Read, Write};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use sctk::data_device_manager::data_offer::DragOffer;
|
use sctk::data_device_manager::data_offer::DragOffer;
|
||||||
use sctk::data_device_manager::data_source::DragSource;
|
use sctk::data_device_manager::data_source::DragSource;
|
||||||
|
use sctk::data_device_manager::WritePipe;
|
||||||
use sctk::reexports::calloop::PostAction;
|
use sctk::reexports::calloop::PostAction;
|
||||||
use sctk::reexports::client::protocol::wl_data_device::WlDataDevice;
|
use sctk::reexports::client::protocol::wl_data_device::WlDataDevice;
|
||||||
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
|
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
|
||||||
|
|
@ -23,13 +24,14 @@ use super::{DndDestinationRectangle, DndEvent, DndRequest, DndSurface, OfferEven
|
||||||
pub(crate) struct DndState<T> {
|
pub(crate) struct DndState<T> {
|
||||||
pub(crate) sender: Option<Box<dyn crate::dnd::Sender<T>>>,
|
pub(crate) sender: Option<Box<dyn crate::dnd::Sender<T>>>,
|
||||||
destinations: HashMap<ObjectId, (DndSurface<T>, Vec<DndDestinationRectangle>)>,
|
destinations: HashMap<ObjectId, (DndSurface<T>, Vec<DndDestinationRectangle>)>,
|
||||||
dnd_sources: Option<DragSource>,
|
pub(crate) dnd_source: Option<DragSource>,
|
||||||
active_surface: Option<(DndSurface<T>, Option<DndDestinationRectangle>)>,
|
active_surface: Option<(DndSurface<T>, Option<DndDestinationRectangle>)>,
|
||||||
source_actions: DndAction,
|
source_actions: DndAction,
|
||||||
selected_action: DndAction,
|
selected_action: DndAction,
|
||||||
selected_mime: Option<MimeType>,
|
selected_mime: Option<MimeType>,
|
||||||
pub(crate) source_content: Box<dyn AsMimeTypes>,
|
pub(crate) source_content: Option<Box<dyn AsMimeTypes>>,
|
||||||
pub(crate) source_mime_types: Rc<Cow<'static, [MimeType]>>,
|
pub(crate) source_mime_types: Rc<Cow<'static, [MimeType]>>,
|
||||||
|
accept_ctr: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Default for DndState<T> {
|
impl<T> Default for DndState<T> {
|
||||||
|
|
@ -37,13 +39,14 @@ impl<T> Default for DndState<T> {
|
||||||
Self {
|
Self {
|
||||||
sender: Default::default(),
|
sender: Default::default(),
|
||||||
destinations: Default::default(),
|
destinations: Default::default(),
|
||||||
dnd_sources: Default::default(),
|
dnd_source: Default::default(),
|
||||||
active_surface: None,
|
active_surface: None,
|
||||||
source_actions: DndAction::empty(),
|
source_actions: DndAction::empty(),
|
||||||
selected_action: DndAction::empty(),
|
selected_action: DndAction::empty(),
|
||||||
selected_mime: None,
|
selected_mime: None,
|
||||||
source_content: Box::new(Text(String::new())),
|
source_content: None,
|
||||||
source_mime_types: Rc::new(Cow::Owned(Vec::new())),
|
source_mime_types: Rc::new(Cow::Owned(Vec::new())),
|
||||||
|
accept_ctr: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +107,8 @@ where
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
dnd_state.set_actions(DndAction::empty(), DndAction::empty());
|
dnd_state.set_actions(DndAction::empty(), DndAction::empty());
|
||||||
dnd_state.accept_mime_type(dnd_state.serial, None);
|
dnd_state.accept_mime_type(self.dnd_state.accept_ctr, None);
|
||||||
|
self.dnd_state.accept_ctr = self.dnd_state.accept_ctr.wrapping_add(1);
|
||||||
self.dnd_state.selected_action = DndAction::empty();
|
self.dnd_state.selected_action = DndAction::empty();
|
||||||
self.dnd_state.selected_mime = None;
|
self.dnd_state.selected_mime = None;
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +120,8 @@ where
|
||||||
{
|
{
|
||||||
dnd_state.set_actions(action, preferred_action);
|
dnd_state.set_actions(action, preferred_action);
|
||||||
self.dnd_state.selected_mime = Some(mime_type.clone());
|
self.dnd_state.selected_mime = Some(mime_type.clone());
|
||||||
dnd_state.accept_mime_type(dnd_state.serial, Some(mime_type.to_string()))
|
dnd_state.accept_mime_type(self.dnd_state.accept_ctr, Some(mime_type.to_string()));
|
||||||
|
self.dnd_state.accept_ctr = self.dnd_state.accept_ctr.wrapping_add(1);
|
||||||
}
|
}
|
||||||
(s.clone(), Some(dest))
|
(s.clone(), Some(dest))
|
||||||
});
|
});
|
||||||
|
|
@ -174,7 +179,6 @@ where
|
||||||
if self.dnd_state.sender.is_none() {
|
if self.dnd_state.sender.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dbg!(&self.dnd_state.destinations);
|
|
||||||
let Some(data_device) = self
|
let Some(data_device) = self
|
||||||
.seats
|
.seats
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -182,8 +186,12 @@ where
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let dnd_state = data_device.data().drag_offer();
|
let drag_offer = data_device.data().drag_offer();
|
||||||
self.update_active_surface(surface, x, y, dnd_state.as_ref());
|
if drag_offer.is_none() && self.dnd_state.source_content.is_none() {
|
||||||
|
// Ignore cancelled internal DnD
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.update_active_surface(surface, x, y, drag_offer.as_ref());
|
||||||
let Some((surface, id)) = self
|
let Some((surface, id)) = self
|
||||||
.dnd_state
|
.dnd_state
|
||||||
.active_surface
|
.active_surface
|
||||||
|
|
@ -220,9 +228,13 @@ where
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let dnd_state = data_device.data().drag_offer();
|
let drag_offer = data_device.data().drag_offer();
|
||||||
|
if drag_offer.is_none() && self.dnd_state.source_content.is_none() {
|
||||||
|
// Ignore cancelled internal DnD
|
||||||
|
return;
|
||||||
|
}
|
||||||
if dest.is_none() {
|
if dest.is_none() {
|
||||||
self.update_active_surface(&surface.surface, x, y, dnd_state.as_ref());
|
self.update_active_surface(&surface.surface, x, y, drag_offer.as_ref());
|
||||||
}
|
}
|
||||||
let id = self.cur_id();
|
let id = self.cur_id();
|
||||||
if let Some(tx) = self.dnd_state.sender.as_ref() {
|
if let Some(tx) = self.dnd_state.sender.as_ref() {
|
||||||
|
|
@ -245,13 +257,99 @@ where
|
||||||
DndRequest::Surface(s, dests) => {
|
DndRequest::Surface(s, dests) => {
|
||||||
self.dnd_state.destinations.insert(s.surface.id(), (s, dests));
|
self.dnd_state.destinations.insert(s.surface.id(), (s, dests));
|
||||||
},
|
},
|
||||||
DndRequest::StartDnd { source, icon, content } => {},
|
DndRequest::StartDnd { internal, source, icon, content, actions } => {
|
||||||
DndRequest::SetAction(_) => {
|
_ = self.start_dnd(internal, source, icon, content, actions);
|
||||||
todo!()
|
},
|
||||||
|
DndRequest::SetAction(a) => {
|
||||||
|
_ = self.user_selected_action(a);
|
||||||
|
},
|
||||||
|
DndRequest::DndEnd => {
|
||||||
|
self.dnd_state.source_content = None;
|
||||||
|
self.dnd_state.dnd_source = None;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_dnd(
|
||||||
|
&mut self,
|
||||||
|
internal: bool,
|
||||||
|
source_surface: DndSurface<T>,
|
||||||
|
icon: Option<DndSurface<T>>,
|
||||||
|
content: Box<dyn AsMimeTypes + Send>,
|
||||||
|
actions: DndAction,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let latest = self
|
||||||
|
.latest_seat
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::Other, "no events received on any seat"))?;
|
||||||
|
let seat = self
|
||||||
|
.seats
|
||||||
|
.get_mut(latest)
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::Other, "active seat lost"))?;
|
||||||
|
let serial = seat.latest_serial;
|
||||||
|
|
||||||
|
let data_device = seat
|
||||||
|
.data_device
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::Other, "data device missing"))?;
|
||||||
|
|
||||||
|
if internal {
|
||||||
|
DragSource::start_internal_drag(
|
||||||
|
data_device,
|
||||||
|
&source_surface.surface,
|
||||||
|
icon.as_ref().map(|s| &s.surface),
|
||||||
|
serial,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let mime_types = content.available();
|
||||||
|
let source = self
|
||||||
|
.data_device_manager_state
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| {
|
||||||
|
s.create_drag_and_drop_source(
|
||||||
|
&self.queue_handle,
|
||||||
|
mime_types.iter().map(|m| m.as_ref()),
|
||||||
|
actions,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::Other, "data device manager missing"))?;
|
||||||
|
source.start_drag(
|
||||||
|
data_device,
|
||||||
|
&source_surface.surface,
|
||||||
|
icon.as_ref().map(|s| &s.surface),
|
||||||
|
serial,
|
||||||
|
);
|
||||||
|
self.dnd_state.dnd_source = Some(source);
|
||||||
|
self.dnd_state.source_content = Some(content);
|
||||||
|
self.dnd_state.source_actions = actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_selected_action(&mut self, a: DndAction) -> std::io::Result<()> {
|
||||||
|
let latest = self
|
||||||
|
.latest_seat
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::Other, "no events received on any seat"))?;
|
||||||
|
let seat = self
|
||||||
|
.seats
|
||||||
|
.get_mut(latest)
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::Other, "active seat lost"))?;
|
||||||
|
|
||||||
|
let offer = seat
|
||||||
|
.data_device
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d.data().drag_offer())
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::Other, "offer does not exist."))?;
|
||||||
|
offer.set_actions(a, a);
|
||||||
|
|
||||||
|
if let Some(mime_type) = self.dnd_state.selected_mime.clone() {
|
||||||
|
_ = self.load_dnd(mime_type);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Load data for the given target.
|
/// Load data for the given target.
|
||||||
pub fn load_dnd(&mut self, mut mime_type: MimeType) -> std::io::Result<()> {
|
pub fn load_dnd(&mut self, mut mime_type: MimeType) -> std::io::Result<()> {
|
||||||
let cur_id = self.cur_id();
|
let cur_id = self.cur_id();
|
||||||
|
|
@ -290,7 +388,7 @@ where
|
||||||
offer.finish();
|
offer.finish();
|
||||||
let _ = tx.send(DndEvent::Offer(cur_id, OfferEvent::Data {
|
let _ = tx.send(DndEvent::Offer(cur_id, OfferEvent::Data {
|
||||||
data: mem::take(&mut content),
|
data: mem::take(&mut content),
|
||||||
mime_type: mem::take(&mut mime_type).to_string(),
|
mime_type: mem::take(&mut mime_type),
|
||||||
}));
|
}));
|
||||||
break PostAction::Remove;
|
break PostAction::Remove;
|
||||||
},
|
},
|
||||||
|
|
@ -306,4 +404,43 @@ where
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send_dnd_request(&self, write_pipe: WritePipe, mime: String) {
|
||||||
|
let Some(content) = self.dnd_state.source_content.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(mime_type) = MimeType::find_allowed(&[mime], &content.available()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark FD as non-blocking so we won't block ourselves.
|
||||||
|
unsafe {
|
||||||
|
if set_non_blocking(write_pipe.as_raw_fd()).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't access the content on the state directly, since it could change during
|
||||||
|
// the send.
|
||||||
|
let contents = content.as_bytes(&mime_type);
|
||||||
|
let Some(contents) = contents else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut written = 0;
|
||||||
|
let _ = self.loop_handle.insert_source(write_pipe, move |_, file, _| {
|
||||||
|
let file = unsafe { file.get_mut() };
|
||||||
|
loop {
|
||||||
|
match file.write(&contents[written..]) {
|
||||||
|
Ok(n) if written + n == contents.len() => {
|
||||||
|
written += n;
|
||||||
|
break PostAction::Remove;
|
||||||
|
},
|
||||||
|
Ok(n) => written += n,
|
||||||
|
Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue,
|
||||||
|
Err(_) => break PostAction::Remove,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use sctk::reexports::client::Connection;
|
||||||
pub mod dnd;
|
pub mod dnd;
|
||||||
pub mod mime;
|
pub mod mime;
|
||||||
mod state;
|
mod state;
|
||||||
mod text;
|
pub mod text;
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
|
use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
|
||||||
|
|
|
||||||
94
src/state.rs
94
src/state.rs
|
|
@ -39,7 +39,7 @@ use sctk::reexports::protocols::wp::primary_selection::zv1::client::{
|
||||||
use wayland_backend::client::ObjectId;
|
use wayland_backend::client::ObjectId;
|
||||||
|
|
||||||
use crate::dnd::state::DndState;
|
use crate::dnd::state::DndState;
|
||||||
use crate::dnd::DndSurface;
|
use crate::dnd::{DndEvent, DndSurface};
|
||||||
use crate::mime::{AsMimeTypes, MimeType};
|
use crate::mime::{AsMimeTypes, MimeType};
|
||||||
use crate::text::Text;
|
use crate::text::Text;
|
||||||
|
|
||||||
|
|
@ -50,14 +50,14 @@ pub struct State<T> {
|
||||||
pub exit: bool,
|
pub exit: bool,
|
||||||
|
|
||||||
registry_state: RegistryState,
|
registry_state: RegistryState,
|
||||||
seat_state: SeatState,
|
pub(crate) seat_state: SeatState,
|
||||||
|
|
||||||
pub(crate) seats: HashMap<ObjectId, ClipboardSeatState>,
|
pub(crate) seats: HashMap<ObjectId, ClipboardSeatState>,
|
||||||
/// The latest seat which got an event.
|
/// The latest seat which got an event.
|
||||||
pub(crate) latest_seat: Option<ObjectId>,
|
pub(crate) latest_seat: Option<ObjectId>,
|
||||||
|
|
||||||
pub(crate) loop_handle: LoopHandle<'static, Self>,
|
pub(crate) loop_handle: LoopHandle<'static, Self>,
|
||||||
queue_handle: QueueHandle<Self>,
|
pub(crate) queue_handle: QueueHandle<Self>,
|
||||||
|
|
||||||
primary_sources: Vec<PrimarySelectionSource>,
|
primary_sources: Vec<PrimarySelectionSource>,
|
||||||
primary_selection_content: Box<dyn AsMimeTypes>,
|
primary_selection_content: Box<dyn AsMimeTypes>,
|
||||||
|
|
@ -148,10 +148,6 @@ impl<T: 'static + Clone> State<T> {
|
||||||
source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial);
|
source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial);
|
||||||
self.primary_sources.push(source);
|
self.primary_sources.push(source);
|
||||||
},
|
},
|
||||||
#[cfg(feature = "dnd")]
|
|
||||||
Target::DnD => {
|
|
||||||
unreachable!()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
|
|
@ -211,21 +207,6 @@ impl<T: 'static + Clone> State<T> {
|
||||||
|
|
||||||
(selection.receive(mime_type.to_string())?, mime_type)
|
(selection.receive(mime_type.to_string())?, mime_type)
|
||||||
},
|
},
|
||||||
#[cfg(feature = "dnd")]
|
|
||||||
Target::DnD => {
|
|
||||||
let offer = seat
|
|
||||||
.data_device
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|d| d.data().drag_offer())
|
|
||||||
.ok_or_else(|| Error::new(ErrorKind::Other, "offer does not exist."))?;
|
|
||||||
let Some(mime) = allowed_mime_types.first() else {
|
|
||||||
return Err(Error::new(
|
|
||||||
ErrorKind::NotFound,
|
|
||||||
"supported mime-type is not found",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
(offer.receive(mime.to_string())?, mime.clone())
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mark FD as non-blocking so we won't block ourselves.
|
// Mark FD as non-blocking so we won't block ourselves.
|
||||||
|
|
@ -262,8 +243,6 @@ impl<T: 'static + Clone> State<T> {
|
||||||
let Some(mime_type) = MimeType::find_allowed(&[mime], match ty {
|
let Some(mime_type) = MimeType::find_allowed(&[mime], match ty {
|
||||||
Target::Clipboard => &self.data_selection_mime_types,
|
Target::Clipboard => &self.data_selection_mime_types,
|
||||||
Target::Primary => &self.primary_selection_mime_types,
|
Target::Primary => &self.primary_selection_mime_types,
|
||||||
#[cfg(feature = "dnd")]
|
|
||||||
Target::DnD => &self.dnd_state.source_mime_types,
|
|
||||||
}) else {
|
}) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -280,8 +259,6 @@ impl<T: 'static + Clone> State<T> {
|
||||||
let contents = match ty {
|
let contents = match ty {
|
||||||
Target::Clipboard => self.data_selection_content.as_bytes(&mime_type),
|
Target::Clipboard => self.data_selection_content.as_bytes(&mime_type),
|
||||||
Target::Primary => self.primary_selection_content.as_bytes(&mime_type),
|
Target::Primary => self.primary_selection_content.as_bytes(&mime_type),
|
||||||
#[cfg(feature = "dnd")]
|
|
||||||
Target::DnD => self.dnd_state.source_content.as_bytes(&mime_type),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(contents) = contents else {
|
let Some(contents) = contents else {
|
||||||
|
|
@ -471,15 +448,34 @@ impl<T: 'static + Clone> DataSourceHandler for State<T> {
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Connection,
|
_: &Connection,
|
||||||
_: &QueueHandle<Self>,
|
_: &QueueHandle<Self>,
|
||||||
_: &WlDataSource,
|
_source: &WlDataSource,
|
||||||
mime: String,
|
mime: String,
|
||||||
write_pipe: WritePipe,
|
write_pipe: WritePipe,
|
||||||
) {
|
) {
|
||||||
|
#[cfg(feature = "dnd")]
|
||||||
|
if self
|
||||||
|
.dnd_state
|
||||||
|
.dnd_source
|
||||||
|
.as_ref()
|
||||||
|
.map(|my_source| my_source.inner() == _source)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
self.send_dnd_request(write_pipe, mime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.send_request(Target::Clipboard, write_pipe, mime)
|
self.send_request(Target::Clipboard, write_pipe, mime)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, deleted: &WlDataSource) {
|
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, deleted: &WlDataSource) {
|
||||||
self.data_sources.retain(|source| source.inner() != deleted)
|
self.data_sources.retain(|source| source.inner() != deleted);
|
||||||
|
#[cfg(feature = "dnd")]
|
||||||
|
{
|
||||||
|
self.dnd_state.source_content = None;
|
||||||
|
self.dnd_state.dnd_source = None;
|
||||||
|
if let Some(s) = self.dnd_state.sender.as_ref() {
|
||||||
|
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Cancelled));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_mime(
|
fn accept_mime(
|
||||||
|
|
@ -487,15 +483,44 @@ impl<T: 'static + Clone> DataSourceHandler for State<T> {
|
||||||
_: &Connection,
|
_: &Connection,
|
||||||
_: &QueueHandle<Self>,
|
_: &QueueHandle<Self>,
|
||||||
_: &WlDataSource,
|
_: &WlDataSource,
|
||||||
_: Option<String>,
|
m: Option<String>,
|
||||||
) {
|
) {
|
||||||
|
#[cfg(feature = "dnd")]
|
||||||
|
{
|
||||||
|
if let Some(s) = self.dnd_state.sender.as_ref() {
|
||||||
|
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Mime(m.map(|s| MimeType::from(Cow::Owned(s))))));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
|
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
|
||||||
|
#[cfg(feature = "dnd")]
|
||||||
|
{
|
||||||
|
if let Some(s) = self.dnd_state.sender.as_ref() {
|
||||||
|
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Dropped))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn action(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, _: DndAction) {}
|
fn action(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, a: DndAction) {
|
||||||
|
#[cfg(feature = "dnd")]
|
||||||
|
{
|
||||||
|
if let Some(s) = self.dnd_state.sender.as_ref() {
|
||||||
|
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Action(a)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
|
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
|
||||||
|
#[cfg(feature = "dnd")]
|
||||||
|
{
|
||||||
|
self.dnd_state.source_content = None;
|
||||||
|
self.dnd_state.dnd_source = None;
|
||||||
|
if let Some(s) = self.dnd_state.sender.as_ref() {
|
||||||
|
_ = s.send(DndEvent::Source(crate::dnd::SourceEvent::Finished));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static + Clone> DataOfferHandler for State<T> {
|
impl<T: 'static + Clone> DataOfferHandler for State<T> {
|
||||||
|
|
@ -605,9 +630,6 @@ pub enum Target {
|
||||||
Clipboard,
|
Clipboard,
|
||||||
/// The target is primary selection.
|
/// The target is primary selection.
|
||||||
Primary,
|
Primary,
|
||||||
#[cfg(feature = "dnd")]
|
|
||||||
/// The targe is a DnD offer.
|
|
||||||
DnD,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|
@ -619,7 +641,7 @@ pub(crate) struct ClipboardSeatState {
|
||||||
pub(crate) has_focus: bool,
|
pub(crate) has_focus: bool,
|
||||||
|
|
||||||
/// The latest serial used to set the selection content.
|
/// The latest serial used to set the selection content.
|
||||||
latest_serial: u32,
|
pub(crate) latest_serial: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for ClipboardSeatState {
|
impl Drop for ClipboardSeatState {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue