1use crate::CapabilitySupport;
9use incular_core::Offset;
10use std::{
11 collections::HashSet,
12 fmt,
13 path::PathBuf,
14 sync::{Arc, OnceLock},
15};
16
17#[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#[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#[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#[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#[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 #[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#[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#[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#[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#[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 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
619pub 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
677pub enum TransferOperation {
678 Copy,
679 Move,
680 Link,
681}
682
683#[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#[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 allowed_operations: TransferOperations::COPY,
761 }
762 }
763}
764
765#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
768pub struct ExternalDragResponse {
769 pub requested_operation: Option<TransferOperation>,
770}