mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 07:58:12 +08:00
Desktop: Switch to winit clipboard API (#4519)
* Desktop: Upgrade winit and port DnD to the new data transfer API * Desktop: Read and write the clipboard via the winit data transfer API
This commit is contained in:
Generated
+179
-350
File diff suppressed because it is too large
Load Diff
@@ -260,6 +260,7 @@ lto = "thin"
|
|||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
winit = { git = "https://github.com/timon-schelling/winit", branch = "graphite" }
|
||||||
rfd = { git = "https://github.com/timon-schelling/rfd.git", branch = "graphite" } # TODO: Remove this once https://github.com/PolyMeilex/rfd/pull/317 is merged and released
|
rfd = { git = "https://github.com/timon-schelling/rfd.git", branch = "graphite" } # TODO: Remove this once https://github.com/PolyMeilex/rfd/pull/317 is merged and released
|
||||||
cef = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-151" }
|
cef = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-151" }
|
||||||
cef-dll-sys = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-151" }
|
cef-dll-sys = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-151" }
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ clap = { workspace = true, features = ["derive"] }
|
|||||||
interprocess = "2.4.2"
|
interprocess = "2.4.2"
|
||||||
fd-lock = "4.0.4"
|
fd-lock = "4.0.4"
|
||||||
ctrlc = "3.5.1"
|
ctrlc = "3.5.1"
|
||||||
window_clipboard = "0.5"
|
|
||||||
|
|
||||||
# Windows-specific dependencies
|
# Windows-specific dependencies
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
|||||||
+80
-22
@@ -9,10 +9,11 @@ use std::sync::mpsc::{Receiver, SyncSender};
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::data_transfer::{DataTransferSendBuilder, TypeHint};
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::event::{ElementState, MouseButton, StartCause, WindowEvent};
|
use winit::event::{ElementState, MouseButton, StartCause, WindowEvent};
|
||||||
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
|
use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
|
||||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
use winit::event_loop::{ActiveEventLoop, AsyncRequestSerial, ControlFlow, DndAction, EventLoop};
|
||||||
use winit::window::WindowId;
|
use winit::window::WindowId;
|
||||||
|
|
||||||
use crate::dirs;
|
use crate::dirs;
|
||||||
@@ -35,6 +36,8 @@ pub(crate) struct App {
|
|||||||
window_maximized: bool,
|
window_maximized: bool,
|
||||||
window_fullscreen: bool,
|
window_fullscreen: bool,
|
||||||
window_pending_drag: bool,
|
window_pending_drag: bool,
|
||||||
|
pending_dnd_fetch: Option<AsyncRequestSerial>,
|
||||||
|
pending_clipboard_fetch: Option<AsyncRequestSerial>,
|
||||||
input_state: InputState,
|
input_state: InputState,
|
||||||
ui_scale: f64,
|
ui_scale: f64,
|
||||||
app_event_receiver: Receiver<AppEvent>,
|
app_event_receiver: Receiver<AppEvent>,
|
||||||
@@ -107,6 +110,8 @@ impl App {
|
|||||||
window_maximized: false,
|
window_maximized: false,
|
||||||
window_fullscreen: false,
|
window_fullscreen: false,
|
||||||
window_pending_drag: false,
|
window_pending_drag: false,
|
||||||
|
pending_dnd_fetch: None,
|
||||||
|
pending_clipboard_fetch: None,
|
||||||
input_state: InputState::new(),
|
input_state: InputState::new(),
|
||||||
ui_scale: 1.,
|
ui_scale: 1.,
|
||||||
app_event_receiver,
|
app_event_receiver,
|
||||||
@@ -335,16 +340,10 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::ClipboardRead => {
|
DesktopFrontendMessage::ClipboardRead => {
|
||||||
if let Some(window) = &self.window {
|
self.app_event_scheduler.schedule(AppEvent::ClipboardRead);
|
||||||
let content = window.clipboard_read();
|
|
||||||
let message = DesktopWrapperMessage::ClipboardReadResult { content };
|
|
||||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::ClipboardWrite { content } => {
|
DesktopFrontendMessage::ClipboardWrite { content } => {
|
||||||
if let Some(window) = &mut self.window {
|
self.app_event_scheduler.schedule(AppEvent::ClipboardWrite { content });
|
||||||
window.clipboard_write(content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::PointerLock => {
|
DesktopFrontendMessage::PointerLock => {
|
||||||
self.input_state.lock_pointer();
|
self.input_state.lock_pointer();
|
||||||
@@ -467,6 +466,32 @@ impl App {
|
|||||||
self.ui_frame_received = true;
|
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) => {
|
AppEvent::CursorChange(cursor) => {
|
||||||
if let Some(window) = &mut self.window {
|
if let Some(window) = &mut self.window {
|
||||||
window.set_cursor(event_loop, cursor);
|
window.set_cursor(event_loop, cursor);
|
||||||
@@ -538,7 +563,7 @@ impl ApplicationHandler for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||||
// Handle pointer lock release
|
// Handle pointer lock release
|
||||||
if let WindowEvent::PointerButton {
|
if let WindowEvent::PointerButton {
|
||||||
state: ElementState::Released,
|
state: ElementState::Released,
|
||||||
@@ -601,20 +626,53 @@ impl ApplicationHandler for App {
|
|||||||
self.exit(Some(ExitReason::UiAccelerationFailure));
|
self.exit(Some(ExitReason::UiAccelerationFailure));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::DragDropped { paths, .. } => {
|
WindowEvent::DragEntered { id, .. } => {
|
||||||
for path in paths {
|
let accepts = event_loop.data_transfer(id).is_ok_and(|data_transfer| data_transfer.has_type(&TypeHint::UriList));
|
||||||
match fs::read(&path) {
|
let actions: &[DndAction] = if accepts { &[DndAction::Copy] } else { &[] };
|
||||||
Ok(content) => {
|
if let Err(e) = event_loop.set_valid_dnd_actions(id, actions) {
|
||||||
let message = DesktopWrapperMessage::ImportFile { path, content };
|
tracing::error!("Failed to set valid drag and drop actions: {e}");
|
||||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WindowEvent::DragDropped { id, .. } => match event_loop.fetch_data_transfer(id, &TypeHint::UriList) {
|
||||||
|
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;
|
||||||
|
for path in paths {
|
||||||
|
match fs::read(&path) {
|
||||||
|
Ok(content) => {
|
||||||
|
let message = DesktopWrapperMessage::ImportFile { path, content };
|
||||||
|
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
|
||||||
|
Err(e) => {
|
||||||
|
self.pending_dnd_fetch = None;
|
||||||
|
tracing::error!("Failed to read dropped data: {e}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
WindowEvent::PointerMoved { .. } | WindowEvent::PointerLeft { position: Some(_), .. } | WindowEvent::PointerEntered { .. }
|
WindowEvent::PointerMoved { .. } | WindowEvent::PointerLeft { position: Some(_), .. } | WindowEvent::PointerEntered { .. }
|
||||||
if !self.input_state.pointer_locked() && self.window_pending_drag =>
|
if !self.input_state.pointer_locked() && self.window_pending_drag =>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ pub(crate) enum AppEvent {
|
|||||||
WebCommunicationInitialized,
|
WebCommunicationInitialized,
|
||||||
DesktopWrapperMessage(DesktopWrapperMessage),
|
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||||
|
ClipboardRead,
|
||||||
|
ClipboardWrite {
|
||||||
|
content: String,
|
||||||
|
},
|
||||||
Exit,
|
Exit,
|
||||||
UiCrashed,
|
UiCrashed,
|
||||||
OpenFiles(Vec<std::path::PathBuf>),
|
OpenFiles(Vec<std::path::PathBuf>),
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ impl InputState {
|
|||||||
let input = match delta {
|
let input = match delta {
|
||||||
MouseScrollDelta::LineDelta(x, y) => InputEvent::pointer().scrolled_lines(f64::from(*x), f64::from(*y)),
|
MouseScrollDelta::LineDelta(x, y) => InputEvent::pointer().scrolled_lines(f64::from(*x), f64::from(*y)),
|
||||||
MouseScrollDelta::PixelDelta(position) => InputEvent::pointer().scrolled_pixels(position.x, position.y),
|
MouseScrollDelta::PixelDelta(position) => InputEvent::pointer().scrolled_pixels(position.x, position.y),
|
||||||
|
_ => return,
|
||||||
};
|
};
|
||||||
ui_callback(input.modifiers(self.modifiers).build());
|
ui_callback(input.modifiers(self.modifiers).build());
|
||||||
return;
|
return;
|
||||||
@@ -229,6 +230,7 @@ impl InputState {
|
|||||||
let (x, y) = match delta {
|
let (x, y) = match delta {
|
||||||
MouseScrollDelta::LineDelta(x, y) => (f64::from(*x) * SCROLL_LINE_WIDTH, f64::from(*y) * SCROLL_LINE_HEIGHT),
|
MouseScrollDelta::LineDelta(x, y) => (f64::from(*x) * SCROLL_LINE_WIDTH, f64::from(*y) * SCROLL_LINE_HEIGHT),
|
||||||
MouseScrollDelta::PixelDelta(position) => (position.x, position.y),
|
MouseScrollDelta::PixelDelta(position) => (position.x, position.y),
|
||||||
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let scroll_delta = ScrollDelta::new(-x * SCROLL_SPEED_X, -y * SCROLL_SPEED_Y, 0.);
|
let scroll_delta = ScrollDelta::new(-x * SCROLL_SPEED_X, -y * SCROLL_SPEED_Y, 0.);
|
||||||
|
|||||||
@@ -43,13 +43,6 @@ pub(crate) struct Window {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
native_handle: native::NativeWindowImpl,
|
native_handle: native::NativeWindowImpl,
|
||||||
custom_cursors: HashMap<CustomCursorSource, CustomCursor>,
|
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 {
|
impl Window {
|
||||||
@@ -70,12 +63,10 @@ impl Window {
|
|||||||
|
|
||||||
let winit_window = event_loop.create_window(attributes).unwrap();
|
let winit_window = event_loop.create_window(attributes).unwrap();
|
||||||
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
|
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
|
||||||
let clipboard = unsafe { window_clipboard::Clipboard::connect(&winit_window) }.ok();
|
|
||||||
Self {
|
Self {
|
||||||
winit_window: winit_window.into(),
|
winit_window: winit_window.into(),
|
||||||
native_handle,
|
native_handle,
|
||||||
custom_cursors: HashMap::new(),
|
custom_cursors: HashMap::new(),
|
||||||
clipboard,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,28 +199,4 @@ impl Window {
|
|||||||
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
|
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
|
||||||
self.native_handle.update_menu(entries);
|
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user