Desktop: Read and write the clipboard via the winit data transfer API

This commit is contained in:
Timon
2026-09-15 01:17:53 +00:00
parent 1f92109a80
commit edf263342c
5 changed files with 105 additions and 312 deletions
-1
View File
@@ -49,7 +49,6 @@ clap = { workspace = true, features = ["derive"] }
interprocess = "2.4.2"
fd-lock = "4.0.4"
ctrlc = "3.5.1"
window_clipboard = "0.5"
# Windows-specific dependencies
[target.'cfg(target_os = "windows")'.dependencies]
+45 -9
View File
@@ -9,7 +9,7 @@ use std::sync::mpsc::{Receiver, SyncSender};
use std::thread;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::data_transfer::TypeHint;
use winit::data_transfer::{DataTransferSendBuilder, TypeHint};
use winit::dpi::PhysicalSize;
use winit::event::{ElementState, MouseButton, StartCause, WindowEvent};
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
@@ -37,6 +37,7 @@ pub(crate) struct App {
window_fullscreen: bool,
window_pending_drag: bool,
pending_dnd_fetch: Option<AsyncRequestSerial>,
pending_clipboard_fetch: Option<AsyncRequestSerial>,
input_state: InputState,
ui_scale: f64,
app_event_receiver: Receiver<AppEvent>,
@@ -110,6 +111,7 @@ impl App {
window_fullscreen: false,
window_pending_drag: false,
pending_dnd_fetch: None,
pending_clipboard_fetch: None,
input_state: InputState::new(),
ui_scale: 1.,
app_event_receiver,
@@ -338,16 +340,10 @@ impl App {
}
}
DesktopFrontendMessage::ClipboardRead => {
if let Some(window) = &self.window {
let content = window.clipboard_read();
let message = DesktopWrapperMessage::ClipboardReadResult { content };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
self.app_event_scheduler.schedule(AppEvent::ClipboardRead);
}
DesktopFrontendMessage::ClipboardWrite { content } => {
if let Some(window) = &mut self.window {
window.clipboard_write(content);
}
self.app_event_scheduler.schedule(AppEvent::ClipboardWrite { content });
}
DesktopFrontendMessage::PointerLock => {
self.input_state.lock_pointer();
@@ -470,6 +466,32 @@ impl App {
self.ui_frame_received = true;
}
}
AppEvent::ClipboardRead => {
let result = event_loop.clipboard().and_then(|id| {
let Some(id) = id else { return Ok(None) };
let data_transfer = event_loop.data_transfer(id)?;
if !data_transfer.has_type(&TypeHint::Plaintext) {
return Ok(None);
}
event_loop.fetch_data_transfer(id, &TypeHint::Plaintext).map(Some)
});
match result {
Ok(Some(serial)) => self.pending_clipboard_fetch = Some(serial),
Ok(None) => self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::ClipboardReadResult { content: None }),
Err(e) => {
tracing::error!("Failed to read from clipboard: {e}");
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::ClipboardReadResult { content: None });
}
}
}
AppEvent::ClipboardWrite { content } => {
let send_data = DataTransferSendBuilder::new(content)
.with_type(TypeHint::Plaintext, |content: &String, _| Some(content.clone()))
.build();
if let Err(e) = event_loop.set_clipboard(send_data) {
tracing::error!("Failed to write to clipboard: {e}");
}
}
AppEvent::CursorChange(cursor) => {
if let Some(window) = &mut self.window {
window.set_cursor(event_loop, cursor);
@@ -615,6 +637,20 @@ impl ApplicationHandler for App {
Ok(serial) => self.pending_dnd_fetch = Some(serial),
Err(e) => tracing::error!("Failed to fetch dropped data: {e}"),
},
WindowEvent::DataTransferReceived { serial, ref value, .. } if self.pending_clipboard_fetch == Some(serial) => match value.try_as_string() {
Ok(content) => {
self.pending_clipboard_fetch = None;
let message = DesktopWrapperMessage::ClipboardReadResult { content: Some(content) };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => {
self.pending_clipboard_fetch = None;
tracing::error!("Failed to read from clipboard: {e}");
let message = DesktopWrapperMessage::ClipboardReadResult { content: None };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
},
WindowEvent::DataTransferReceived { serial, ref value, .. } if self.pending_dnd_fetch == Some(serial) => match value.try_as_file_paths() {
Ok(paths) => {
self.pending_dnd_fetch = None;
+4
View File
@@ -8,6 +8,10 @@ pub(crate) enum AppEvent {
WebCommunicationInitialized,
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
ClipboardRead,
ClipboardWrite {
content: String,
},
Exit,
UiCrashed,
OpenFiles(Vec<std::path::PathBuf>),
-33
View File
@@ -43,13 +43,6 @@ pub(crate) struct Window {
#[allow(dead_code)]
native_handle: native::NativeWindowImpl,
custom_cursors: HashMap<CustomCursorSource, CustomCursor>,
clipboard: Option<window_clipboard::Clipboard>,
}
impl Drop for Window {
fn drop(&mut self) {
// Clipboard must be dropped before `winit_window`
drop(self.clipboard.take());
}
}
impl Window {
@@ -70,12 +63,10 @@ impl Window {
let winit_window = event_loop.create_window(attributes).unwrap();
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
let clipboard = unsafe { window_clipboard::Clipboard::connect(&winit_window) }.ok();
Self {
winit_window: winit_window.into(),
native_handle,
custom_cursors: HashMap::new(),
clipboard,
}
}
@@ -208,28 +199,4 @@ impl Window {
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
self.native_handle.update_menu(entries);
}
pub(crate) fn clipboard_read(&self) -> Option<String> {
let Some(clipboard) = &self.clipboard else {
tracing::error!("Clipboard not available");
return None;
};
match clipboard.read() {
Ok(data) => Some(data),
Err(e) => {
tracing::error!("Failed to read from clipboard: {e}");
None
}
}
}
pub(crate) fn clipboard_write(&mut self, data: String) {
let Some(clipboard) = &mut self.clipboard else {
tracing::error!("Clipboard not available");
return;
};
if let Err(e) = clipboard.write(data) {
tracing::error!("Failed to write to clipboard: {e}")
}
}
}