Skip to main content

plat/
dnd.rs

1// dnd -- drag and drop between programs: what a drag carries, the target hook, the pending
2// drop and the app's own outgoing drag (the parts every OS shares)       (docs/GUIDE.md ยง7)
3//
4//   DT_FILES / DT_TEXT / DT_URL / DT_HTML / DT_IMAGE   kinds of content (bits, DT_STD = all);
5//                                DT_USER0 << k: the app's own kinds (internal drags only)
6//   DE_COPY / DE_MOVE / DE_LINK  effects (bits)
7//   DragData                     the content of one drag (paths, text, URLs, HTML, an image, the
8//                                app's own bytes) and the effects
9//   drag_hook(Some(f))           f(pw, x, y, types) -> (accepted types, accepted effects): what the
10//                                UI takes at a point (px) of platform window pw.  The OS layers
11//                                ask it synchronously while something is dragged over a window
12//                                (IDropTarget::DragOver, XdndPosition, wl_data_device.motion).
13//   drag_query(pw, x, y, types, allowed, mods) -> (types, effect)   (OS layers) the hook's answer
14//                                with one effect picked; without a hook: files, copy (as before).
15//                                Files are never moved: a source deletes what it moved, and
16//                                the app gets only the paths (copy or link instead).  The hook
17//                                answers per drop target (ui: drop_effect_set): a target that
18//                                takes no effect refuses the drag there (the system's pointer
19//                                shows it)
20//   drag_query_all(...) -> (types, effect, effects)   drag_query and every effect the target
21//                                takes there (Wayland's set_actions names them all)
22//   drag_decide(types, effects, offered, allowed, mods) -> (types, effect, effects)   what
23//                                drag_query makes of a hook's answer (tests ask it headless)
24//   drag_effect_pick(allowed, mods) -> effect   Ctrl copy, Shift move, Ctrl+Shift link; else
25//                                the first of copy / move / link the mask allows
26//   drop_data_take() -> DragData the pending drop of E_DROP: the paths of drop_take and the rest
27//   drop_hold(on)                (the UI) the program answers drops itself: where the platform
28//                                allows a later answer, a drop's answer to its source waits after
29//                                E_DROP for drop_finish (Windows: a source whose data object is in
30//                                IDataObjectAsyncCapability's async mode; X11: XdndFinished;
31//                                Wayland: wl_data_offer.finish); elsewhere it went at the drop
32//   drop_waiting() -> serial     the drop whose answer waits (0: none), read after drop_data_take
33//   drop_allowed() -> DE_*       the effects the source of that drop allows, as the platform gives
34//                                them with the drop (0: it does not say), read after drop_data_take
35//   drop_finish(serial, effect) -> answered   answer that drop's source: the effect it was taken
36//                                with, 0 = not taken
37//   drop_set_text / drop_set_html / drop_add_url / drop_set_image / drop_set_effect /
38//   drop_set_allowed             (OS layers) fill the pending drop (drop_reset empties it)
39//   drop_text_from(bytes, utf16) / drop_url_list(text)   decoding helpers of the OS layers
40//   drag_start(data) -> bool     drag data out of the app while the left button is held (the UI
41//                                calls it when an internal drag leaves its window); false when
42//                                the platform cannot (headless, browser) or a drag runs.
43//                                E_DRAG_END follows (data = the effect, 0 = cancelled / refused).
44//   drag_running() -> bool       an outgoing drag runs
45//
46// Events: E_DRAG (x, y = position in window px, data = DT_* offered | DE_* allowed << 8; data 0:
47// the drag left the window, was dropped or cancelled) while another program's drag (or our
48// own outgoing one) is over a window; E_DROP (x, y, data = the number of paths) when it is
49// dropped there; E_DRAG_END when our own outgoing drag ended.
50// Windows: OLE (win_dnd.rs: IDropTarget per window, DoDragDrop with our IDataObject / IDropSource);
51// X11: Xdnd 5 target and source (linux_x11_dnd.rs); Wayland: wl_data_device (linux_wl_clip.rs).
52// Everything that comes from another program is bounded (sizes, counts) and checked.
53
54use alloc::string::String;
55use alloc::vec::Vec;
56use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
57
58use rt::sync::SpinLock;
59
60// ---- kinds of content (bits)
61pub const DT_FILES: u32 = 1; // paths (file:// URIs, CF_HDROP)
62pub const DT_TEXT: u32 = 2; // UTF-8 text
63pub const DT_URL: u32 = 4; // URLs that are not local files
64pub const DT_HTML: u32 = 8; // a UTF-8 HTML fragment
65pub const DT_IMAGE: u32 = 16; // an image file (PNG; a DIB arrives as BMP)
66pub const DT_STD: u32 = 31; // every kind other programs know
67pub const DT_USER0: u32 = 1 << 8; // the app's own kinds: DT_USER0 << k, k = 0..23
68
69// ---- effects (bits)
70pub const DE_COPY: u32 = 1; // the target copies (DROPEFFECT_COPY, XdndActionCopy)
71pub const DE_MOVE: u32 = 2; // the target moves: the source deletes its original
72pub const DE_LINK: u32 = 4; // the target links to it (not on Wayland)
73pub const DE_ALL: u32 = 7; // every effect
74
75const TEXT_MAX: usize = crate::CLIP_RICH_MAX; // text / HTML of one drop
76const IMAGE_MAX: usize = 1 << 28; // one image file
77const URL_MAX: usize = 32768; // bytes of one URL
78const URLS_MAX: usize = 4096;
79const URL_BYTES_MAX: usize = 1 << 24;
80
81/// the content of one drag (a drop that arrives, or what the app drags)
82#[derive(Clone, Default, Debug, PartialEq)]
83pub struct DragData {
84    /// paths of files and folders
85    pub files: Vec<String>,
86    /// plain text
87    pub text: Option<String>,
88    /// URLs (not local files)
89    pub urls: Vec<String>,
90    /// an HTML fragment (UTF-8)
91    pub html: Option<String>,
92    /// an image file: PNG (a drop of a DIB / image/bmp arrives as a BMP file)
93    pub image: Option<Vec<u8>>,
94    /// (drag out) the same image as a BMP file, offered as DIB / image/bmp too (optional)
95    pub bmp: Option<Vec<u8>>,
96    /// the app's own kind (DT_USER0 << k bits) and bytes: internal drags only
97    pub app: u32,
98    pub bytes: Vec<u8>,
99    /// what the drag shows next to the pointer inside the app (empty: from the content)
100    pub label: String,
101    /// drag out: the DE_* effects the source allows (0 = copy | move); a drop: the effect chosen
102    pub effects: u32,
103}
104
105impl DragData {
106    /// the DT_* kinds it carries (the app's own kinds included)
107    pub fn types(&self) -> u32 {
108        let mut t = self.app & !DT_STD;
109        if !self.files.is_empty() {
110            t |= DT_FILES;
111        }
112        if self.text.is_some() {
113            t |= DT_TEXT;
114        }
115        if !self.urls.is_empty() {
116            t |= DT_URL;
117        }
118        if self.html.is_some() {
119            t |= DT_HTML;
120        }
121        if self.image.is_some() {
122            t |= DT_IMAGE;
123        }
124        t
125    }
126
127    /// nothing to drag
128    pub fn is_empty(&self) -> bool {
129        self.types() == 0
130    }
131
132    /// text only
133    pub fn of_text(s: &str) -> DragData {
134        DragData { text: Some(String::from(s)), ..DragData::default() }
135    }
136
137    /// paths only
138    pub fn of_files(paths: &[&str]) -> DragData {
139        DragData { files: paths.iter().map(|p| String::from(*p)).collect(), ..DragData::default() }
140    }
141
142    /// the app's own kind (DT_USER0 << k) and bytes
143    pub fn of_app(kind: u32, bytes: &[u8]) -> DragData {
144        DragData { app: kind & !DT_STD, bytes: bytes.to_vec(), ..DragData::default() }
145    }
146
147    /// the effects a drag out allows (0 means copy | move)
148    pub fn allowed(&self) -> u32 {
149        if self.effects & DE_ALL == 0 { DE_COPY | DE_MOVE } else { self.effects & DE_ALL }
150    }
151}
152
153// ---------------------------------------------------------------- the target hook
154
155/// what the UI takes at a point: f(platform window, x, y px, offered DT_*) -> (accepted DT_*,
156/// accepted DE_*)
157pub type DragHook = fn(i32, i32, i32, u32) -> (u32, u32);
158
159static HOOK: AtomicUsize = AtomicUsize::new(0);
160
161/// install (Some) or remove (None) the drop target hook (the UI does it for its windows)
162pub fn drag_hook(f: Option<DragHook>) {
163    HOOK.store(f.map_or(0, |f| f as usize), Ordering::Release);
164}
165
166/// the effect of a drop: the modifier keys choose (Ctrl copy, Shift move, Ctrl+Shift link),
167/// else the first of copy / move / link that `allowed` has (0: none)
168pub fn drag_effect_pick(allowed: u32, mods: u8) -> u32 {
169    let a = allowed & DE_ALL;
170    let ctrl = mods & crate::MOD_CTRL != 0;
171    let shift = mods & crate::MOD_SHIFT != 0;
172    let want = match (ctrl, shift) {
173        (true, true) => DE_LINK,
174        (true, false) => DE_COPY,
175        (false, true) => DE_MOVE,
176        _ => 0,
177    };
178    if want & a != 0 {
179        return want;
180    }
181    [DE_COPY, DE_MOVE, DE_LINK].into_iter().find(|e| a & e != 0).unwrap_or(0)
182}
183
184/// (OS layers) what platform window pw takes at (x, y) px of a drag offering `types` with the
185/// effects `allowed` (0 = copy | move) -> (accepted types, the effect); (0, 0) = refused
186pub fn drag_query(pw: i32, x: i32, y: i32, types: u32, allowed: u32, mods: u8) -> (u32, u32) {
187    let (t, e, _) = drag_query_all(pw, x, y, types, allowed, mods);
188    (t, e)
189}
190
191/// (OS layers) drag_query and every DE_* effect the target there takes of the allowed ones
192/// (Wayland's wl_data_offer.set_actions) -> (accepted types, the effect, the effects); (0, 0, 0)
193/// = refused
194pub fn drag_query_all(pw: i32, x: i32, y: i32, types: u32, allowed: u32, mods: u8) -> (u32, u32, u32) {
195    let h = HOOK.load(Ordering::Acquire);
196    let (t, e) = if h == 0 {
197        (types & DT_FILES, DE_ALL) // no UI: files, as before
198    } else {
199        // SAFETY: HOOK only ever holds 0 or a DragHook stored by drag_hook.
200        let f: DragHook = unsafe { core::mem::transmute::<usize, DragHook>(h) };
201        f(pw, x, y, types)
202    };
203    drag_decide(t, e, types, allowed, mods)
204}
205
206/// what drag_query makes of the hook's answer (accepted types t, effects e) to a drag offering
207/// `types` with the effects `allowed` (0 = copy | move) and the modifier keys `mods` -> (accepted
208/// types, the effect, every effect taken); (0, 0, 0) = refused (no kind or no effect in common).
209/// Files are never moved.  For the OS layers and headless tests (ui's `dragover expect`)
210pub fn drag_decide(t: u32, e: u32, types: u32, allowed: u32, mods: u8) -> (u32, u32, u32) {
211    let t = t & types;
212    let mut allowed = if allowed & DE_ALL == 0 { DE_COPY | DE_MOVE } else { allowed & DE_ALL };
213    if t & DT_FILES != 0 {
214        // a move would make the source delete the files: the app gets only their paths
215        allowed &= !DE_MOVE;
216    }
217    let all = e & allowed;
218    let eff = drag_effect_pick(all, mods);
219    if t == 0 || eff == 0 { (0, 0, 0) } else { (t, eff, all) }
220}
221
222// ---------------------------------------------------------------- the pending drop
223
224struct Rich {
225    text: Option<String>,
226    html: Option<String>,
227    urls: Vec<String>,
228    url_bytes: usize,
229    image: Option<Vec<u8>>,
230    effect: u32,
231    allowed: u32,
232}
233
234static RICH: SpinLock<Rich> =
235    SpinLock::new(Rich { text: None, html: None, urls: Vec::new(), url_bytes: 0, image: None, effect: 0, allowed: 0 });
236
237// the effects the source of the drop drop_data_take took last allows (drop_allowed)
238static TAKEN_ALLOWED: AtomicU32 = AtomicU32::new(0);
239
240/// (drop_reset) forget the pending text, HTML, URLs and image
241pub(crate) fn rich_reset() {
242    let mut r = RICH.lock();
243    r.text = None;
244    r.html = None;
245    r.urls = Vec::new();
246    r.url_bytes = 0;
247    r.image = None;
248    r.effect = 0;
249    r.allowed = 0;
250}
251
252// text of another program: UTF-8 (invalid bytes replaced), up to the first NUL, bounded
253fn text_of(bytes: &[u8]) -> Option<String> {
254    let end = bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len());
255    if end > TEXT_MAX {
256        return None;
257    }
258    Some(String::from_utf8_lossy(&bytes[..end]).into_owned())
259}
260
261/// (OS layers) the pending drop's text (UTF-8 bytes; cut at a NUL) -> false when too large
262pub fn drop_set_text(bytes: &[u8]) -> bool {
263    let Some(s) = text_of(bytes) else { return false };
264    RICH.lock().text = Some(s);
265    true
266}
267
268/// (OS layers) the pending drop's HTML fragment (UTF-8 bytes) -> false when too large
269pub fn drop_set_html(bytes: &[u8]) -> bool {
270    let Some(s) = text_of(bytes) else { return false };
271    RICH.lock().html = Some(s);
272    true
273}
274
275/// (OS layers) one URL of the pending drop ("scheme:...", no blanks or control characters,
276/// bounded, no duplicates) -> added
277pub fn drop_add_url(url: &str) -> bool {
278    let url = url.trim();
279    if url.is_empty() || url.len() > URL_MAX || url.chars().any(|c| c.is_control() || c.is_whitespace()) || !has_scheme(url) {
280        return false;
281    }
282    let mut r = RICH.lock();
283    if r.urls.len() >= URLS_MAX || r.url_bytes + url.len() > URL_BYTES_MAX || r.urls.iter().any(|u| u == url) {
284        return false;
285    }
286    r.url_bytes += url.len();
287    r.urls.push(String::from(url));
288    true
289}
290
291/// (OS layers) the pending drop's image file (PNG or BMP bytes) -> false when too large
292pub fn drop_set_image(bytes: Vec<u8>) -> bool {
293    if bytes.is_empty() || bytes.len() > IMAGE_MAX {
294        return false;
295    }
296    RICH.lock().image = Some(bytes);
297    true
298}
299
300/// (OS layers) the effect the drop was made with (DE_*)
301pub fn drop_set_effect(effect: u32) {
302    RICH.lock().effect = effect & DE_ALL;
303}
304
305/// (OS layers) the DE_* effects the source of the pending drop allows, as the platform gives them
306/// with the drop: Windows IDropTarget::Drop's pdwEffect, X11 the source's XdndActionList with the
307/// action of its last XdndPosition (every effect without a list), Wayland the offer's
308/// source_actions, macOS the drag's operation mask.  Not called (the browser, Android): 0
309pub fn drop_set_allowed(effects: u32) {
310    RICH.lock().allowed = effects & DE_ALL;
311}
312
313/// the pending drop (E_DROP): its paths (drop_take) and the rest; both start empty again
314pub fn drop_data_take() -> DragData {
315    let files = crate::drop_take();
316    let mut r = RICH.lock();
317    let d = DragData {
318        files,
319        text: r.text.take(),
320        urls: core::mem::take(&mut r.urls),
321        html: r.html.take(),
322        image: r.image.take(),
323        effects: core::mem::take(&mut r.effect),
324        ..DragData::default()
325    };
326    r.url_bytes = 0;
327    TAKEN_ALLOWED.store(core::mem::take(&mut r.allowed), Ordering::Release);
328    d
329}
330
331/// drop_allowed() -> the DE_* effects the source of the drop drop_data_take took last allows, as
332/// the platform gave them with the drop (drop_set_allowed: the same the drag said in E_DRAG over
333/// the window); 0: the platform does not say (the browser, Android).  Read it right after
334/// drop_data_take
335pub fn drop_allowed() -> u32 {
336    TAKEN_ALLOWED.load(Ordering::Acquire)
337}
338
339// "scheme:" in front (RFC 3986: a letter, then letters, digits, + - .)
340fn has_scheme(u: &str) -> bool {
341    let b = u.as_bytes();
342    let Some(colon) = b.iter().position(|&c| c == b':') else { return false };
343    colon > 0
344        && b[0].is_ascii_alphabetic()
345        && b[..colon].iter().all(|&c| c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.'))
346}
347
348/// (OS layers) text in other programs' encodings -> UTF-8 bytes: UTF-16 LE (utf16, up to a
349/// NUL unit) or bytes as they are (up to a NUL)
350pub fn drop_text_from(bytes: &[u8], utf16: bool) -> Vec<u8> {
351    if !utf16 {
352        let end = bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len());
353        return bytes[..end].to_vec();
354    }
355    let units = bytes.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).take_while(|&u| u != 0);
356    let mut s = String::new();
357    for c in core::char::decode_utf16(units) {
358        s.push(c.unwrap_or('\u{FFFD}'));
359    }
360    s.into_bytes()
361}
362
363/// (OS layers) the lines of a URL list (text/uri-list, text/x-moz-url, UniformResourceLocator):
364/// '#' lines skipped, every other line one URL; local files are left to drop_uri_list -> URLs
365/// added (also used for the URL a browser drags)
366pub fn drop_url_list(text: &[u8]) -> u32 {
367    let mut n = 0;
368    for line in text.split(|&c| c == b'\n') {
369        let l = line.strip_suffix(b"\r").unwrap_or(line);
370        if l.is_empty() || l[0] == b'#' || (l.len() >= 5 && l[..5].eq_ignore_ascii_case(b"file:")) {
371            continue;
372        }
373        if let Ok(s) = core::str::from_utf8(l) {
374            if drop_add_url(s) {
375                n += 1;
376            }
377        }
378    }
379    n
380}
381
382// ---------------------------------------------------------------- the answer of a drop
383
384// drop_hold is on; the serial of the drop whose answer waits (0: none) and the OS layer's function
385// that gives it; the last serial handed out
386static HOLD: AtomicBool = AtomicBool::new(false);
387static WAITING: AtomicU32 = AtomicU32::new(0);
388static FINISH: AtomicUsize = AtomicUsize::new(0);
389static SERIAL: AtomicU32 = AtomicU32::new(0);
390
391/// drop_hold(on): (the UI, at its start) the program answers drops itself.  On, the OS layers keep
392/// a drop's answer to its source open after E_DROP where the platform allows a later answer, until
393/// drop_finish: Windows when the source's data object offers IDataObjectAsyncCapability in async
394/// mode (StartOperation in IDropTarget::Drop, EndOperation later), X11 (XdndFinished), Wayland
395/// (wl_data_offer.finish); everywhere else, and on Windows for every other source, the answer went
396/// at the drop (drop_waiting says 0).  Off (the default): every drop is answered at once, as before
397pub fn drop_hold(on: bool) {
398    HOLD.store(on, Ordering::Release);
399}
400
401/// (OS layers) drop_hold is on
402#[cfg_attr(any(target_arch = "wasm32", target_os = "android", target_vendor = "apple"), allow(dead_code))]
403pub(crate) fn drop_held() -> bool {
404    HOLD.load(Ordering::Acquire)
405}
406
407/// drop_waiting() -> the serial of the last drop whose answer to its source waits for drop_finish;
408/// 0: none (it was answered at the drop, or no drop came).  Read it right after drop_data_take
409pub fn drop_waiting() -> u32 {
410    WAITING.load(Ordering::Acquire)
411}
412
413/// drop_finish(serial, effect) -> answered: the source of drop `serial` (drop_waiting) learns how
414/// the drop ended: the DE_* effect it was taken with, 0 = not taken.  false: that drop does not
415/// wait (answered already, or a newer drop replaced it).  Windows: EndOperation(S_OK, effect),
416/// not taken EndOperation(E_ABORT, DROPEFFECT_NONE); X11: XdndFinished with "accepted" and the
417/// effect's action; Wayland: taken wl_data_offer.finish (the action stays the one negotiated
418/// before the drop), not taken the offer destroyed without finish
419pub fn drop_finish(serial: u32, effect: u32) -> bool {
420    if serial == 0 || WAITING.compare_exchange(serial, 0, Ordering::AcqRel, Ordering::Acquire).is_err() {
421        return false;
422    }
423    let f = FINISH.swap(0, Ordering::AcqRel);
424    if f != 0 {
425        // SAFETY: FINISH only ever holds 0 or a fn(u32) stored by drop_wait_begin.
426        let f: fn(u32) = unsafe { core::mem::transmute::<usize, fn(u32)>(f) };
427        f(effect & DE_ALL);
428    }
429    true
430}
431
432/// (OS layers) the drop about to be pushed keeps its answer open until finish(effect) is called
433/// through drop_finish -> its serial.  A drop that still waited is no longer waited for: the layer
434/// answers it itself before (as at its drop)
435#[cfg_attr(any(target_arch = "wasm32", target_os = "android", target_vendor = "apple"), allow(dead_code))]
436pub(crate) fn drop_wait_begin(finish: fn(u32)) -> u32 {
437    let mut s = SERIAL.fetch_add(1, Ordering::AcqRel).wrapping_add(1);
438    if s == 0 {
439        s = SERIAL.fetch_add(1, Ordering::AcqRel).wrapping_add(1);
440    }
441    FINISH.store(finish as usize, Ordering::Release);
442    WAITING.store(s, Ordering::Release);
443    s
444}
445
446/// (OS layers) the drop that waited was answered by the layer itself (its window went away)
447#[cfg_attr(any(target_arch = "wasm32", target_os = "android", target_vendor = "apple"), allow(dead_code))]
448pub(crate) fn drop_wait_clear() {
449    WAITING.store(0, Ordering::Release);
450    FINISH.store(0, Ordering::Release);
451}
452
453// ---------------------------------------------------------------- the outgoing drag
454
455static RUNNING: AtomicBool = AtomicBool::new(false);
456
457/// drag `data` out of the app (call while the left button is held) -> started; E_DRAG_END
458/// (data = the effect, 0 = cancelled or refused) follows.  Only the standard kinds leave
459/// the app.
460pub fn drag_start(data: DragData) -> bool {
461    if data.types() & DT_STD == 0 || RUNNING.load(Ordering::Acquire) {
462        return false;
463    }
464    RUNNING.store(true, Ordering::Release);
465    let ok = crate::os_drag_start(data);
466    if !ok {
467        RUNNING.store(false, Ordering::Release);
468    }
469    ok
470}
471
472/// an outgoing drag runs (until its E_DRAG_END is queued)
473pub fn drag_running() -> bool {
474    RUNNING.load(Ordering::Acquire)
475}
476
477/// (OS layers) the outgoing drag ended: E_DRAG_END for window w with the effect (0 = none)
478#[cfg_attr(any(target_arch = "wasm32", target_os = "android"), allow(dead_code))] // no drags out there yet
479pub(crate) fn drag_finished(w: u8, effect: u32) {
480    RUNNING.store(false, Ordering::Release);
481    crate::evq_push(crate::Event { typ: crate::E_DRAG_END, win: w, data: effect & DE_ALL, ..crate::Event::default() });
482}