use std::collections::HashMap;
use std::io::{Read, Write};
use std::os::fd::{AsFd, FromRawFd, OwnedFd};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Arc, Mutex};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use teksilo_canvas::Point;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::window::TeksiloWindowId;
use teksilo_core::{
AppEventPoster, DragImageData, DropOutcome, ExternalDropData, OutboundDragData,
};
use wayland_backend::client::ObjectId;
use wayland_backend::sys::client::Backend;
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::wl_data_device::{Event as DataDeviceEvent, WlDataDevice};
use wayland_client::protocol::wl_data_device_manager::{DndAction, WlDataDeviceManager};
use wayland_client::protocol::wl_data_offer::{Event as DataOfferEvent, WlDataOffer};
use wayland_client::protocol::wl_data_source::{Event as DataSourceEvent, WlDataSource};
use wayland_client::protocol::wl_pointer::{ButtonState, Event as PointerEvent, WlPointer};
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum};
use super::{
ExternalDndBackend, ExternalDndEventPayload, ExternalDndGuard, ExternalDragEvent, NoopDndGuard,
outbound_bytes, outbound_mimes,
};
enum OutboundCommand {
Begin {
data: OutboundDragData,
#[allow(dead_code)] image: Option<DragImageData>,
},
}
const PREFERRED_MIMES: &[&str] = &["text/uri-list", "text/plain;charset=utf-8", "text/plain"];
#[derive(Clone)]
struct Route {
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
}
#[derive(Clone, Copy)]
struct Claim {
device: TeksiloWindowId,
serial: u32,
}
#[derive(Default)]
struct SharedDnd {
routes: Mutex<HashMap<u32, Route>>,
claim: Mutex<Option<Claim>>,
}
impl SharedDnd {
fn route_for(&self, surface: &ObjectId) -> Option<Route> {
self.routes
.lock()
.ok()?
.get(&surface.protocol_id())
.cloned()
}
fn claim_drag(&self, serial: u32, device: TeksiloWindowId) -> bool {
let Ok(mut claim) = self.claim.lock() else {
return true;
};
match *claim {
Some(existing) if existing.serial == serial => existing.device == device,
_ => {
*claim = Some(Claim { device, serial });
true
}
}
}
fn release_claim(&self, device: TeksiloWindowId) {
if let Ok(mut claim) = self.claim.lock()
&& claim.is_some_and(|c| c.device == device)
{
*claim = None;
}
}
}
struct DndState {
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
conn: Connection,
qh: QueueHandle<DndState>,
shared: Arc<SharedDnd>,
active_route: Option<Route>,
current_offer: Option<WlDataOffer>,
offer_mimes: Vec<String>,
position: Point,
data_device_manager: WlDataDeviceManager,
data_device: WlDataDevice,
origin_surface: Option<WlSurface>,
last_press_serial: u32,
outbound_source: Option<WlDataSource>,
outbound_data: Option<OutboundDragData>,
outbound_action: DndAction,
outbound_finished: bool,
cmd_rx: Receiver<OutboundCommand>,
_seat: WlSeat,
_pointer: WlPointer,
}
impl DndState {
fn post(&self, event: ExternalDragEvent) {
self.poster.post_external(Box::new(ExternalDndEventPayload {
window_id_owner: self.window_id,
event,
}));
}
fn post_routed(&self, event: ExternalDragEvent) {
let Some(route) = &self.active_route else {
return;
};
route
.poster
.post_external(Box::new(ExternalDndEventPayload {
window_id_owner: route.window_id,
event,
}));
}
fn process_outbound_commands(&mut self) {
while let Ok(cmd) = self.cmd_rx.try_recv() {
match cmd {
OutboundCommand::Begin { data, image: _ } => self.begin_outbound(data),
}
}
}
fn begin_outbound(&mut self, data: OutboundDragData) {
let Some(origin) = self.origin_surface.clone() else {
self.post(ExternalDragEvent::DragEnded {
outcome: DropOutcome::Cancelled,
});
return;
};
if self.last_press_serial == 0 {
self.post(ExternalDragEvent::DragEnded {
outcome: DropOutcome::Cancelled,
});
return;
}
if let Some(src) = self.outbound_source.take() {
src.destroy();
}
let source = self.data_device_manager.create_data_source(&self.qh, ());
for mime in outbound_mimes(&data) {
source.offer(mime);
}
if source.version() >= 3 {
source.set_actions(DndAction::Copy);
}
self.data_device.start_drag(
Some(&source),
&origin,
None, self.last_press_serial,
);
let _ = self.conn.flush();
self.outbound_data = Some(data);
self.outbound_source = Some(source);
self.outbound_action = DndAction::empty();
self.outbound_finished = false;
}
fn serve_send(&self, mime_type: &str, fd: OwnedFd) {
let Some(data) = &self.outbound_data else {
return;
};
let bytes = outbound_bytes(data, mime_type);
let mut file = std::fs::File::from(fd);
let _ = file.write_all(&bytes);
}
fn finish_outbound(&mut self, outcome: DropOutcome) {
if let Some(src) = self.outbound_source.take() {
src.destroy();
}
self.outbound_data = None;
self.post(ExternalDragEvent::DragEnded { outcome });
}
}
impl Dispatch<WlRegistry, GlobalListContents> for DndState {
fn event(
_: &mut Self,
_: &WlRegistry,
_: <WlRegistry as Proxy>::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WlSeat, ()> for DndState {
fn event(
_: &mut Self,
_: &WlSeat,
_: <WlSeat as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WlPointer, ()> for DndState {
fn event(
state: &mut Self,
_pointer: &WlPointer,
event: PointerEvent,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let PointerEvent::Button {
serial, state: btn, ..
} = event
&& btn == WEnum::Value(ButtonState::Pressed)
{
state.last_press_serial = serial;
}
}
}
impl Dispatch<WlDataSource, ()> for DndState {
fn event(
state: &mut Self,
_source: &WlDataSource,
event: DataSourceEvent,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
DataSourceEvent::Send { mime_type, fd } => {
state.serve_send(&mime_type, fd);
let _ = state.conn.flush();
}
DataSourceEvent::Action {
dnd_action: WEnum::Value(action),
} => {
state.outbound_action = action;
}
DataSourceEvent::DndDropPerformed => {
state.outbound_finished = true;
}
DataSourceEvent::DndFinished => {
let outcome = if state.outbound_action.contains(DndAction::Move) {
DropOutcome::OsMove
} else {
DropOutcome::OsCopy
};
state.finish_outbound(outcome);
}
DataSourceEvent::Cancelled => {
if state.outbound_finished {
let outcome = if state.outbound_action.contains(DndAction::Move) {
DropOutcome::OsMove
} else {
DropOutcome::OsCopy
};
state.finish_outbound(outcome);
} else {
state.finish_outbound(DropOutcome::Cancelled);
}
}
_ => {}
}
}
}
impl Dispatch<WlDataDeviceManager, ()> for DndState {
fn event(
_: &mut Self,
_: &WlDataDeviceManager,
_: <WlDataDeviceManager as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WlDataOffer, ()> for DndState {
fn event(
state: &mut Self,
_offer: &WlDataOffer,
event: DataOfferEvent,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let DataOfferEvent::Offer { mime_type } = event {
state.offer_mimes.push(mime_type);
}
}
}
impl Dispatch<WlDataDevice, ()> for DndState {
fn event(
state: &mut Self,
_device: &WlDataDevice,
event: DataDeviceEvent,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
DataDeviceEvent::DataOffer { id } => {
state.current_offer = Some(id);
state.offer_mimes.clear();
}
DataDeviceEvent::Enter {
serial,
surface,
x,
y,
id,
} => {
let route = state.shared.route_for(&surface.id());
let Some(route) = route else {
state.active_route = None;
return;
};
if !state.shared.claim_drag(serial, state.window_id) {
state.active_route = None;
return;
}
state.active_route = Some(route);
state.position = Point::new(x as f32, y as f32);
if let Some(offer) = &id {
if let Some(mime) = pick_mime(&state.offer_mimes) {
offer.accept(serial, Some(mime));
}
if offer.version() >= 3 {
offer.set_actions(DndAction::Copy, DndAction::Copy);
}
}
state.post_routed(ExternalDragEvent::Entered {
data: ExternalDropData {
formats: state.offer_mimes.clone(),
..Default::default()
},
position: state.position,
});
}
DataDeviceEvent::Motion { x, y, .. } => {
if state.active_route.is_none() {
return;
}
state.position = Point::new(x as f32, y as f32);
state.post_routed(ExternalDragEvent::Moved {
position: state.position,
});
}
DataDeviceEvent::Leave => {
state.post_routed(ExternalDragEvent::Left);
state.active_route = None;
state.shared.release_claim(state.window_id);
state.current_offer = None;
}
DataDeviceEvent::Drop => {
if state.active_route.is_none() {
return;
}
let data = if state.outbound_source.is_some() {
ExternalDropData::default()
} else {
state
.current_offer
.as_ref()
.map(|offer| receive(&state.conn, offer, &state.offer_mimes))
.unwrap_or_default()
};
state.post_routed(ExternalDragEvent::Dropped {
data,
position: state.position,
});
if let Some(offer) = state.current_offer.take() {
if offer.version() >= 3 {
offer.finish();
}
offer.destroy();
}
state.active_route = None;
state.shared.release_claim(state.window_id);
}
DataDeviceEvent::Selection { .. } => {}
_ => {}
}
}
wayland_client::event_created_child!(DndState, WlDataDevice, [
wayland_client::protocol::wl_data_device::EVT_DATA_OFFER_OPCODE => (WlDataOffer, ()),
]);
}
fn pick_mime(offered: &[String]) -> Option<String> {
for pref in PREFERRED_MIMES {
if let Some(m) = offered.iter().find(|m| m.as_str() == *pref) {
return Some(m.clone());
}
}
None
}
fn receive(conn: &Connection, offer: &WlDataOffer, mimes: &[String]) -> ExternalDropData {
let Some(mime) = pick_mime(mimes) else {
return ExternalDropData::default();
};
let mut fds = [0i32; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return ExternalDropData::default();
}
let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
offer.receive(mime.clone(), write_fd.as_fd());
let _ = conn.flush();
drop(write_fd);
let mut buf = Vec::new();
let mut file = std::fs::File::from(read_fd);
let _ = file.read_to_end(&mut buf);
let text = String::from_utf8_lossy(&buf).into_owned();
if mime == "text/uri-list" {
ExternalDropData::from_uri_list(&text)
} else {
ExternalDropData {
text: Some(text),
..Default::default()
}
}
}
pub struct WaylandDndGuard {
cmd_tx: Option<Sender<OutboundCommand>>,
shared: Arc<SharedDnd>,
route_key: Option<u32>,
window_id: TeksiloWindowId,
}
impl Drop for WaylandDndGuard {
fn drop(&mut self) {
if let Some(key) = self.route_key
&& let Ok(mut routes) = self.shared.routes.lock()
{
routes.remove(&key);
}
self.shared.release_claim(self.window_id);
}
}
impl ExternalDndGuard for WaylandDndGuard {
fn begin_drag(&self, data: &OutboundDragData, image: Option<&DragImageData>) -> bool {
let Some(tx) = &self.cmd_tx else {
return false;
};
tx.send(OutboundCommand::Begin {
data: data.clone(),
image: image.cloned(),
})
.is_ok()
}
}
#[derive(Default)]
pub struct WaylandExternalDndBackend {
shared: Arc<SharedDnd>,
}
impl WaylandExternalDndBackend {
pub fn new() -> Self {
Self::default()
}
}
impl ExternalDndBackend for WaylandExternalDndBackend {
fn attach(
&mut self,
parent: ParentHandle,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard> {
let RawDisplayHandle::Wayland(display) = parent.raw_display_handle() else {
return Box::new(NoopDndGuard);
};
let RawWindowHandle::Wayland(window) = parent.raw_window_handle() else {
return Box::new(NoopDndGuard);
};
let backend = unsafe { Backend::from_foreign_display(display.display.as_ptr() as *mut _) };
let conn = Connection::from_backend(backend);
let Ok((globals, mut queue)) = registry_queue_init::<DndState>(&conn) else {
return Box::new(NoopDndGuard);
};
let qh = queue.handle();
let Ok(seat) = globals.bind::<WlSeat, _, _>(&qh, 1..=5, ()) else {
return Box::new(NoopDndGuard);
};
let Ok(ddm) = globals.bind::<WlDataDeviceManager, _, _>(&qh, 1..=3, ()) else {
return Box::new(NoopDndGuard);
};
let data_device = ddm.get_data_device(&seat, &qh, ());
let pointer = seat.get_pointer(&qh, ());
let target_surface = unsafe {
ObjectId::from_ptr(WlSurface::interface(), window.surface.as_ptr() as *mut _)
}
.ok();
let origin_surface = target_surface
.clone()
.and_then(|id| WlSurface::from_id(&conn, id).ok());
let route_key = target_surface.as_ref().map(ObjectId::protocol_id);
if let Some(key) = route_key
&& let Ok(mut routes) = self.shared.routes.lock()
{
routes.insert(
key,
Route {
window_id,
poster: poster.clone(),
},
);
}
let (cmd_tx, cmd_rx) = channel::<OutboundCommand>();
let mut state = DndState {
window_id,
poster,
conn,
qh: qh.clone(),
shared: self.shared.clone(),
active_route: None,
current_offer: None,
offer_mimes: Vec::new(),
position: Point::new(0.0, 0.0),
data_device_manager: ddm,
data_device,
origin_surface,
last_press_serial: 0,
outbound_source: None,
outbound_data: None,
outbound_action: DndAction::empty(),
outbound_finished: false,
cmd_rx,
_seat: seat,
_pointer: pointer,
};
std::thread::Builder::new()
.name(format!("teksilo-wayland-dnd-{}", window_id.raw()))
.spawn(move || {
while queue.dispatch_pending(&mut state).is_ok() {
state.process_outbound_commands();
let _ = state.conn.flush();
std::thread::sleep(std::time::Duration::from_millis(8));
}
})
.ok();
Box::new(WaylandDndGuard {
cmd_tx: Some(cmd_tx),
shared: self.shared.clone(),
route_key,
window_id,
})
}
}