Skip to content

Commit c5850be

Browse files
Move clipboard, drag-and-drop, and file opening onto a shared multi-format data transfer type (#4945)
* Generalize data transfer systems * Move clipboard items to ingest as application data * Carry application data as parsed items * Accept image drags on desktop * Fix tests
1 parent f11e2d7 commit c5850be

47 files changed

Lines changed: 1155 additions & 719 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎desktop/src/app.rs‎

Lines changed: 39 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ use std::sync::mpsc::{Receiver, Sender};
88
use std::thread;
99
use std::time::{Duration, Instant};
1010
use winit::application::ApplicationHandler;
11-
use winit::data_transfer::{DataTransferSendBuilder, TypeHint};
11+
use winit::data_transfer::TypeHint;
1212
use winit::dpi::PhysicalSize;
1313
use winit::event::{ElementState, MouseButton, StartCause, WindowEvent};
1414
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
15-
use winit::event_loop::{ActiveEventLoop, AsyncRequestSerial, ControlFlow, DndAction, EventLoop};
15+
use winit::event_loop::{ActiveEventLoop, ControlFlow, DndAction, EventLoop};
1616
use winit::window::WindowId;
1717

18+
use crate::data_transfer::{self, TransferReads};
1819
use crate::dirs;
1920
use crate::event::{AppEvent, AppEventScheduler};
2021
use crate::input::InputState;
@@ -24,7 +25,7 @@ use crate::render::{RenderError, RenderState};
2425
use crate::spawner::TokioSpawner;
2526
use crate::ui::{Acceleration, InputEvent, UiCommand, UiConfig, UiInstance};
2627
use crate::window::Window;
27-
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, IngestAction};
28+
use crate::wrapper::messages::{DataTransfer, DesktopFrontendMessage, DesktopWrapperMessage, IngestAction};
2829
use crate::wrapper::{DesktopWrapper, FolderStore, MmapResourceStorage, WgpuContext, WgpuExecutor, serialize_frontend_messages};
2930

