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",
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, thiserror::Error)]
177pub enum TransferDataError {
178    #[error("invalid transfer media type {0:?}")]
179    InvalidMediaType(String),
180    #[error("RGBA8 image dimensions overflow: {width}x{height}")]
181    InvalidImageDimensions { width: u32, height: u32 },
182    #[error("RGBA8 image {width}x{height} requires {expected} bytes, got {actual}")]
183    InvalidImageLength {
184        width: u32,
185        height: u32,
186        actual: usize,
187        expected: usize,
188    },
189    #[error("duplicate transfer representation {0:?}")]
190    DuplicateFormat(TransferFormat),
191    #[error("transfer representation {format:?} cannot materialize {data} data")]
192    TypeMismatch {
193        format: TransferFormat,
194        data: &'static str,
195    },
196    #[error("transfer provider failed: {0}")]
197    ProviderFailed(String),
198}
199
200struct LazyTransferData {
201    provider: Arc<dyn Fn() -> Result<TransferData, TransferDataError> + Send + Sync>,
202    value: OnceLock<Result<TransferData, TransferDataError>>,
203}
204
205impl LazyTransferData {
206    fn materialize(&self) -> Result<TransferData, TransferDataError> {
207        self.value.get_or_init(|| (self.provider)()).clone()
208    }
209}
210
211#[derive(Clone)]
212enum TransferPayload {
213    Immediate(TransferData),
214    Lazy(Arc<LazyTransferData>),
215}
216
217impl TransferPayload {
218    fn materialize(&self) -> Result<TransferData, TransferDataError> {
219        match self {
220            Self::Immediate(data) => Ok(data.clone()),
221            Self::Lazy(data) => data.materialize(),
222        }
223    }
224}
225
226impl fmt::Debug for TransferPayload {
227    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228        match self {
229            Self::Immediate(data) => formatter.debug_tuple("Immediate").field(data).finish(),
230            Self::Lazy(_) => formatter.write_str("Lazy(<provider>)"),
231        }
232    }
233}
234
235impl PartialEq for TransferPayload {
236    fn eq(&self, other: &Self) -> bool {
237        match (self, other) {
238            (Self::Immediate(left), Self::Immediate(right)) => left == right,
239            (Self::Lazy(left), Self::Lazy(right)) => Arc::ptr_eq(left, right),
240            _ => false,
241        }
242    }
243}
244
245impl Eq for TransferPayload {}
246
247/// One advertised representation of a logical transfer item.
248#[derive(Clone, Debug, PartialEq, Eq)]
249pub struct TransferRepresentation {
250    format: TransferFormat,
251    payload: TransferPayload,
252}
253
254impl TransferRepresentation {
255    fn immediate(format: TransferFormat, data: TransferData) -> Self {
256        debug_assert!(data_matches_format(&format, &data));
257        Self {
258            format,
259            payload: TransferPayload::Immediate(data),
260        }
261    }
262
263    #[must_use]
264    pub fn plain_text(value: impl Into<String>) -> Self {
265        Self::immediate(TransferFormat::PlainText, TransferData::text(value))
266    }
267
268    #[must_use]
269    pub fn html(value: impl Into<String>) -> Self {
270        Self::immediate(TransferFormat::Html, TransferData::text(value))
271    }
272
273    #[must_use]
274    pub fn uri_list(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
275        Self::immediate(TransferFormat::UriList, TransferData::uri_list(values))
276    }
277
278    #[must_use]
279    pub fn files(values: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
280        Self::immediate(TransferFormat::Files, TransferData::files(values))
281    }
282
283    #[must_use]
284    pub fn rgba8_image(image: TransferImage) -> Self {
285        Self::immediate(TransferFormat::Rgba8Image, TransferData::Rgba8Image(image))
286    }
287
288    #[must_use]
289    pub fn custom(media_type: MediaType, bytes: impl Into<Arc<[u8]>>) -> Self {
290        Self::immediate(
291            TransferFormat::Custom(media_type),
292            TransferData::bytes(bytes),
293        )
294    }
295
296    /// Creates a delayed representation. The provider is invoked on first
297    /// materialization and its result (including failure) is cached and shared
298    /// by every clone of this representation.
299    #[must_use]
300    pub fn lazy(
301        format: TransferFormat,
302        provider: impl Fn() -> Result<TransferData, TransferDataError> + Send + Sync + 'static,
303    ) -> Self {
304        Self {
305            format,
306            payload: TransferPayload::Lazy(Arc::new(LazyTransferData {
307                provider: Arc::new(provider),
308                value: OnceLock::new(),
309            })),
310        }
311    }
312
313    #[must_use]
314    pub const fn format(&self) -> &TransferFormat {
315        &self.format
316    }
317
318    pub fn materialize(&self) -> Result<TransferData, TransferDataError> {
319        let data = self.payload.materialize()?;
320        if data_matches_format(&self.format, &data) {
321            Ok(data)
322        } else {
323            Err(TransferDataError::TypeMismatch {
324                format: self.format.clone(),
325                data: transfer_data_name(&data),
326            })
327        }
328    }
329}
330
331fn data_matches_format(format: &TransferFormat, data: &TransferData) -> bool {
332    matches!(
333        (format, data),
334        (
335            TransferFormat::PlainText | TransferFormat::Html,
336            TransferData::Text(_)
337        ) | (TransferFormat::UriList, TransferData::UriList(_))
338            | (TransferFormat::Files, TransferData::Files(_))
339            | (TransferFormat::Rgba8Image, TransferData::Rgba8Image(_))
340            | (TransferFormat::Custom(_), TransferData::Bytes(_))
341    )
342}
343
344fn transfer_data_name(data: &TransferData) -> &'static str {
345    match data {
346        TransferData::Text(_) => "text",
347        TransferData::UriList(_) => "URI list",
348        TransferData::Files(_) => "file list",
349        TransferData::Rgba8Image(_) => "RGBA8 image",
350        TransferData::Bytes(_) => "bytes",
351    }
352}
353
354/// Multiple representations of one logical clipboard/drag item.
355#[derive(Clone, Debug, PartialEq, Eq)]
356pub struct TransferItem {
357    representations: Arc<[TransferRepresentation]>,
358}
359
360impl TransferItem {
361    pub fn new(
362        representations: impl IntoIterator<Item = TransferRepresentation>,
363    ) -> Result<Self, TransferDataError> {
364        let representations = representations.into_iter().collect::<Vec<_>>();
365        let mut formats = HashSet::with_capacity(representations.len());
366        for representation in &representations {
367            if !formats.insert(representation.format.clone()) {
368                return Err(TransferDataError::DuplicateFormat(
369                    representation.format.clone(),
370                ));
371            }
372        }
373        Ok(Self {
374            representations: representations.into(),
375        })
376    }
377
378    #[must_use]
379    pub fn single(representation: TransferRepresentation) -> Self {
380        Self {
381            representations: vec![representation].into(),
382        }
383    }
384
385    #[must_use]
386    pub fn representations(&self) -> &[TransferRepresentation] {
387        &self.representations
388    }
389
390    #[must_use]
391    pub fn representation(&self, format: &TransferFormat) -> Option<&TransferRepresentation> {
392        self.representations
393            .iter()
394            .find(|representation| representation.format() == format)
395    }
396
397    fn filtered(&self, request: &TransferReadRequest) -> Option<Self> {
398        let representations = self
399            .representations
400            .iter()
401            .filter(|representation| request.includes(representation.format()))
402            .cloned()
403            .collect::<Vec<_>>();
404        (!representations.is_empty()).then(|| Self {
405            representations: representations.into(),
406        })
407    }
408}
409
410/// Ordered collection of clipboard or external drag items.
411#[derive(Clone, Debug, Default, PartialEq, Eq)]
412pub struct DataTransfer {
413    items: Arc<[TransferItem]>,
414}
415
416impl DataTransfer {
417    #[must_use]
418    pub fn new(items: impl IntoIterator<Item = TransferItem>) -> Self {
419        Self {
420            items: items.into_iter().collect::<Vec<_>>().into(),
421        }
422    }
423
424    #[must_use]
425    pub fn single(item: TransferItem) -> Self {
426        Self {
427            items: vec![item].into(),
428        }
429    }
430
431    #[must_use]
432    pub fn plain_text(value: impl Into<String>) -> Self {
433        Self::single(TransferItem::single(TransferRepresentation::plain_text(
434            value,
435        )))
436    }
437
438    #[must_use]
439    pub fn files(paths: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
440        Self::single(TransferItem::single(TransferRepresentation::files(paths)))
441    }
442
443    #[must_use]
444    pub fn items(&self) -> &[TransferItem] {
445        &self.items
446    }
447
448    #[must_use]
449    pub fn is_empty(&self) -> bool {
450        self.items.is_empty()
451    }
452
453    #[must_use]
454    pub fn representation(&self, format: &TransferFormat) -> Option<&TransferRepresentation> {
455        self.items
456            .iter()
457            .find_map(|item| item.representation(format))
458    }
459
460    pub fn materialize(
461        &self,
462        format: &TransferFormat,
463    ) -> Result<Option<TransferData>, TransferDataError> {
464        self.representation(format)
465            .map(TransferRepresentation::materialize)
466            .transpose()
467    }
468
469    pub fn plain_text_value(&self) -> Result<Option<Arc<str>>, TransferDataError> {
470        match self.materialize(&TransferFormat::PlainText)? {
471            Some(TransferData::Text(text)) => Ok(Some(text)),
472            Some(data) => Err(TransferDataError::TypeMismatch {
473                format: TransferFormat::PlainText,
474                data: transfer_data_name(&data),
475            }),
476            None => Ok(None),
477        }
478    }
479
480    #[must_use]
481    pub fn formats(&self) -> Vec<TransferFormat> {
482        let mut formats = Vec::new();
483        for item in self.items.iter() {
484            for representation in item.representations.iter() {
485                if !formats.contains(&representation.format) {
486                    formats.push(representation.format.clone());
487                }
488            }
489        }
490        formats
491    }
492
493    #[must_use]
494    pub fn filtered(&self, request: &TransferReadRequest) -> Self {
495        Self::new(self.items.iter().filter_map(|item| item.filtered(request)))
496    }
497}
498
499/// Requested clipboard representations. Empty means every representation the
500/// backend can enumerate without inventing conversions.
501#[derive(Clone, Debug, Default, PartialEq, Eq)]
502pub struct TransferReadRequest {
503    formats: Arc<[TransferFormat]>,
504}
505
506impl TransferReadRequest {
507    #[must_use]
508    pub fn all() -> Self {
509        Self::default()
510    }
511
512    #[must_use]
513    pub fn format(format: TransferFormat) -> Self {
514        Self {
515            formats: vec![format].into(),
516        }
517    }
518
519    #[must_use]
520    pub fn formats(formats: impl IntoIterator<Item = TransferFormat>) -> Self {
521        let mut unique = Vec::new();
522        for format in formats {
523            if !unique.contains(&format) {
524                unique.push(format);
525            }
526        }
527        Self {
528            formats: unique.into(),
529        }
530    }
531
532    #[must_use]
533    pub fn requested_formats(&self) -> &[TransferFormat] {
534        &self.formats
535    }
536
537    #[must_use]
538    pub fn includes(&self, format: &TransferFormat) -> bool {
539        self.formats.is_empty() || self.formats.iter().any(|requested| requested == format)
540    }
541}
542
543/// Per-format support for one direction of clipboard access.
544#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
545pub struct TransferFormatCapabilities {
546    pub plain_text: CapabilitySupport,
547    pub html: CapabilitySupport,
548    pub uri_list: CapabilitySupport,
549    pub files: CapabilitySupport,
550    pub rgba8_image: CapabilitySupport,
551    pub custom: CapabilitySupport,
552}
553
554impl TransferFormatCapabilities {
555    #[must_use]
556    pub const fn support(self, format: &TransferFormat) -> CapabilitySupport {
557        match format {
558            TransferFormat::PlainText => self.plain_text,
559            TransferFormat::Html => self.html,
560            TransferFormat::UriList => self.uri_list,
561            TransferFormat::Files => self.files,
562            TransferFormat::Rgba8Image => self.rgba8_image,
563            TransferFormat::Custom(_) => self.custom,
564        }
565    }
566
567    #[must_use]
568    pub const fn all(support: CapabilitySupport) -> Self {
569        Self {
570            plain_text: support,
571            html: support,
572            uri_list: support,
573            files: support,
574            rgba8_image: support,
575            custom: support,
576        }
577    }
578}
579
580#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
581pub struct ClipboardCapabilities {
582    pub read: TransferFormatCapabilities,
583    pub write: TransferFormatCapabilities,
584    /// Whether the backend can retain a provider without materializing its data
585    /// during the `write` call.
586    pub lazy_write: CapabilitySupport,
587}
588
589#[derive(Clone, Debug, PartialEq, Eq)]
590pub struct ClipboardWriteReport {
591    pub written: Vec<TransferFormat>,
592    pub skipped: Vec<TransferFormat>,
593}
594
595impl ClipboardWriteReport {
596    #[must_use]
597    pub fn all_written(formats: Vec<TransferFormat>) -> Self {
598        Self {
599            written: formats,
600            skipped: Vec::new(),
601        }
602    }
603}
604
605#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
606pub enum ClipboardError {
607    #[error("clipboard content is unavailable")]
608    Unavailable,
609    #[error("clipboard format {0:?} is unsupported")]
610    UnsupportedFormat(TransferFormat),
611    #[error("clipboard cannot represent this combination atomically")]
612    UnsupportedCombination,
613    #[error("{0}")]
614    Transfer(#[from] TransferDataError),
615    #[error("clipboard backend failed: {0}")]
616    Backend(String),
617}
618
619/// Clipboard boundary shared by memory, desktop, and custom hosts.
620pub trait Clipboard {
621    fn capabilities(&self) -> ClipboardCapabilities;
622
623    fn read(&mut self, request: TransferReadRequest) -> Result<DataTransfer, ClipboardError>;
624
625    fn write(&mut self, transfer: DataTransfer) -> Result<ClipboardWriteReport, ClipboardError>;
626
627    /// Plain-text convenience layered over [`Self::read`].
628    fn get_text(&mut self) -> Option<String> {
629        let transfer = self
630            .read(TransferReadRequest::format(TransferFormat::PlainText))
631            .ok()?;
632        transfer
633            .plain_text_value()
634            .ok()?
635            .map(|text| text.to_string())
636    }
637
638    /// Plain-text convenience layered over [`Self::write`].
639    fn set_text(&mut self, text: String) {
640        let _ = self.write(DataTransfer::plain_text(text));
641    }
642}
643
644#[derive(Default)]
645pub struct MemoryClipboard {
646    transfer: Option<DataTransfer>,
647}
648
649impl Clipboard for MemoryClipboard {
650    fn capabilities(&self) -> ClipboardCapabilities {
651        ClipboardCapabilities {
652            read: TransferFormatCapabilities::all(CapabilitySupport::Supported),
653            write: TransferFormatCapabilities::all(CapabilitySupport::Supported),
654            lazy_write: CapabilitySupport::Supported,
655        }
656    }
657
658    fn read(&mut self, request: TransferReadRequest) -> Result<DataTransfer, ClipboardError> {
659        let transfer = self.transfer.as_ref().ok_or(ClipboardError::Unavailable)?;
660        let filtered = transfer.filtered(&request);
661        if filtered.is_empty() {
662            Err(ClipboardError::Unavailable)
663        } else {
664            Ok(filtered)
665        }
666    }
667
668    fn write(&mut self, transfer: DataTransfer) -> Result<ClipboardWriteReport, ClipboardError> {
669        let formats = transfer.formats();
670        self.transfer = Some(transfer);
671        Ok(ClipboardWriteReport::all_written(formats))
672    }
673}
674
675/// One operation an external drag source permits a target to request.
676#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
677pub enum TransferOperation {
678    Copy,
679    Move,
680    Link,
681}
682
683/// Compact operation set used for drag/drop negotiation.
684#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
685pub struct TransferOperations(u8);
686
687impl TransferOperations {
688    pub const COPY: Self = Self(1 << 0);
689    pub const MOVE: Self = Self(1 << 1);
690    pub const LINK: Self = Self(1 << 2);
691
692    #[must_use]
693    pub const fn empty() -> Self {
694        Self(0)
695    }
696
697    #[must_use]
698    pub const fn contains(self, operation: TransferOperation) -> bool {
699        let bit = match operation {
700            TransferOperation::Copy => Self::COPY.0,
701            TransferOperation::Move => Self::MOVE.0,
702            TransferOperation::Link => Self::LINK.0,
703        };
704        self.0 & bit != 0
705    }
706
707    #[must_use]
708    pub const fn union(self, other: Self) -> Self {
709        Self(self.0 | other.0)
710    }
711
712    #[must_use]
713    pub const fn preferred(self) -> Option<TransferOperation> {
714        if self.contains(TransferOperation::Copy) {
715            Some(TransferOperation::Copy)
716        } else if self.contains(TransferOperation::Move) {
717            Some(TransferOperation::Move)
718        } else if self.contains(TransferOperation::Link) {
719            Some(TransferOperation::Link)
720        } else {
721            None
722        }
723    }
724}
725
726#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
727pub enum ExternalDragPhase {
728    Enter,
729    Over,
730    Leave,
731    Cancel,
732    Drop,
733}
734
735/// Normalized incoming drag sample. `transfer` is the same shared transfer
736/// model used by the clipboard; backends never reinterpret a file as text or
737/// fabricate unsupported rich representations.
738#[derive(Clone, Debug, PartialEq)]
739pub struct ExternalDragEvent {
740    pub phase: ExternalDragPhase,
741    pub transfer: DataTransfer,
742    pub position: Offset,
743    pub allowed_operations: TransferOperations,
744}
745
746impl ExternalDragEvent {
747    #[must_use]
748    pub fn files(
749        phase: ExternalDragPhase,
750        paths: impl IntoIterator<Item = impl Into<PathBuf>>,
751        position: Offset,
752    ) -> Self {
753        Self {
754            phase,
755            transfer: DataTransfer::files(paths),
756            position,
757            // Winit's file-drop abstraction has no protocol for promising a
758            // source-side move/delete. Copy is therefore the only truthful
759            // operation until a richer native adapter says otherwise.
760            allowed_operations: TransferOperations::COPY,
761        }
762    }
763}
764
765/// Target negotiation result returned to a native adapter when it can honor a
766/// requested drag operation.
767#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
768pub struct ExternalDragResponse {
769    pub requested_operation: Option<TransferOperation>,
770}