i_slint_core/data_transfer.rs
1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Types and helpers related to the [`DataTransfer`] type, which implements type-indexed arbitrary
5//! data transfer both within an application and between applications.
6
7use alloc::rc::Rc;
8use core::any::Any;
9
10#[cfg(feature = "std")]
11use crate::SharedVector;
12use crate::{SharedString, api::Image};
13
14#[cfg(feature = "ffi")]
15pub mod ffi;
16
17/// Hidden type to make `DataTransfer` smaller and easier to use in FFI.
18///
19/// In particular, `Image` is a different size depending on feature flags, so this
20/// allows `DataTransfer` to have a normalized size no matter what flags are enabled.
21#[derive(Default, Clone, PartialEq)]
22struct DataTransferInner {
23 // TODO: Custom binary data providers with custom MIME types.
24 /// Special-cased support for images, as the precise implementation of transferring
25 /// images differs between platforms.
26 image: Option<Image>,
27 /// Special-cased support for plain text, as the precise implementation of transferring
28 /// text differs between platforms.
29 plain_text: Option<SharedString>,
30 /// Special-cased support for files, as platforms transfer them as URLs or
31 /// native path lists. Stored as local filesystem paths.
32 #[cfg(feature = "std")]
33 file_paths: Option<SharedVector<std::path::PathBuf>>,
34}
35
36/// `DataTransfer` abstracts over the various ways of transferring data within an application
37/// and between applications.
38///
39/// The details will depend on the current platform, but the common features are:
40///
41/// - Each `DataTransfer` contains multiple views over the same data in different formats
42/// - The `DataTransfer` may contain an in-memory representation of the data, which can be
43/// sent and received within the current application
44/// - Serializing to/deserializing from a given format may be done eagerly or lazily[^lazy-note]
45///
46/// [^lazy-note]: Platforms differ on which formats can and cannot be lazy, but all support it in
47/// some capacity. Reading data from a `DataTransfer` cannot be assumed to be a cheap operation.
48///
49/// Currently, plain text, image data, and file paths are supported. Precisely how this maps
50/// to the backend will depend on platform and features. Work to expand this API is ongoing, see
51/// [the tracking issue for drag-and-drop][dnd-tracking-issue] to follow its progress.
52///
53/// [dnd-tracking-issue]: https://github.com/slint-ui/slint/issues/1967
54///
55/// The easiest way to construct this type is with the [`Default`] implementation, followed
56/// by [`set_plain_text`](DataTransfer::set_plain_text) or [`set_image`](DataTransfer::set_image).
57/// There are also implementations of [`From<SharedString>`](SharedString) and [`From<Image>`](Image)
58/// which construct a new `DataTransfer` using those methods respectively. The opposites of these
59/// operations are [`plain_text`](DataTransfer::plain_text) and
60/// [`image`](DataTransfer::image).
61///
62/// ```rust
63/// # use i_slint_core::{DataTransfer, string::ToSharedString as _};
64///
65/// let message = "Hello, world!";
66/// let data = DataTransfer::from(message.to_shared_string());
67/// assert_eq!(data.plain_text().unwrap(), message);
68/// ```
69#[derive(Clone, Default)]
70#[repr(C)]
71pub struct DataTransfer {
72 /// Special-cased types. `Option<Rc>` to prevent allocating if this `DataTransfer`
73 /// only contains `user_data`.
74 inner: Option<Rc<DataTransferInner>>,
75 /// A custom in-memory value. No MIME type-based dispatch is done here - if the user
76 /// wants to store one of a set of possible values, they should store their own enum
77 /// and handle the dispatch themselves.
78 user_data: Option<Rc<dyn Any>>,
79}
80
81impl core::fmt::Debug for DataTransfer {
82 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83 let mut s = f.debug_struct("DataTransfer");
84 s.field("has_plain_text", &self.has_plain_text()).field("has_image", &self.has_image());
85 #[cfg(feature = "std")]
86 s.field("has_file_paths", &self.has_file_paths());
87 s.field("has_user_data", &self.user_data.is_some());
88 s.finish()
89 }
90}
91
92// `PartialEq` doesn't really make sense for `DataTransfer`, but since it's required for values
93// that Slint interacts with, we can at least make a best-effort attempt. This will return true
94// if `other` is an unmodified clone of `self`, but if any modification has been done to either
95// value since cloning then this will return false even if the two values are semantically
96// identical.
97impl PartialEq for DataTransfer {
98 fn eq(&self, other: &Self) -> bool {
99 self.inner == other.inner
100 && self.user_data.as_ref().map(Rc::as_ptr) == other.user_data.as_ref().map(Rc::as_ptr)
101 }
102}
103
104impl From<SharedString> for DataTransfer {
105 fn from(value: SharedString) -> Self {
106 let mut out = DataTransfer::default();
107
108 out.set_plain_text(value);
109
110 out
111 }
112}
113
114impl From<Image> for DataTransfer {
115 fn from(value: Image) -> Self {
116 let mut out = DataTransfer::default();
117
118 out.set_image(value);
119
120 out
121 }
122}
123
124/// An error which can occur while fetching data from a `DataTransfer`.
125#[derive(Debug, Clone)]
126#[non_exhaustive]
127pub enum DataTransferError {
128 /// The type was not listed in the set of available MIME types.
129 TypeNotFound,
130}
131
132impl core::error::Error for DataTransferError {}
133
134impl core::fmt::Display for DataTransferError {
135 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136 match self {
137 Self::TypeNotFound => {
138 write!(f, "Type not supplied by data transfer")
139 }
140 }
141 }
142}
143
144impl DataTransfer {
145 /// Sets an image to be transferred by this [`DataTransfer`].
146 ///
147 /// The image can be read using [`image`](DataTransfer::image). If
148 /// you only need the [`DataTransfer`] to have an image representation, use
149 /// [`From<Image>`](Image).
150 ///
151 /// Each [`DataTransfer`] can only have a single image set at once. If this
152 /// method is called multiple times, the previous image will be overwritten.
153 /// However, you can have, for example, both an image representation and a
154 /// plain text representation set simultaneously on the same [`DataTransfer`].
155 ///
156 /// Passing a default-constructed `Image` clears the previously-set image
157 /// instead of storing it, so afterwards [`has_image`](DataTransfer::has_image)
158 /// returns `false`. If the resulting `DataTransfer` carries no plain text,
159 /// image, or user data, it compares equal to [`DataTransfer::default()`].
160 pub fn set_image(&mut self, image: Image) -> &mut Self {
161 if matches!(image.0, crate::ImageInner::None) {
162 self.clear_image();
163 } else {
164 Rc::make_mut(self.inner.get_or_insert_default()).image = Some(image);
165 }
166 self
167 }
168
169 /// Sets unstyled, basic text to be transferred by this [`DataTransfer`].
170 ///
171 /// The image can be read using [`plain_text`](DataTransfer::plain_text).
172 /// If you only need the [`DataTransfer`] to have a plain text representation,
173 /// use [`From<SharedString>`](SharedString).
174 ///
175 /// Each [`DataTransfer`] can only have a single plain text representation
176 /// set at once. If this method is called multiple times, the previous text
177 /// will be overwritten. However, you can have, for example, both an image
178 /// representation and a plain text representation set simultaneously on the
179 /// same [`DataTransfer`].
180 ///
181 /// Passing an empty string clears the previously-set plain text instead of
182 /// storing it, so afterwards [`has_plain_text`](DataTransfer::has_plain_text)
183 /// returns `false`. If the resulting `DataTransfer` carries no plain text,
184 /// image, or user data, it compares equal to [`DataTransfer::default()`].
185 pub fn set_plain_text(&mut self, plain_text: SharedString) -> &mut Self {
186 if plain_text.is_empty() {
187 self.clear_plain_text();
188 } else {
189 Rc::make_mut(self.inner.get_or_insert_default()).plain_text = Some(plain_text);
190 }
191 self
192 }
193
194 /// Sets a list of local file paths to be transferred by this [`DataTransfer`],
195 /// overwriting any previously set list. An empty list clears the file paths.
196 /// The paths can be read using [`file_paths`](DataTransfer::file_paths).
197 ///
198 /// ```rust
199 /// # use i_slint_core::DataTransfer;
200 /// let mut transfer = DataTransfer::default();
201 /// transfer.set_file_paths(["/home/user/a.txt", "/home/user/b.png"]);
202 /// ```
203 #[cfg(feature = "std")]
204 pub fn set_file_paths<I>(&mut self, file_paths: I) -> &mut Self
205 where
206 I: IntoIterator,
207 I::Item: AsRef<std::path::Path>,
208 {
209 self.set_file_paths_vec(
210 file_paths.into_iter().map(|path| path.as_ref().to_path_buf()).collect(),
211 );
212 self
213 }
214
215 /// `set_file_paths` on the internal representation, for the FFI layer.
216 #[cfg(feature = "std")]
217 fn set_file_paths_vec(&mut self, file_paths: SharedVector<std::path::PathBuf>) {
218 if file_paths.is_empty() {
219 self.clear_file_paths();
220 } else {
221 Rc::make_mut(self.inner.get_or_insert_default()).file_paths = Some(file_paths);
222 }
223 }
224
225 fn clear_image(&mut self) {
226 let Some(inner_rc) = self.inner.as_mut() else { return };
227 if inner_rc.image.is_some() {
228 Rc::make_mut(inner_rc).image = None;
229 }
230 self.release_empty_inner();
231 }
232
233 fn clear_plain_text(&mut self) {
234 let Some(inner_rc) = self.inner.as_mut() else { return };
235 if inner_rc.plain_text.is_some() {
236 Rc::make_mut(inner_rc).plain_text = None;
237 }
238 self.release_empty_inner();
239 }
240
241 #[cfg(feature = "std")]
242 fn clear_file_paths(&mut self) {
243 let Some(inner_rc) = self.inner.as_mut() else { return };
244 if inner_rc.file_paths.is_some() {
245 Rc::make_mut(inner_rc).file_paths = None;
246 }
247 self.release_empty_inner();
248 }
249
250 /// Drops the inner allocation when nothing is stored in it, so that a fully
251 /// cleared transfer compares equal to `DataTransfer::default()`.
252 fn release_empty_inner(&mut self) {
253 if self.inner.as_ref().is_some_and(|inner| **inner == DataTransferInner::default()) {
254 self.inner = None;
255 }
256 }
257
258 /// Whether this transfer carries data another application could receive: plain text,
259 /// an image or file paths. User data stays within the process, so it doesn't count.
260 ///
261 /// Reads the payload as a whole rather than testing each kind, so a payload added to
262 /// [`DataTransferInner`] later counts without touching the callers.
263 pub(crate) fn has_native_data(&self) -> bool {
264 self.inner.is_some()
265 }
266
267 /// Returns `true` if this data transfer advertises that it is readable as an [`Image`].
268 ///
269 /// This does not necessarily mean that `image` will return `Ok`, as an I/O error
270 /// may occur.
271 pub fn has_image(&self) -> bool {
272 self.inner.as_ref().is_some_and(|inner| inner.image.is_some())
273 }
274
275 /// Returns `true` if this data transfer advertises that it is readable as plain text.
276 ///
277 /// This does not necessarily mean that `plain_text` will return `Ok`, as an I/O
278 /// error may occur.
279 pub fn has_plain_text(&self) -> bool {
280 self.inner.as_ref().is_some_and(|inner| inner.plain_text.is_some())
281 }
282
283 /// Returns `true` if this data transfer advertises that it is readable as a list
284 /// of file paths.
285 ///
286 /// This does not necessarily mean that `file_paths` will return `Ok`, as an I/O
287 /// error may occur.
288 #[cfg(feature = "std")]
289 pub fn has_file_paths(&self) -> bool {
290 self.inner.as_ref().is_some_and(|inner| inner.file_paths.is_some())
291 }
292
293 /// Returns `true` if this data transfer carries no data: no plain text, no image,
294 /// no file paths, and no user data. A `DataTransfer` constructed via
295 /// [`Default::default`] is empty.
296 pub fn is_empty(&self) -> bool {
297 // `release_empty_inner` keeps `inner` at `None` whenever it stores nothing.
298 self.inner.is_none() && self.user_data.is_none()
299 }
300
301 /// Set the application-internal data represented by this [`DataTransfer`].
302 /// This can be read with [`DataTransfer::user_data`], and allows circumventing
303 /// serialize/deserializing the data to bytes when a drag-and-drop or copy-paste
304 /// operation stays within the application.
305 pub fn set_user_data(&mut self, value: Rc<dyn Any>) -> &mut Self {
306 self.user_data = Some(value);
307 self
308 }
309
310 /// Helper to read this [`DataTransfer`] as plain text, supporting multiple encodings.
311 ///
312 /// The caller should assume that this method call may do I/O.
313 pub fn plain_text(&self) -> Result<SharedString, DataTransferError> {
314 self.inner
315 .as_ref()
316 .and_then(|inner| inner.plain_text.clone())
317 .ok_or(DataTransferError::TypeNotFound)
318 }
319
320 /// Helper to read this [`DataTransfer`] as an image, supporting multiple image types.
321 ///
322 /// The caller should assume that this method call may do I/O.
323 pub fn image(&self) -> Result<Image, DataTransferError> {
324 self.inner
325 .as_ref()
326 .and_then(|inner| inner.image.clone())
327 .ok_or(DataTransferError::TypeNotFound)
328 }
329
330 /// Helper to read this [`DataTransfer`] as a list of local file paths.
331 /// The returned iterator borrows this `DataTransfer`.
332 ///
333 /// The caller should assume that this method call may do I/O.
334 #[cfg(feature = "std")]
335 pub fn file_paths(
336 &self,
337 ) -> Result<impl Iterator<Item = &std::path::Path> + '_, DataTransferError> {
338 self.inner
339 .as_ref()
340 .and_then(|inner| inner.file_paths.as_ref())
341 .map(|paths| paths.iter().map(std::path::PathBuf::as_path))
342 .ok_or(DataTransferError::TypeNotFound)
343 }
344
345 /// Get the application-internal data represented by this [`DataTransfer`], if
346 /// one exists.
347 pub fn user_data(&self) -> Option<Rc<dyn Any>> {
348 self.user_data.clone()
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use crate::graphics::{Rgba8Pixel, SharedPixelBuffer};
356
357 #[test]
358 fn set_plain_text_with_empty_string_clears() {
359 let mut dt = DataTransfer::default();
360 dt.set_plain_text("hello".into());
361 assert!(dt.has_plain_text());
362 dt.set_plain_text("".into());
363 assert!(!dt.has_plain_text());
364 assert!(dt.plain_text().is_err());
365 }
366
367 #[test]
368 fn set_image_with_default_image_clears() {
369 let mut dt = DataTransfer::default();
370 let buffer = SharedPixelBuffer::<Rgba8Pixel>::new(2, 2);
371 dt.set_image(Image::from_rgba8(buffer));
372 assert!(dt.has_image());
373 dt.set_image(Image::default());
374 assert!(!dt.has_image());
375 assert!(dt.image().is_err());
376 }
377
378 #[test]
379 fn set_plain_text_with_empty_string_on_default_stays_empty() {
380 let mut dt = DataTransfer::default();
381 dt.set_plain_text("".into());
382 assert!(!dt.has_plain_text());
383 assert!(dt.is_empty());
384 // The clear path on an already-default transfer must not allocate an
385 // inner Rc — otherwise it would compare unequal to `default()` even
386 // though both are observably empty.
387 assert!(dt.inner.is_none());
388 assert_eq!(dt, DataTransfer::default());
389 }
390
391 #[test]
392 fn set_image_with_default_image_on_default_stays_empty() {
393 let mut dt = DataTransfer::default();
394 dt.set_image(Image::default());
395 assert!(!dt.has_image());
396 assert!(dt.is_empty());
397 assert!(dt.inner.is_none());
398 assert_eq!(dt, DataTransfer::default());
399 }
400
401 #[test]
402 fn cleared_transfer_compares_equal_to_default() {
403 // After clearing every field, the inner Rc must be released so the
404 // transfer compares equal to a freshly-constructed default.
405 let mut dt = DataTransfer::default();
406 dt.set_plain_text("hello".into());
407 dt.set_image(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(2, 2)));
408 dt.set_file_paths(["/tmp/a"]);
409 assert!(!dt.is_empty());
410 dt.set_plain_text("".into());
411 assert!(dt.inner.is_some(), "image and files still set, inner must remain");
412 dt.set_image(Image::default());
413 assert!(dt.inner.is_some(), "files still set, inner must remain");
414 dt.set_file_paths(core::iter::empty::<&str>());
415 assert!(dt.is_empty());
416 assert!(dt.inner.is_none());
417 assert_eq!(dt, DataTransfer::default());
418 }
419
420 #[test]
421 fn every_payload_counts_as_native_data() {
422 assert!(!DataTransfer::default().has_native_data());
423
424 for set in [
425 (|dt: &mut DataTransfer| {
426 dt.set_plain_text("hello".into());
427 }) as fn(&mut DataTransfer),
428 |dt| {
429 dt.set_image(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(2, 2)));
430 },
431 |dt| {
432 dt.set_file_paths(["/tmp/a"]);
433 },
434 ] {
435 let mut dt = DataTransfer::default();
436 set(&mut dt);
437 assert!(dt.has_native_data());
438 }
439 }
440
441 #[test]
442 fn set_file_paths_with_empty_list_clears() {
443 use std::path::Path;
444 use std::vec::Vec;
445 let mut dt = DataTransfer::default();
446 dt.set_file_paths(["/tmp/a", "/tmp/b"]);
447 assert!(dt.has_file_paths());
448 assert_eq!(
449 dt.file_paths().unwrap().collect::<Vec<_>>(),
450 [Path::new("/tmp/a"), Path::new("/tmp/b")]
451 );
452 dt.set_file_paths(core::iter::empty::<&str>());
453 assert!(!dt.has_file_paths());
454 assert!(dt.file_paths().is_err());
455 assert!(dt.inner.is_none());
456 assert_eq!(dt, DataTransfer::default());
457 }
458}