Skip to main content

incular_platform/
data_transfer.rs

1//! Shared clipboard and external data-transfer model.
2//!
3//! A transfer item may advertise several representations of the same logical
4//! value (for example plain text and HTML). Payloads are reference counted and
5//! lazy providers are materialized at most once, so large data is never cloned
6//! merely because a transfer crosses the platform/runtime/widget boundaries.
7
8use crate::CapabilitySupport;
9use incular_core::Offset;
10use std::{
11    collections::HashSet,
12    fmt,
13    path::PathBuf,
14    sync::{Arc, OnceLock},
15};
16
17/// Stable identifier for an application-defined transfer representation.
18///
19/// MIME types (`application/json`) and UTI-like identifiers
20/// (`com.example.document`) are both accepted. Incular deliberately does not
21/// reinterpret or normalize application-defined identifiers.
22#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct MediaType(String);
24
25impl MediaType {
26    pub fn new(value: impl Into<String>) -> Result<Self, TransferDataError> {
27        let value = value.into();
28        if value.is_empty()
29            || value.trim() != value
30            || value.chars().any(|character| {
31                character.is_control() || character.is_whitespace() || character == '\0'
32            })
33        {
34            return Err(TransferDataError::InvalidMediaType(value));
35        }
36        Ok(Self(value))
37    }
38
39    #[must_use]
40    pub fn as_str(&self) -> &str {
41        &self.0
42    }
43}
44
45impl fmt::Display for MediaType {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        formatter.write_str(&self.0)
48    }
49}
50
51/// Portable representation kinds shared by clipboard and external drag/drop.
52#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
53pub enum TransferFormat {
54    PlainText,
55    Html,
56    UriList,
57    Files,
58    Rgba8Image,
59    Custom(MediaType),
60}
61
62impl TransferFormat {
63    #[must_use]
64    pub fn media_type(&self) -> &str {
65        match self {
66            Self::PlainText => "text/plain;charset=utf-8",
67            Self::Html => "text/html",
68            Self::UriList => "text/uri-list",
69            Self::Files => "application/x-incular-file-list",
70            Self::Rgba8Image => "application/x-incular-rgba8",
71            Self::Custom(media_type) => media_type.as_str(),
72        }
73    }
74}
75
76/// Owned, unpremultiplied RGBA8 image transfer.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct TransferImage {
79    width: u32,
80    height: u32,
81    rgba: Arc<[u8]>,
82}
83
84impl TransferImage {
85    pub fn new(
86        width: u32,
87        height: u32,
88        rgba: impl Into<Arc<[u8]>>,
89    ) -> Result<Self, TransferDataError> {
90        let rgba = rgba.into();
91        let expected = u64::from(width)
92            .checked_mul(u64::from(height))
93            .and_then(|pixels| pixels.checked_mul(4))
94            .and_then(|bytes| usize::try_from(bytes).ok())
95            .ok_or(TransferDataError::InvalidImageDimensions { width, height })?;
96        if rgba.len() != expected {
97            return Err(TransferDataError::InvalidImageLength {
98                width,
99                height,
100                actual: rgba.len(),
101                expected,
102            });
103        }
104        Ok(Self {
105            width,
106            height,
107            rgba,
108        })
109    }
110
111    #[must_use]
112    pub const fn width(&self) -> u32 {
113        self.width
114    }
115
116    #[must_use]
117    pub const fn height(&self) -> u32 {
118        self.height
119    }
120
121    #[must_use]
122    pub fn rgba(&self) -> &[u8] {
123        &self.rgba
124    }
125
126    #[must_use]
127    pub fn shared_rgba(&self) -> Arc<[u8]> {
128        self.rgba.clone()
129    }
130}
131
132/// Materialized transfer data. Large payload variants retain shared storage.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub enum TransferData {
135    Text(Arc<str>),
136    UriList(Arc<[String]>),
137    Files(Arc<[PathBuf]>),
138    Rgba8Image(TransferImage),
139    Bytes(Arc<[u8]>),
140}
141
142impl TransferData {
143    #[must_use]
144    pub fn text(value: impl Into<String>) -> Self {
145        Self::Text(Arc::<str>::from(value.into()))
146    }
147
148    #[must_use]
149    pub fn uri_list(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
150        Self::UriList(
151            values
152                .into_iter()
153                .map(Into::into)
154                .collect::<Vec<_>>()
155                .into(),
156        )
157    }
158
159    #[must_use]
160    pub fn files(values: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
161        Self::Files(
162            values
163                .into_iter()
164                .map(Into::into)
165                .collect::<Vec<_>>()
166                .into(),
167        )
168    }
169
170    #[must_use]
171    pub fn bytes(value: impl Into<Arc<[u8]>>) -> Self {
172        Self::Bytes(value.into())
173    }
174}
175
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub enum TransferDataError {
178    InvalidMediaType(String),
179    InvalidImageDimensions {
180        width: u32,
181        height: u32,
182    },
183    InvalidImageLength {
184        width: u32,
185        height: u32,
186        actual: usize,
187        expected: usize,
188    },
189    DuplicateFormat(TransferFormat),
190    TypeMismatch {
191        format: TransferFormat,
192        data: &'static str,
193    },
194    ProviderFailed(String),
195}
196
197impl fmt::Display for TransferDataError {
198    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            Self::InvalidMediaType(value) => {
201                write!(formatter, "invalid transfer media type {value:?}")
202            }
203            Self::InvalidImageDimensions { width, height } => {
204                write!(
205                    formatter,
206                    "RGBA8 image dimensions overflow: {width}x{height}"
207                )
208            }
209            Self::InvalidImageLength {
210                width,
211                height,
212                actual,
213                expected,
214            } => write!(
215                formatter,
216                "RGBA8 image {width}x{height} requires {expected} bytes, got {actual}"
217            ),
218            Self::DuplicateFormat(format) => {
219                write!(formatter, "duplicate transfer representation {format:?}")
220            }
221            Self::TypeMismatch { format, data } => write!(
222                formatter,
223                "transfer representation {format:?} cannot materialize {data} data"
224            ),
225            Self::ProviderFailed(message) => {
226                write!(formatter, "transfer provider failed: {message}")
227            }
228        }
229    }
230}
231
232impl std::error::Error for TransferDataError {}
233
234struct LazyTransferData {
235    provider: Arc<dyn Fn() -> Result<TransferData, TransferDataError> + Send + Sync>,
236    value: OnceLock<Result<TransferData, TransferDataError>>,
237}
238
239impl LazyTransferData {
240    fn materialize(&self) -> Result<TransferData, TransferDataError> {
241        self.value.get_or_init(|| (self.provider)()).clone()
242    }
243}
244
245#[derive(Clone)]
246enum TransferPayload {
247    Immediate(TransferData),
248    Lazy(Arc<LazyTransferData>),
249}
250
251impl TransferPayload {
252    fn materialize(&self) -> Result<TransferData, TransferDataError> {
253        match self {
254            Self::Immediate(data) => Ok(data.clone()),
255            Self::Lazy(data) => data.materialize(),
256        }
257    }
258}
259
260impl fmt::Debug for TransferPayload {
261    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262        match self {
263            Self::Immediate(data) => formatter.debug_tuple("Immediate").field(data).finish(),
264            Self::Lazy(_) => formatter.write_str("Lazy(<provider>)"),
265        }
266    }
267}
268
269impl PartialEq for TransferPayload {
270    fn eq(&self, other: &Self) -> bool {
271        match (self, other) {
272            (Self::Immediate(left), Self::Immediate(right)) => left == right,
273            (Self::Lazy(left), Self::Lazy(right)) => Arc::ptr_eq(left, right),
274            _ => false,
275        }
276    }
277}
278
279impl Eq for TransferPayload {}
280
281/// One advertised representation of a logical transfer item.
282#[derive(Clone, Debug, PartialEq, Eq)]
283pub struct TransferRepresentation {
284    format: TransferFormat,
285    payload: TransferPayload,
286}
287
288impl TransferRepresentation {
289    fn immediate(format: TransferFormat, data: TransferData) -> Self {
290        debug_assert!(data_matches_format(&format, &data));
291        Self {
292            format,
293            payload: TransferPayload::Immediate(data),
294        }
295    }
296
297    #[must_use]
298    pub fn plain_text(value: impl Into<String>) -> Self {
299        Self::immediate(TransferFormat::PlainText, TransferData::text(value))
300    }
301
302    #[must_use]
303    pub fn html(value: impl Into<String>) -> Self {
304        Self::immediate(TransferFormat::Html, TransferData::text(value))
305    }
306
307    #[must_use]
308    pub fn uri_list(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
309        Self::immediate(TransferFormat::UriList, TransferData::uri_list(values))
310    }
311
312    #[must_use]
313    pub fn files(values: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
314        Self::immediate(TransferFormat::Files, TransferData::files(values))
315    }
316
317    #[must_use]
318    pub fn rgba8_image(image: TransferImage) -> Self {
319        Self::immediate(TransferFormat::Rgba8Image, TransferData::Rgba8Image(image))
320    }
321
322    #[must_use]
323    pub fn custom(media_type: MediaType, bytes: impl Into<Arc<[u8]>>) -> Self {
324        Self::immediate(
325            TransferFormat::Custom(media_type),
326            TransferData::bytes(bytes),
327        )
328    }
329
330    /// Creates a delayed representation. The provider is invoked on first
331    /// materialization and its result (including failure) is cached and shared
332    /// by every clone of this representation.
333    #[must_use]
334    pub fn lazy(
335        format: TransferFormat,
336        provider: impl Fn() -> Result<TransferData, TransferDataError> + Send + Sync + 'static,
337    ) -> Self {
338        Self {
339            format,
340            payload: TransferPayload::Lazy(Arc::new(LazyTransferData {
341                provider: Arc::new(provider),
342                value: OnceLock::new(),
343            })),
344        }
345    }
346
347    #[must_use]
348    pub const fn format(&self) -> &TransferFormat {
349        &self.format
350    }
351
352    pub fn materialize(&self) -> Result<TransferData, TransferDataError> {
353        let data = self.payload.materialize()?;
354        if data_matches_format(&self.format, &data) {
355            Ok(data)
356        } else {
357            Err(TransferDataError::TypeMismatch {
358                format: self.format.clone(),
359                data: transfer_data_name(&data),
360            })
361        }
362    }
363}
364
365fn data_matches_format(format: &TransferFormat, data: &TransferData) -> bool {
366    matches!(
367        (format, data),
368        (
369            TransferFormat::PlainText | TransferFormat::Html,
370            TransferData::Text(_)
371        ) | (TransferFormat::UriList, TransferData::UriList(_))
372            | (TransferFormat::Files, TransferData::Files(_))
373            | (TransferFormat::Rgba8Image, TransferData::Rgba8Image(_))
374            | (TransferFormat::Custom(_), TransferData::Bytes(_))
375    )
376}
377
378fn transfer_data_name(data: &TransferData) -> &'static str {
379    match data {
380        TransferData::Text(_) => "text",
381        TransferData::UriList(_) => "URI list",
382        TransferData::Files(_) => "file list",
383        TransferData::Rgba8Image(_) => "RGBA8 image",
384        TransferData::Bytes(_) => "bytes",
385    }
386}
387
388/// Multiple representations of one logical clipboard/drag item.
389#[derive(Clone, Debug, PartialEq, Eq)]
390pub struct TransferItem {
391    representations: Arc<[TransferRepresentation]>,
392}
393
394impl TransferItem {
395    pub fn new(
396        representations: impl IntoIterator<Item = TransferRepresentation>,
397    ) -> Result<Self, TransferDataError> {
398        let representations = representations.into_iter().collect::<Vec<_>>();
399        let mut formats = HashSet::with_capacity(representations.len());
400        for representation in &representations {
401            if !formats.insert(representation.format.clone()) {
402                return Err(TransferDataError::DuplicateFormat(
403                    representation.format.clone(),
404                ));
405            }
406        }
407        Ok(Self {
408            representations: representations.into(),
409        })
410    }
411
412    #[must_use]
413    pub fn single(representation: TransferRepresentation) -> Self {
414        Self {
415            representations: vec![representation].into(),
416        }
417    }
418
419    #[must_use]
420    pub fn representations(&self) -> &[TransferRepresentation] {
421        &self.representations
422    }
423
424    #[must_use]
425    pub fn representation(&self, format: &TransferFormat) -> Option<&TransferRepresentation> {
426        self.representations
427            .iter()
428            .find(|representation| representation.format() == format)
429    }
430
431    fn filtered(&self, request: &TransferReadRequest) -> Option<Self> {
432        let representations = self
433            .representations
434            .iter()
435            .filter(|representation| request.includes(representation.format()))
436            .cloned()
437            .collect::<Vec<_>>();
438        (!representations.is_empty()).then(|| Self {
439            representations: representations.into(),
440        })
441    }
442}
443
444/// Ordered collection of clipboard or external drag items.
445#[derive(Clone, Debug, Default, PartialEq, Eq)]
446pub struct DataTransfer {
447    items: Arc<[TransferItem]>,
448}
449
450impl DataTransfer {
451    #[must_use]
452    pub fn new(items: impl IntoIterator<Item = TransferItem>) -> Self {
453        Self {
454            items: items.into_iter().collect::<Vec<_>>().into(),
455        }
456    }
457
458    #[must_use]
459    pub fn single(item: TransferItem) -> Self {
460        Self {
461            items: vec![item].into(),
462        }
463    }
464
465    #[must_use]
466    pub fn plain_text(value: impl Into<String>) -> Self {
467        Self::single(TransferItem::single(TransferRepresentation::plain_text(
468            value,
469        )))
470    }
471
472    #[must_use]
473    pub fn files(paths: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
474        Self::single(TransferItem::single(TransferRepresentation::files(paths)))
475    }
476
477    #[must_use]
478    pub fn items(&self) -> &[TransferItem] {
479        &self.items
480    }
481
482    #[must_use]
483    pub fn is_empty(&self) -> bool {
484        self.items.is_empty()
485    }
486
487    #[must_use]
488    pub fn representation(&self, format: &TransferFormat) -> Option<&TransferRepresentation> {
489        self.items
490            .iter()
491            .find_map(|item| item.representation(format))
492    }
493
494    pub fn materialize(
495        &self,
496        format: &TransferFormat,
497    ) -> Result<Option<TransferData>, TransferDataError> {
498        self.representation(format)
499            .map(TransferRepresentation::materialize)
500            .transpose()
501    }
502
503    pub fn plain_text_value(&self) -> Result<Option<Arc<str>>, TransferDataError> {
504        match self.materialize(&TransferFormat::PlainText)? {
505            Some(TransferData::Text(text)) => Ok(Some(text)),
506            Some(data) => Err(TransferDataError::TypeMismatch {
507                format: TransferFormat::PlainText,
508                data: transfer_data_name(&data),
509            }),
510            None => Ok(None),
511        }
512    }
513
514    #[must_use]
515    pub fn formats(&self) -> Vec<TransferFormat> {
516        let mut formats = Vec::new();
517        for item in self.items.iter() {
518            for representation in item.representations.iter() {
519                if !formats.contains(&representation.format) {
520                    formats.push(representation.format.clone());
521                }
522            }
523        }
524        formats
525    }
526
527    #[must_use]
528    pub fn filtered(&self, request: &TransferReadRequest) -> Self {
529        Self::new(self.items.iter().filter_map(|item| item.filtered(request)))
530    }
531}
532
533/// Requested clipboard representations. Empty means every representation the
534/// backend can enumerate without inventing conversions.
535#[derive(Clone, Debug, Default, PartialEq, Eq)]
536pub struct TransferReadRequest {
537    formats: Arc<[TransferFormat]>,
538}
539
540impl TransferReadRequest {
541    #[must_use]
542    pub fn all() -> Self {
543        Self::default()
544    }
545
546    #[must_use]
547    pub fn format(format: TransferFormat) -> Self {
548        Self {
549            formats: vec![format].into(),
550        }
551    }
552
553    #[must_use]
554    pub fn formats(formats: impl IntoIterator<Item = TransferFormat>) -> Self {
555        let mut unique = Vec::new();
556        for format in formats {
557            if !unique.contains(&format) {
558                unique.push(format);
559            }
560        }
561        Self {
562            formats: unique.into(),
563        }
564    }
565
566    #[must_use]
567    pub fn requested_formats(&self) -> &[TransferFormat] {
568        &self.formats
569    }
570
571    #[must_use]
572    pub fn includes(&self, format: &TransferFormat) -> bool {
573        self.formats.is_empty() || self.formats.iter().any(|requested| requested == format)
574    }
575}
576
577/// Per-format support for one direction of clipboard access.
578#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
579pub struct TransferFormatCapabilities {
580    pub plain_text: CapabilitySupport,
581    pub html: CapabilitySupport,
582    pub uri_list: CapabilitySupport,
583    pub files: CapabilitySupport,
584    pub rgba8_image: CapabilitySupport,
585    pub custom: CapabilitySupport,
586}
587
588impl TransferFormatCapabilities {
589    #[must_use]
590    pub const fn support(self, format: &TransferFormat) -> CapabilitySupport {
591        match format {
592            TransferFormat::PlainText => self.plain_text,
593            TransferFormat::Html => self.html,
594            TransferFormat::UriList => self.uri_list,
595            TransferFormat::Files => self.files,
596            TransferFormat::Rgba8Image => self.rgba8_image,
597            TransferFormat::Custom(_) => self.custom,
598        }
599    }
600
601    #[must_use]
602    pub const fn all(support: CapabilitySupport) -> Self {
603        Self {
604            plain_text: support,
605            html: support,
606            uri_list: support,
607            files: support,
608            rgba8_image: support,
609            custom: support,
610        }
611    }
612}
613
614#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
615pub struct ClipboardCapabilities {
616    pub read: TransferFormatCapabilities,
617    pub write: TransferFormatCapabilities,
618    /// Whether the backend can retain a provider without materializing its data
619    /// during the `write` call.
620    pub lazy_write: CapabilitySupport,
621}
622
623#[derive(Clone, Debug, PartialEq, Eq)]
624pub struct ClipboardWriteReport {
625    pub written: Vec<TransferFormat>,
626    pub skipped: Vec<TransferFormat>,
627}
628
629impl ClipboardWriteReport {
630    #[must_use]
631    pub fn all_written(formats: Vec<TransferFormat>) -> Self {
632        Self {
633            written: formats,
634            skipped: Vec::new(),
635        }
636    }
637}
638
639#[derive(Clone, Debug, PartialEq, Eq)]
640pub enum ClipboardError {
641    Unavailable,
642    UnsupportedFormat(TransferFormat),
643    UnsupportedCombination,
644    Transfer(TransferDataError),
645    Backend(String),
646}
647
648impl fmt::Display for ClipboardError {
649    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
650        match self {
651            Self::Unavailable => formatter.write_str("clipboard content is unavailable"),
652            Self::UnsupportedFormat(format) => {
653                write!(formatter, "clipboard format {format:?} is unsupported")
654            }
655            Self::UnsupportedCombination => {
656                formatter.write_str("clipboard cannot represent this combination atomically")
657            }
658            Self::Transfer(error) => error.fmt(formatter),
659            Self::Backend(message) => write!(formatter, "clipboard backend failed: {message}"),
660        }
661    }
662}
663
664impl std::error::Error for ClipboardError {
665    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
666        match self {
667            Self::Transfer(error) => Some(error),
668            _ => None,
669        }
670    }
671}
672
673impl From<TransferDataError> for ClipboardError {
674    fn from(value: TransferDataError) -> Self {
675        Self::Transfer(value)
676    }
677}
678
679/// Clipboard boundary shared by memory, desktop, and custom hosts.
680pub trait Clipboard {
681    fn capabilities(&self) -> ClipboardCapabilities;
682
683    fn read(&mut self, request: TransferReadRequest) -> Result<DataTransfer, ClipboardError>;
684
685    fn write(&mut self, transfer: DataTransfer) -> Result<ClipboardWriteReport, ClipboardError>;
686
687    /// Plain-text convenience layered over [`Self::read`].
688    fn get_text(&mut self) -> Option<String> {
689        let transfer = self
690            .read(TransferReadRequest::format(TransferFormat::PlainText))
691            .ok()?;
692        transfer
693            .plain_text_value()
694            .ok()?
695            .map(|text| text.to_string())
696    }
697
698    /// Plain-text convenience layered over [`Self::write`].
699    fn set_text(&mut self, text: String) {
700        let _ = self.write(DataTransfer::plain_text(text));
701    }
702}
703
704#[derive(Default)]
705pub struct MemoryClipboard {
706    transfer: Option<DataTransfer>,
707}
708
709impl Clipboard for MemoryClipboard {
710    fn capabilities(&self) -> ClipboardCapabilities {
711        ClipboardCapabilities {
712            read: TransferFormatCapabilities::all(CapabilitySupport::Supported),
713            write: TransferFormatCapabilities::all(CapabilitySupport::Supported),
714            lazy_write: CapabilitySupport::Supported,
715        }
716    }
717
718    fn read(&mut self, request: TransferReadRequest) -> Result<DataTransfer, ClipboardError> {
719        let transfer = self.transfer.as_ref().ok_or(ClipboardError::Unavailable)?;
720        let filtered = transfer.filtered(&request);
721        if filtered.is_empty() {
722            Err(ClipboardError::Unavailable)
723        } else {
724            Ok(filtered)
725        }
726    }
727
728    fn write(&mut self, transfer: DataTransfer) -> Result<ClipboardWriteReport, ClipboardError> {
729        let formats = transfer.formats();
730        self.transfer = Some(transfer);
731        Ok(ClipboardWriteReport::all_written(formats))
732    }
733}
734
735/// One operation an external drag source permits a target to request.
736#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
737pub enum TransferOperation {
738    Copy,
739    Move,
740    Link,
741}
742
743/// Compact operation set used for drag/drop negotiation.
744#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
745pub struct TransferOperations(u8);
746
747impl TransferOperations {
748    pub const COPY: Self = Self(1 << 0);
749    pub const MOVE: Self = Self(1 << 1);
750    pub const LINK: Self = Self(1 << 2);
751
752    #[must_use]
753    pub const fn empty() -> Self {
754        Self(0)
755    }
756
757    #[must_use]
758    pub const fn contains(self, operation: TransferOperation) -> bool {
759        let bit = match operation {
760            TransferOperation::Copy => Self::COPY.0,
761            TransferOperation::Move => Self::MOVE.0,
762            TransferOperation::Link => Self::LINK.0,
763        };
764        self.0 & bit != 0
765    }
766
767    #[must_use]
768    pub const fn union(self, other: Self) -> Self {
769        Self(self.0 | other.0)
770    }
771
772    #[must_use]
773    pub const fn preferred(self) -> Option<TransferOperation> {
774        if self.contains(TransferOperation::Copy) {
775            Some(TransferOperation::Copy)
776        } else if self.contains(TransferOperation::Move) {
777            Some(TransferOperation::Move)
778        } else if self.contains(TransferOperation::Link) {
779            Some(TransferOperation::Link)
780        } else {
781            None
782        }
783    }
784}
785
786#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
787pub enum ExternalDragPhase {
788    Enter,
789    Over,
790    Leave,
791    Cancel,
792    Drop,
793}
794
795/// Normalized incoming drag sample. `transfer` is the same shared transfer
796/// model used by the clipboard; backends never reinterpret a file as text or
797/// fabricate unsupported rich representations.
798#[derive(Clone, Debug, PartialEq)]
799pub struct ExternalDragEvent {
800    pub phase: ExternalDragPhase,
801    pub transfer: DataTransfer,
802    pub position: Offset,
803    pub allowed_operations: TransferOperations,
804}
805
806impl ExternalDragEvent {
807    #[must_use]
808    pub fn files(
809        phase: ExternalDragPhase,
810        paths: impl IntoIterator<Item = impl Into<PathBuf>>,
811        position: Offset,
812    ) -> Self {
813        Self {
814            phase,
815            transfer: DataTransfer::files(paths),
816            position,
817            // Winit's file-drop abstraction has no protocol for promising a
818            // source-side move/delete. Copy is therefore the only truthful
819            // operation until a richer native adapter says otherwise.
820            allowed_operations: TransferOperations::COPY,
821        }
822    }
823}
824
825/// Target negotiation result returned to a native adapter when it can honor a
826/// requested drag operation.
827#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
828pub struct ExternalDragResponse {
829    pub requested_operation: Option<TransferOperation>,
830}