3031
pub(crate) struct App {
@@ -36,8 +37,7 @@ pub(crate) struct App {
3637
window_maximized: bool,
3738
window_fullscreen: bool,
3839
window_pending_drag: bool,
39-
pending_dnd_fetch: Option<AsyncRequestSerial>,
40-
pending_clipboard_fetch: Option<AsyncRequestSerial>,
40+
transfer_reads: TransferReads,
4141
input_state: InputState,
4242
app_event_receiver: Receiver<AppEvent>,
4343
app_event_scheduler: AppEventScheduler,
@@ -133,8 +133,7 @@ impl App {
133133
window_maximized: false,
134134
window_fullscreen: false,
135135
window_pending_drag: false,
136-
pending_dnd_fetch: None,
137-
pending_clipboard_fetch: None,
136+
transfer_reads: TransferReads::default(),
138137
input_state: InputState::new(),
139138
app_event_receiver,
140139
app_event_scheduler,
@@ -256,16 +255,10 @@ impl App {
256255
futures::executor::block_on(dialog.pick_file()).into_iter().collect()
257256
};
258257

259-
for handle in handles {
260-
let path = handle.path().to_path_buf();
261-
match fs::read(&path) {
262-
Ok(content) => {
263-
let action = action.clone();
264-
let message = DesktopWrapperMessage::IngestFile { path, content, action };
265-
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
266-
}
267-
Err(e) => tracing::error!("Failed to read file {}: {}", path.display(), e),
268-
}
258+
let files = data_transfer::read_files(handles.iter().map(|handle| handle.path().to_path_buf()));
259+
if !files.is_empty() {
260+
let message = DesktopWrapperMessage::Ingest { content: DataTransfer(files), action };
261+
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
269262
}
270263
});
271264
}
@@ -380,8 +373,8 @@ impl App {
380373
DesktopFrontendMessage::ClipboardRead => {
381374
self.app_event_scheduler.schedule(AppEvent::ClipboardRead);
382375
}
383-
DesktopFrontendMessage::ClipboardWrite { content, svg } => {
384-
self.app_event_scheduler.schedule(AppEvent::ClipboardWrite { content, svg });
376+
DesktopFrontendMessage::ClipboardWrite(content) => {
377+
self.app_event_scheduler.schedule(AppEvent::ClipboardWrite(content));
385378
}
386379
DesktopFrontendMessage::PointerLock => {
387380
self.input_state.lock_pointer();
@@ -502,31 +495,15 @@ impl App {
502495
}
503496
}
504497
AppEvent::ClipboardRead => {
505-
let result = event_loop.clipboard().and_then(|id| {
506-
let Some(id) = id else { return Ok(None) };
507-
let data_transfer = event_loop.data_transfer(id)?;
508-
if !data_transfer.has_type(&TypeHint::Plaintext) {
509-
return Ok(None);
510-
}
511-
event_loop.fetch_data_transfer(id, &TypeHint::Plaintext).map(Some)
512-
});
513-
match result {
514-
Ok(Some(serial)) => self.pending_clipboard_fetch = Some(serial),
515-
Ok(None) => self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::ClipboardReadResult { content: None }),
516-
Err(e) => {
517-
tracing::error!("Failed to read from clipboard: {e}");
518-
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::ClipboardReadResult { content: None });
519-
}
498+
let read = event_loop
499+
.clipboard()
500+
.and_then(|id| id.map_or(Ok(()), |id| self.transfer_reads.start(event_loop, id, IngestAction::Paste)));
501+
if let Err(e) = read {
502+
tracing::error!("Failed to read from clipboard: {e}");
520503
}
521504
}
522-
AppEvent::ClipboardWrite { content, svg } => {
523-
let has_svg = svg.is_some();
524-
let mut builder = DataTransferSendBuilder::new((content, svg)).with_type(TypeHint::Plaintext, |(content, _), _| Some(content.clone()));
525-
if has_svg {
526-
builder = builder.with_type(TypeHint::Image { extension_hint: Some("svg") }, |(_, svg), _| svg.as_ref().map(|svg| svg.as_bytes().to_vec()));
527-
}
528-
let send_data = builder.build();
529-
if let Err(e) = event_loop.set_clipboard(send_data) {
505+
AppEvent::ClipboardWrite(content) => {
506+
if let Err(e) = event_loop.set_clipboard(data_transfer::send(content)) {
530507
tracing::error!("Failed to write to clipboard: {e}");
531508
}
532509
}
@@ -559,18 +536,16 @@ impl App {
559536
}
560537
let app_event_scheduler = self.app_event_scheduler.clone();
561538
let _ = thread::spawn(move || {
562-
for path in paths {
539+
for path in &paths {
563540
tracing::info!("Opening file: {}", path.display());
564-
if let Ok(content) = fs::read(&path) {
565-
let message = DesktopWrapperMessage::IngestFile {
566-
path,
567-
content,
568-
action: IngestAction::Open,
569-
};
570-
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
571-
} else {
572-
tracing::error!("Failed to read file: {}", path.display());
573-
}
541+
}
542+
let files = data_transfer::read_files(paths);
543+
if !files.is_empty() {
544+
let message = DesktopWrapperMessage::Ingest {
545+
content: DataTransfer(files),
546+
action: IngestAction::Open,
547+
};
548+
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
574549
}
575550
});
576551
}
@@ -670,57 +645,25 @@ impl ApplicationHandler for App {
670645
}
671646
}
672647
WindowEvent::DragEntered { id, .. } => {
673-
let accepts = event_loop.data_transfer(id).is_ok_and(|data_transfer| data_transfer.has_type(&TypeHint::UriList));
648+
let accepts = event_loop.data_transfer(id).is_ok_and(|data_transfer| {
649+
data_transfer.has_type(&TypeHint::UriList) || data_transfer.has_type(&TypeHint::Image { extension_hint: None }) || data_transfer.has_type(&TypeHint::Plaintext)
650+
});
674651
let actions: &[DndAction] = if accepts { &[DndAction::Copy] } else { &[] };
675652
if let Err(e) = event_loop.set_valid_dnd_actions(id, actions) {
676653
tracing::error!("Failed to set valid drag and drop actions: {e}");
677654
}
678655
}
679-
WindowEvent::DragDropped { id, .. } => match event_loop.fetch_data_transfer(id, &TypeHint::UriList) {
680-
Ok(serial) => self.pending_dnd_fetch = Some(serial),
681-
Err(e) => tracing::error!("Failed to fetch dropped data: {e}"),
682-
},
683-
WindowEvent::DataTransferReceived { serial, ref value, .. } if self.pending_clipboard_fetch == Some(serial) => match value.try_as_string() {
684-
Ok(content) => {
685-
self.pending_clipboard_fetch = None;
686-
let message = DesktopWrapperMessage::ClipboardReadResult { content: Some(content) };
687-
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
656+
WindowEvent::DragDropped { id, .. } => {
657+
if let Err(e) = self.transfer_reads.start(event_loop, id, IngestAction::Import) {
658+
tracing::error!("Failed to fetch dropped data: {e}");
688659
}
689-
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
690-
Err(e) => {
691-
self.pending_clipboard_fetch = None;
692-
tracing::error!("Failed to read from clipboard: {e}");
693-
let message = DesktopWrapperMessage::ClipboardReadResult { content: None };
660+
}
661+
WindowEvent::DataTransferReceived { serial, ref value, .. } => {
662+
if let Some((content, action)) = self.transfer_reads.receive(serial, value.as_ref()) {
663+
let message = DesktopWrapperMessage::Ingest { content, action };
694664
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
695665
}
696-
},
697-
WindowEvent::DataTransferReceived { serial, ref value, .. } if self.pending_dnd_fetch == Some(serial) => match value.try_as_uris() {
698-
Ok(uris) => {
699-
self.pending_dnd_fetch = None;
700-
let paths = uris.iter().filter_map(|uri| url::Url::parse(uri).ok()?.to_file_path().ok());
701-
for path in paths {
702-
match fs::read(&path) {
703-
Ok(content) => {
704-
let message = DesktopWrapperMessage::IngestFile {
705-
path,
706-
content,
707-
action: IngestAction::Import,
708-
};
709-
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
710-
}
711-
Err(e) => {
712-
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
713-
return;
714-
}
715-
};
716-
}
717-
}
718-
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
719-
Err(e) => {
720-
self.pending_dnd_fetch = None;
721-
tracing::error!("Failed to read dropped data: {e}");
722-
}
723-
},
666+
}
724667

725668
WindowEvent::PointerMoved { .. } | WindowEvent::PointerLeft { position: Some(_), .. } | WindowEvent::PointerEntered { .. }
726669
if !self.input_state.pointer_locked() && self.window_pending_drag =>

‎desktop/src/data_transfer.rs‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use std::fs;
2+
use std::io;
3+
use std::path::PathBuf;
4+
use winit::data_transfer::{DataTransferId, DataTransferSend, DataTransferSendBuilder, SendData, TypeHint, TypedData};
5+
use winit::error::TransferError;
6+
use winit::event_loop::{ActiveEventLoop, AsyncRequestSerial};
7+
8+
use crate::wrapper::messages::{DataTransfer, DataTransferItem, DataType, IngestAction, RawData};
9+
10+
fn type_hint(data_type: DataType) -> Option<TypeHint> {
11+
match data_type {
12+
DataType::Text => Some(TypeHint::Plaintext),
13+
DataType::Svg | DataType::Raster(_) => Some(TypeHint::Image {
14+
extension_hint: Some(data_type.extensions().first().copied()?),
15+
}),
16+
_ => None,
17+
}
18+
}
19+
20+
fn data_type(hint: TypeHint) -> Option<DataType> {
21+
match hint {
22+
TypeHint::Plaintext => Some(DataType::Text),
23+
TypeHint::Image { extension_hint: Some(extension) } => match DataType::from_extension(extension) {
24+
data_type @ (DataType::Svg | DataType::Raster(_)) => Some(data_type),
25+
_ => None,
26+
},
27+
_ => None,
28+
}
29+
}
30+
31+
pub(crate) fn send(content: DataTransfer) -> Box<dyn DataTransferSend> {
32+
let mut builder = DataTransferSendBuilder::new(());
33+
for item in content.0 {
34+
let Some((data_type, data)) = item.into_raw() else { continue };
35+
let Some(hint) = type_hint(data_type) else { continue };
36+
let data = match (hint, data) {
37+
(TypeHint::Plaintext, RawData::Text(text)) => SendData::String(text),
38+
(TypeHint::Plaintext, RawData::Bytes(_)) => continue,
39+
(_, data) => SendData::Bytes(data.into_bytes()),
40+
};
41+
builder.add_type(hint, move |_, _| Some(data.clone()));
42+
}
43+
builder.build()
44+
}
45+
46+
pub(crate) fn read_files(paths: impl IntoIterator<Item = PathBuf>) -> Vec<DataTransferItem> {
47+
paths
48+
.into_iter()
49+
.filter_map(|path| match fs::read(&path) {
50+
Ok(data) => Some(DataTransferItem::from_file(path, None, data)),
51+
Err(e) => {
52+
tracing::error!("Failed to read file {}: {e}", path.display());
53+
None
54+
}
55+
})
56+
.collect()
57+
}
58+
59+
#[derive(Default)]
60+
pub(crate) struct TransferReads(Vec<TransferRead>);
61+
62+
struct TransferRead {
63+
action: IngestAction,
64+
fetches: Vec<(AsyncRequestSerial, TypeHint)>,
65+
items: Vec<DataTransferItem>,
66+
}
67+
68+
impl TransferReads {
69+
pub(crate) fn start(&mut self, event_loop: &dyn ActiveEventLoop, id: DataTransferId, action: IngestAction) -> Result<(), TransferError> {
70+
let mut hints = Vec::new();
71+
for hint in event_loop.data_transfer(id)?.available_types().iter().filter_map(|transfer_type| transfer_type.hint()) {
72+
if (hint == TypeHint::UriList || data_type(hint).is_some()) && !hints.contains(&hint) {
73+
hints.push(hint);
74+
}
75+
}
76+
77+
let mut fetches = Vec::new();
78+
for hint in hints {
79+
fetches.push((event_loop.fetch_data_transfer(id, &hint)?, hint));
80+
}
81+
82+
if !fetches.is_empty() {
83+
self.0.push(TransferRead { action, fetches, items: Vec::new() });
84+
}
85+
Ok(())
86+
}
87+
88+
pub(crate) fn receive(&mut self, serial: AsyncRequestSerial, value: &dyn TypedData) -> Option<(DataTransfer, IngestAction)> {
89+
let (index, fetch) = self
90+
.0
91+
.iter()
92+
.enumerate()
93+
.find_map(|(index, read)| Some((index, read.fetches.iter().position(|(fetch, _)| *fetch == serial)?)))?;
94+
let read = &mut self.0[index];
95+
let hint = read.fetches[fetch].1;
96+
97+
let received = match hint {
98+
TypeHint::UriList => value.try_as_uris().map(|uris| {
99+
let paths = uris.iter().filter_map(|uri| url::Url::parse(uri).ok()?.to_file_path().ok());
100+
read.items.extend(read_files(paths));
101+
}),
102+
TypeHint::Plaintext => value.try_as_string().map(|text| read.items.extend(DataTransferItem::from_raw(DataType::Text, RawData::Text(text)))),
103+
_ => value
104+
.try_as_bytes()
105+
.map(|bytes| read.items.extend(data_type(hint).and_then(|data_type| DataTransferItem::from_raw(data_type, RawData::Bytes(bytes))))),
106+
};
107+
match received {
108+
Err(e) if e.kind() == io::ErrorKind::WouldBlock => return None,
109+
Err(e) => tracing::error!("Failed to read transferred data: {e}"),
110+
Ok(()) => {}
111+
}
112+
read.fetches.remove(fetch);
113+
if !read.fetches.is_empty() {
114+
return None;
115+
}
116+
117+
let read = self.0.remove(index);
118+
Some((DataTransfer(read.items), read.action))
119+
}
120+
}

‎desktop/src/event.rs‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::ui::Cursor;
22
use crate::wrapper::Texture;
3-
use crate::wrapper::messages::DesktopWrapperMessage;
3+
use crate::wrapper::messages::{DataTransfer, DesktopWrapperMessage};
44

55
pub(crate) enum AppEvent {
66
UiUpdate(wgpu::Texture),
@@ -9,10 +9,7 @@ pub(crate) enum AppEvent {
99
DesktopWrapperMessage(DesktopWrapperMessage),
1010
ViewportTexture(Option<Texture>),
1111
ClipboardRead,
12-
ClipboardWrite {
13-
content: String,
14-
svg: Option<String>,
15-
},
12+
ClipboardWrite(DataTransfer),
1613
Exit,
1714
UiCrashed,
1815
OpenFiles(Vec<std::path::PathBuf>),

‎desktop/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub(crate) use graphite_desktop_wrapper as wrapper;
1414

1515
mod app;
1616
mod cli;
17+
mod data_transfer;
1718
mod dirs;
1819
mod event;
1920
mod gpu_context;

‎desktop/wrapper/src/handle_desktop_wrapper_message.rs‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw;
21
use graphite_editor::messages::prelude::*;
32

43
use super::DesktopWrapperMessageDispatcher;
@@ -15,13 +14,8 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
1514
DesktopWrapperMessage::Input(message) => {
1615
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
1716
}
18-
DesktopWrapperMessage::IngestFile { path, content, action } => {
19-
dispatcher.queue_editor_message(IngestMessage::Ingest {
20-
data: content,
21-
action,
22-
mime_type: String::new(),
23-
path: Some(path),
24-
});
17+
DesktopWrapperMessage::Ingest { content, action } => {
18+
dispatcher.queue_editor_message(IngestMessage::Ingest { content, action });
2519
}
2620
DesktopWrapperMessage::SaveFileDialogResult { path, context } => match context {
2721
SaveFileDialogContext::Document { document_id, content } => {
@@ -61,14 +55,6 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
6155
}
6256
#[cfg(not(target_os = "macos"))]
6357
DesktopWrapperMessage::MenuEvent { id: _ } => {}
64-
DesktopWrapperMessage::ClipboardReadResult { content } => {
65-
if let Some(content) = content {
66-
let message = ClipboardMessage::ReadClipboard {
67-
content: ClipboardContentRaw::Text(content),
68-
};
69-
dispatcher.queue_editor_message(message);
70-
}
71-
}
7258
DesktopWrapperMessage::PointerLockMove { x, y } => {
7359
let message = AppWindowMessage::PointerLockMove { x, y };
7460
dispatcher.queue_editor_message(message);

0 commit comments

Comments
 (0)