winit/platform_impl/linux/x11/
dnd.rs

1use std::{
2    io,
3    os::raw::*,
4    path::{Path, PathBuf},
5    str::Utf8Error,
6    sync::Arc,
7};
8
9use percent_encoding::percent_decode;
10
11use super::{ffi, util, XConnection, XError};
12
13#[derive(Debug)]
14pub struct DndAtoms {
15    pub aware: ffi::Atom,
16    pub enter: ffi::Atom,
17    pub leave: ffi::Atom,
18    pub drop: ffi::Atom,
19    pub position: ffi::Atom,
20    pub status: ffi::Atom,
21    pub action_private: ffi::Atom,
22    pub selection: ffi::Atom,
23    pub finished: ffi::Atom,
24    pub type_list: ffi::Atom,
25    pub uri_list: ffi::Atom,
26    pub none: ffi::Atom,
27}
28
29impl DndAtoms {
30    pub fn new(xconn: &Arc<XConnection>) -> Result<Self, XError> {
31        let names = [
32            b"XdndAware\0".as_ptr() as *mut c_char,
33            b"XdndEnter\0".as_ptr() as *mut c_char,
34            b"XdndLeave\0".as_ptr() as *mut c_char,
35            b"XdndDrop\0".as_ptr() as *mut c_char,
36            b"XdndPosition\0".as_ptr() as *mut c_char,
37            b"XdndStatus\0".as_ptr() as *mut c_char,
38            b"XdndActionPrivate\0".as_ptr() as *mut c_char,
39            b"XdndSelection\0".as_ptr() as *mut c_char,
40            b"XdndFinished\0".as_ptr() as *mut c_char,
41            b"XdndTypeList\0".as_ptr() as *mut c_char,
42            b"text/uri-list\0".as_ptr() as *mut c_char,
43            b"None\0".as_ptr() as *mut c_char,
44        ];
45        let atoms = unsafe { xconn.get_atoms(&names) }?;
46        Ok(DndAtoms {
47            aware: atoms[0],
48            enter: atoms[1],
49            leave: atoms[2],
50            drop: atoms[3],
51            position: atoms[4],
52            status: atoms[5],
53            action_private: atoms[6],
54            selection: atoms[7],
55            finished: atoms[8],
56            type_list: atoms[9],
57            uri_list: atoms[10],
58            none: atoms[11],
59        })
60    }
61}
62
63#[derive(Debug, Clone, Copy)]
64pub enum DndState {
65    Accepted,
66    Rejected,
67}
68
69#[derive(Debug)]
70pub enum DndDataParseError {
71    EmptyData,
72    InvalidUtf8(Utf8Error),
73    HostnameSpecified(String),
74    UnexpectedProtocol(String),
75    UnresolvablePath(io::Error),
76}
77
78impl From<Utf8Error> for DndDataParseError {
79    fn from(e: Utf8Error) -> Self {
80        DndDataParseError::InvalidUtf8(e)
81    }
82}
83
84impl From<io::Error> for DndDataParseError {
85    fn from(e: io::Error) -> Self {
86        DndDataParseError::UnresolvablePath(e)
87    }
88}
89
90pub struct Dnd {
91    xconn: Arc<XConnection>,
92    pub atoms: DndAtoms,
93    // Populated by XdndEnter event handler
94    pub version: Option<c_long>,
95    pub type_list: Option<Vec<c_ulong>>,
96    // Populated by XdndPosition event handler
97    pub source_window: Option<c_ulong>,
98    // Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
99    pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>,
100}
101
102impl Dnd {
103    pub fn new(xconn: Arc<XConnection>) -> Result<Self, XError> {
104        let atoms = DndAtoms::new(&xconn)?;
105        Ok(Dnd {
106            xconn,
107            atoms,
108            version: None,
109            type_list: None,
110            source_window: None,
111            result: None,
112        })
113    }
114
115    pub fn reset(&mut self) {
116        self.version = None;
117        self.type_list = None;
118        self.source_window = None;
119        self.result = None;
120    }
121
122    pub unsafe fn send_status(
123        &self,
124        this_window: c_ulong,
125        target_window: c_ulong,
126        state: DndState,
127    ) -> Result<(), XError> {
128        let (accepted, action) = match state {
129            DndState::Accepted => (1, self.atoms.action_private as c_long),
130            DndState::Rejected => (0, self.atoms.none as c_long),
131        };
132        self.xconn
133            .send_client_msg(
134                target_window,
135                target_window,
136                self.atoms.status,
137                None,
138                [this_window as c_long, accepted, 0, 0, action],
139            )
140            .flush()
141    }
142
143    pub unsafe fn send_finished(
144        &self,
145        this_window: c_ulong,
146        target_window: c_ulong,
147        state: DndState,
148    ) -> Result<(), XError> {
149        let (accepted, action) = match state {
150            DndState::Accepted => (1, self.atoms.action_private as c_long),
151            DndState::Rejected => (0, self.atoms.none as c_long),
152        };
153        self.xconn
154            .send_client_msg(
155                target_window,
156                target_window,
157                self.atoms.finished,
158                None,
159                [this_window as c_long, accepted, action, 0, 0],
160            )
161            .flush()
162    }
163
164    pub unsafe fn get_type_list(
165        &self,
166        source_window: c_ulong,
167    ) -> Result<Vec<ffi::Atom>, util::GetPropertyError> {
168        self.xconn
169            .get_property(source_window, self.atoms.type_list, ffi::XA_ATOM)
170    }
171
172    pub unsafe fn convert_selection(&self, window: c_ulong, time: c_ulong) {
173        (self.xconn.xlib.XConvertSelection)(
174            self.xconn.display,
175            self.atoms.selection,
176            self.atoms.uri_list,
177            self.atoms.selection,
178            window,
179            time,
180        );
181    }
182
183    pub unsafe fn read_data(
184        &self,
185        window: c_ulong,
186    ) -> Result<Vec<c_uchar>, util::GetPropertyError> {
187        self.xconn
188            .get_property(window, self.atoms.selection, self.atoms.uri_list)
189    }
190
191    pub fn parse_data(&self, data: &mut Vec<c_uchar>) -> Result<Vec<PathBuf>, DndDataParseError> {
192        if !data.is_empty() {
193            let mut path_list = Vec::new();
194            let decoded = percent_decode(data).decode_utf8()?.into_owned();
195            for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) {
196                // The format is specified as protocol://host/path
197                // However, it's typically simply protocol:///path
198                let path_str = if uri.starts_with("file://") {
199                    let path_str = uri.replace("file://", "");
200                    if !path_str.starts_with('/') {
201                        // A hostname is specified
202                        // Supporting this case is beyond the scope of my mental health
203                        return Err(DndDataParseError::HostnameSpecified(path_str));
204                    }
205                    path_str
206                } else {
207                    // Only the file protocol is supported
208                    return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned()));
209                };
210
211                let path = Path::new(&path_str).canonicalize()?;
212                path_list.push(path);
213            }
214            Ok(path_list)
215        } else {
216            Err(DndDataParseError::EmptyData)
217        }
218    }
219}