Skip to main content

inset_embedder/
drag_drop.rs

1//! What is dragged from outside the application over a view: the same items a paste hands over.
2//!
3//! No dart:ui counterpart: Flutter leaves external drops to plugins. A report is a drag in
4//! `PointerReport`'s terms, a view, a phase and a physical position, carrying what is dragged in
5//! the one model every system has for paste and for drag and drop alike: macOS's dragging
6//! pasteboard is an `NSPasteboard`, Windows' drop hands an `IDataObject`, Wayland's drag a data
7//! offer, Android's `DragEvent` a `ClipData`, the web's `DragEvent` a `DataTransfer`. So a drag
8//! carries [`DataTransferItem`]s, as a paste does, a file among them as a pasted file is: offered
9//! as [`URI_LIST`](crate::URI_LIST).
10//!
11//! The invariant: a drag passing over the app does not hand it what is dragged. As in the HTML
12//! spec's drag data store, protected until the drop and read-only during it, an item's types are
13//! seen from [`DragChange::Entered`] on, and its content is read only while the drop is reported:
14//! a read asked for before fails with [`ClipboardError::Denied`], one asked for after with
15//! [`ClipboardError::Changed`], as every system lets a drop's data go once its drop is handled;
16//! one asked for during the report may complete later. A host hands out the items of a
17//! [`DraggedItems`], which holds them to it.
18
19use std::cell::Cell;
20use std::fmt;
21use std::rc::Rc;
22
23use crate::{ClipboardError, ClipboardReadFuture, DataTransferItem, Offset, ViewId};
24
25/// One report of an external drag over a view.
26#[derive(Clone)]
27pub struct DragReport {
28    /// The [`crate::View`] the drag is over.
29    pub view_id: ViewId,
30    /// What happened since the last report.
31    pub change: DragChange,
32    /// Where over the view, in physical pixels, when the host knows: a host may report no
33    /// position at all, and none reports one on [`DragChange::Exited`].
34    pub physical_position: Option<Offset>,
35    /// What is dragged, as a paste reads it: each item's types, each read when asked, while the
36    /// drop is reported. The same items in every report of one drag; a file is an item as a
37    /// pasted file is.
38    pub items: Vec<Rc<dyn DataTransferItem>>,
39}
40
41impl fmt::Debug for DragReport {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        let types: Vec<&[String]> = self.items.iter().map(|item| item.types()).collect();
44        f.debug_struct("DragReport")
45            .field("view_id", &self.view_id)
46            .field("change", &self.change)
47            .field("physical_position", &self.physical_position)
48            .field("items", &types)
49            .finish()
50    }
51}
52
53/// The four phases of an external drag, as every toolkit has them.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum DragChange {
56    /// The drag came over the view.
57    Entered,
58    /// The drag moved over the view.
59    Moved,
60    /// What is dragged was let go over the view: the one report its items are read in.
61    Dropped,
62    /// The drag left the view without dropping.
63    Exited,
64}
65
66/// The items of one drag as a host hands them out, readable only while their drop is reported
67/// ([`DraggedItems::report_drop`]).
68pub struct DraggedItems {
69    reading: Rc<Cell<Reading>>,
70    items: Vec<Rc<dyn DataTransferItem>>,
71}
72
73/// Whether a drag's items may be read.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75enum Reading {
76    /// The drag is passing over: nothing is read.
77    NotDropped,
78    /// Its drop is being reported: what is asked for is read.
79    Dropping,
80    /// Its drop was reported, and the system has let its data go.
81    Over,
82}
83
84impl DraggedItems {
85    /// `items` as the system lists them, each read only while their drop is reported.
86    pub fn new(items: Vec<Box<dyn DataTransferItem>>) -> DraggedItems {
87        let reading = Rc::new(Cell::new(Reading::NotDropped));
88        let items = items
89            .into_iter()
90            .map(|item| {
91                Rc::new(DraggedItem {
92                    item,
93                    reading: Rc::clone(&reading),
94                }) as Rc<dyn DataTransferItem>
95            })
96            .collect();
97        DraggedItems { reading, items }
98    }
99
100    /// Whether the drag carries nothing the app could read, which a host refuses.
101    pub fn is_empty(&self) -> bool {
102        self.items.is_empty()
103    }
104
105    /// The items, for a report.
106    pub fn items(&self) -> Vec<Rc<dyn DataTransferItem>> {
107        self.items.clone()
108    }
109
110    /// Runs `report`, which reports the drop: the items are read while it runs, and never after.
111    pub fn report_drop<R>(&self, report: impl FnOnce() -> R) -> R {
112        self.reading.set(Reading::Dropping);
113        let _over = ReadingEnds(&self.reading);
114        report()
115    }
116}
117
118/// Ends the reading of a drag's items as the report of its drop returns, or unwinds.
119struct ReadingEnds<'a>(&'a Cell<Reading>);
120
121impl Drop for ReadingEnds<'_> {
122    fn drop(&mut self) {
123        self.0.set(Reading::Over);
124    }
125}
126
127/// An item of a drag, read through only while its drop is reported.
128struct DraggedItem {
129    item: Box<dyn DataTransferItem>,
130    reading: Rc<Cell<Reading>>,
131}
132
133impl DataTransferItem for DraggedItem {
134    fn types(&self) -> &[String] {
135        self.item.types()
136    }
137
138    fn read(&self, mime: &str) -> ClipboardReadFuture {
139        match self.reading.get() {
140            Reading::NotDropped => Box::pin(std::future::ready(Err(ClipboardError::Denied))),
141            Reading::Dropping => self.item.read(mime),
142            Reading::Over => Box::pin(std::future::ready(Err(ClipboardError::Changed))),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use std::future::Future;
150    use std::pin::Pin;
151    use std::task::{Context, Poll, Waker};
152
153    use super::*;
154
155    /// Text, read in the call.
156    struct Text([String; 1]);
157
158    impl Text {
159        fn item() -> Box<dyn DataTransferItem> {
160            Box::new(Text(["text/plain".to_owned()]))
161        }
162    }
163
164    impl DataTransferItem for Text {
165        fn types(&self) -> &[String] {
166            &self.0
167        }
168
169        fn read(&self, _mime: &str) -> ClipboardReadFuture {
170            Box::pin(std::future::ready(Ok(b"hi".to_vec())))
171        }
172    }
173
174    fn now<T>(mut future: Pin<Box<dyn Future<Output = T>>>) -> T {
175        match future
176            .as_mut()
177            .poll(&mut Context::from_waker(Waker::noop()))
178        {
179            Poll::Ready(output) => output,
180            Poll::Pending => panic!("the item answers in the call"),
181        }
182    }
183
184    #[test]
185    fn an_item_shows_its_types_while_the_drag_passes_over_but_not_its_content() {
186        let dragged = DraggedItems::new(vec![Text::item()]);
187        let items = dragged.items();
188        assert_eq!(items[0].types(), ["text/plain"]);
189        assert_eq!(
190            now(items[0].read("text/plain")),
191            Err(ClipboardError::Denied)
192        );
193    }
194
195    #[test]
196    fn an_item_is_read_while_its_drop_is_reported_and_not_after() {
197        let dragged = DraggedItems::new(vec![Text::item()]);
198        let items = dragged.items();
199        let read = dragged.report_drop(|| now(items[0].read("text/plain")));
200        assert_eq!(read, Ok(b"hi".to_vec()));
201        assert_eq!(
202            now(items[0].read("text/plain")),
203            Err(ClipboardError::Changed)
204        );
205    }
206
207    #[test]
208    fn a_read_asked_for_during_the_drop_completes_after_it() {
209        let dragged = DraggedItems::new(vec![Text::item()]);
210        let items = dragged.items();
211        let read = dragged.report_drop(|| items[0].read("text/plain"));
212        assert_eq!(now(read), Ok(b"hi".to_vec()));
213    }
214
215    #[test]
216    fn a_drag_carrying_nothing_is_empty() {
217        assert!(DraggedItems::new(Vec::new()).is_empty());
218        assert!(!DraggedItems::new(vec![Text::item()]).is_empty());
219    }
220}