use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use teksilo_canvas::Point;
use teksilo_core::AppEventPoster;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::window::TeksiloWindowId;
use teksilo_core::{DragImageData, DropOutcome, ExternalDropData, OutboundDragData};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(all(unix, not(target_os = "macos")))]
mod wayland;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(all(unix, not(target_os = "macos")))]
mod x11;
#[derive(Debug, Clone)]
pub enum ExternalDragEvent {
Entered {
data: ExternalDropData,
position: Point,
},
Moved {
position: Point,
},
Left,
Dropped {
data: ExternalDropData,
position: Point,
},
DragEnded {
outcome: DropOutcome,
},
}
#[derive(Debug)]
pub struct ExternalDndEventPayload {
pub window_id_owner: TeksiloWindowId,
pub event: ExternalDragEvent,
}
#[derive(Debug)]
pub struct OutboundOsDragRequest {
pub window_id: TeksiloWindowId,
}
#[cfg_attr(
not(all(unix, not(target_os = "macos"))),
allow(dead_code, reason = "only the unix backends export via MIME today")
)]
pub(crate) fn outbound_mimes(data: &OutboundDragData) -> Vec<String> {
let mut mimes: Vec<String> = data.mime.keys().cloned().collect();
if (!data.files.is_empty() || !data.uris.is_empty())
&& !mimes.iter().any(|m| m == "text/uri-list")
{
mimes.push("text/uri-list".to_string());
}
if data.text.is_some() && !mimes.iter().any(|m| m == "text/plain") {
mimes.push("text/plain".to_string());
}
mimes
}
#[cfg_attr(
not(all(unix, not(target_os = "macos"))),
allow(dead_code, reason = "only the unix backends export via MIME today")
)]
pub(crate) fn outbound_bytes(data: &OutboundDragData, mime_type: &str) -> Vec<u8> {
if let Some(bytes) = data.mime.get(mime_type) {
return bytes.clone();
}
match mime_type {
"text/uri-list" => data.to_uri_list().into_bytes(),
"text/plain" | "text/plain;charset=utf-8" => {
data.text.clone().unwrap_or_default().into_bytes()
}
_ => Vec::new(),
}
}
pub trait ExternalDndGuard {
fn begin_drag(&self, _data: &OutboundDragData, _image: Option<&DragImageData>) -> bool {
false
}
fn set_scale_factor(&self, _scale: f64) {}
fn cancel_drag(&self) {}
fn run_pending_outbound_drag(&self) {}
}
pub struct NoopDndGuard;
impl ExternalDndGuard for NoopDndGuard {}
pub trait ExternalDndBackend {
fn attach(
&mut self,
parent: ParentHandle,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard>;
}
impl ExternalDndBackend for Box<dyn ExternalDndBackend> {
fn attach(
&mut self,
parent: ParentHandle,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard> {
(**self).attach(parent, window_id, poster)
}
}
struct ExternalDndState {
backend: RefCell<Box<dyn ExternalDndBackend>>,
guards: RefCell<HashMap<TeksiloWindowId, Box<dyn ExternalDndGuard>>>,
}
#[derive(Clone)]
pub struct ExternalDndHandle {
inner: Rc<ExternalDndState>,
}
impl ExternalDndHandle {
pub fn new<B: ExternalDndBackend + 'static>(backend: B) -> Self {
Self {
inner: Rc::new(ExternalDndState {
backend: RefCell::new(Box::new(backend)),
guards: RefCell::new(HashMap::new()),
}),
}
}
pub fn attach(
&self,
window_id: TeksiloWindowId,
parent: ParentHandle,
poster: Arc<dyn AppEventPoster>,
) {
self.detach(window_id);
let guard = self
.inner
.backend
.borrow_mut()
.attach(parent, window_id, poster);
self.inner.guards.borrow_mut().insert(window_id, guard);
}
pub fn detach(&self, window_id: TeksiloWindowId) {
let guard = self.inner.guards.borrow_mut().remove(&window_id);
drop(guard);
}
pub fn attached_count(&self) -> usize {
self.inner.guards.borrow().len()
}
pub fn set_scale_factor(&self, window_id: TeksiloWindowId, scale: f64) {
if let Some(guard) = self.inner.guards.borrow().get(&window_id) {
guard.set_scale_factor(scale);
}
}
pub fn cancel_drag(&self, window_id: TeksiloWindowId) {
if let Some(guard) = self.inner.guards.borrow().get(&window_id) {
guard.cancel_drag();
}
}
pub fn begin_drag(
&self,
window_id: TeksiloWindowId,
data: &OutboundDragData,
image: Option<&DragImageData>,
) -> bool {
self.inner
.guards
.borrow()
.get(&window_id)
.map(|g| g.begin_drag(data, image))
.unwrap_or(false)
}
pub fn run_pending_outbound_drag(&self, window_id: TeksiloWindowId) {
if let Some(guard) = self.inner.guards.borrow().get(&window_id) {
guard.run_pending_outbound_drag();
}
}
}
impl std::fmt::Debug for ExternalDndHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExternalDndHandle")
.field("attached", &self.inner.guards.borrow().len())
.finish_non_exhaustive()
}
}
#[derive(Default)]
pub struct NoopExternalDndBackend;
impl NoopExternalDndBackend {
pub fn new() -> Self {
Self
}
}
impl ExternalDndBackend for NoopExternalDndBackend {
fn attach(
&mut self,
_parent: ParentHandle,
_window_id: TeksiloWindowId,
_poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard> {
Box::new(NoopDndGuard)
}
}
type AttachmentList = Arc<std::sync::Mutex<Vec<(TeksiloWindowId, Arc<dyn AppEventPoster>)>>>;
#[derive(Clone, Default)]
pub struct MemoryExternalDndBackend {
attachments: AttachmentList,
outbound: Arc<std::sync::Mutex<Vec<OutboundDragData>>>,
}
pub struct MemoryDndGuard {
window_id: TeksiloWindowId,
attachments: AttachmentList,
outbound: Arc<std::sync::Mutex<Vec<OutboundDragData>>>,
}
impl ExternalDndGuard for MemoryDndGuard {
fn begin_drag(&self, data: &OutboundDragData, _image: Option<&DragImageData>) -> bool {
self.outbound.lock().unwrap().push(data.clone());
let _ = self.window_id;
true
}
}
impl Drop for MemoryDndGuard {
fn drop(&mut self) {
if let Ok(mut v) = self.attachments.lock() {
v.retain(|(id, _)| *id != self.window_id);
}
}
}
impl MemoryExternalDndBackend {
pub fn new() -> Self {
Self::default()
}
pub fn emit(&self, window_id: TeksiloWindowId, event: ExternalDragEvent) -> bool {
let poster = {
let v = self.attachments.lock().unwrap();
v.iter()
.find(|(id, _)| *id == window_id)
.map(|(_, p)| p.clone())
};
match poster {
Some(p) => {
p.post_external(Box::new(ExternalDndEventPayload {
window_id_owner: window_id,
event,
}) as Box<dyn Any + Send>);
true
}
None => false,
}
}
pub fn attached_windows(&self) -> Vec<TeksiloWindowId> {
self.attachments
.lock()
.unwrap()
.iter()
.map(|(id, _)| *id)
.collect()
}
pub fn outbound_drags(&self) -> Vec<OutboundDragData> {
self.outbound.lock().unwrap().clone()
}
}
impl ExternalDndBackend for MemoryExternalDndBackend {
fn attach(
&mut self,
_parent: ParentHandle,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard> {
self.attachments.lock().unwrap().push((window_id, poster));
Box::new(MemoryDndGuard {
window_id,
attachments: self.attachments.clone(),
outbound: self.outbound.clone(),
})
}
}
#[cfg(all(unix, not(target_os = "macos")))]
struct UnixExternalDndBackend {
wayland: wayland::WaylandExternalDndBackend,
x11: x11::X11ExternalDndBackend,
}
#[cfg(all(unix, not(target_os = "macos")))]
impl ExternalDndBackend for UnixExternalDndBackend {
fn attach(
&mut self,
parent: ParentHandle,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard> {
match crate::window_system::window_system_for_display_handle(&parent.raw_display_handle()) {
crate::window_system::WindowSystem::Wayland => {
self.wayland.attach(parent, window_id, poster)
}
crate::window_system::WindowSystem::X11 => self.x11.attach(parent, window_id, poster),
crate::window_system::WindowSystem::Unknown => Box::new(NoopDndGuard),
}
}
}
pub fn default_backend() -> Box<dyn ExternalDndBackend> {
#[cfg(target_os = "macos")]
{
Box::new(macos::MacOsExternalDndBackend::new())
}
#[cfg(target_os = "windows")]
{
Box::new(windows::WindowsExternalDndBackend::new())
}
#[cfg(all(unix, not(target_os = "macos")))]
{
Box::new(UnixExternalDndBackend {
wayland: wayland::WaylandExternalDndBackend::new(),
x11: x11::X11ExternalDndBackend::new(),
})
}
#[cfg(not(any(target_os = "macos", target_os = "windows", unix)))]
{
Box::new(NoopExternalDndBackend::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::Mutex;
use teksilo_core::SubscriptionId;
struct CapturingPoster {
captured: Mutex<Vec<Box<dyn Any + Send>>>,
}
impl CapturingPoster {
fn new() -> Arc<Self> {
Arc::new(Self {
captured: Mutex::new(Vec::new()),
})
}
fn drain(&self) -> Vec<Box<dyn Any + Send>> {
std::mem::take(&mut *self.captured.lock().unwrap())
}
}
impl AppEventPoster for CapturingPoster {
fn post_subscription_event(&self, _sub_id: SubscriptionId, _event: Box<dyn Any + Send>) {}
fn post_external(&self, payload: Box<dyn Any + Send>) {
self.captured.lock().unwrap().push(payload);
}
}
fn fake_parent() -> ParentHandle {
DummyWindow::parent()
}
struct DummyWindow;
impl DummyWindow {
fn parent() -> ParentHandle {
ParentHandle::from_window(&DummyWindow).expect("dummy parent handle")
}
}
impl raw_window_handle::HasWindowHandle for DummyWindow {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{RawWindowHandle, WindowHandle, XlibWindowHandle};
let raw = RawWindowHandle::Xlib(XlibWindowHandle::new(1));
Ok(unsafe { WindowHandle::borrow_raw(raw) })
}
}
impl raw_window_handle::HasDisplayHandle for DummyWindow {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
use raw_window_handle::{DisplayHandle, RawDisplayHandle, XlibDisplayHandle};
let raw = RawDisplayHandle::Xlib(XlibDisplayHandle::new(None, 0));
Ok(unsafe { DisplayHandle::borrow_raw(raw) })
}
}
fn win(n: u64) -> TeksiloWindowId {
TeksiloWindowId::new(n)
}
#[test]
fn handle_attaches_and_detaches() {
let backend = MemoryExternalDndBackend::new();
let handle = ExternalDndHandle::new(backend.clone());
let cap = CapturingPoster::new();
let poster: Arc<dyn AppEventPoster> = cap.clone();
handle.attach(win(1), fake_parent(), poster.clone());
assert_eq!(handle.attached_count(), 1);
assert_eq!(backend.attached_windows(), vec![win(1)]);
handle.detach(win(1));
assert_eq!(handle.attached_count(), 0);
assert!(backend.attached_windows().is_empty());
}
#[test]
fn reattach_replaces_previous_guard() {
let backend = MemoryExternalDndBackend::new();
let handle = ExternalDndHandle::new(backend.clone());
let cap = CapturingPoster::new();
let poster: Arc<dyn AppEventPoster> = cap.clone();
handle.attach(win(1), fake_parent(), poster.clone());
handle.attach(win(1), fake_parent(), poster.clone());
assert_eq!(handle.attached_count(), 1);
assert_eq!(backend.attached_windows(), vec![win(1)]);
}
#[test]
fn emit_posts_event_for_attached_window() {
let backend = MemoryExternalDndBackend::new();
let handle = ExternalDndHandle::new(backend.clone());
let cap = CapturingPoster::new();
let poster: Arc<dyn AppEventPoster> = cap.clone();
handle.attach(win(3), fake_parent(), poster);
let data = ExternalDropData {
files: vec![PathBuf::from("/tmp/a.png")],
..Default::default()
};
assert!(backend.emit(
win(3),
ExternalDragEvent::Dropped {
data,
position: Point::new(12.0, 34.0),
},
));
let mut posted = cap.drain();
assert_eq!(posted.len(), 1);
let payload = *posted
.pop()
.unwrap()
.downcast::<ExternalDndEventPayload>()
.expect("payload type matches");
assert_eq!(payload.window_id_owner, win(3));
match payload.event {
ExternalDragEvent::Dropped { data, position } => {
assert_eq!(data.files, vec![PathBuf::from("/tmp/a.png")]);
assert!((position.x - 12.0).abs() < 0.01 && (position.y - 34.0).abs() < 0.01);
}
other => panic!("unexpected event: {other:?}"),
}
}
#[test]
fn emit_for_unattached_window_is_noop() {
let backend = MemoryExternalDndBackend::new();
let _handle = ExternalDndHandle::new(backend.clone());
assert!(!backend.emit(win(99), ExternalDragEvent::Left));
}
#[test]
fn noop_backend_attaches_without_emitting() {
let handle = ExternalDndHandle::new(NoopExternalDndBackend::new());
let cap = CapturingPoster::new();
let poster: Arc<dyn AppEventPoster> = cap.clone();
handle.attach(win(1), fake_parent(), poster);
assert_eq!(handle.attached_count(), 1);
handle.detach(win(1));
assert_eq!(handle.attached_count(), 0);
assert!(cap.drain().is_empty());
}
}