diff --git a/Cargo.lock b/Cargo.lock index d8d6867328..86c97660c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -533,13 +533,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "block2" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2", + "objc2 0.6.3", ] [[package]] @@ -760,7 +769,7 @@ dependencies = [ "libloading 0.9.0", "metal", "objc", - "objc2", + "objc2 0.6.3", "objc2-io-surface", "thiserror 2.0.16", "tracing", @@ -890,6 +899,45 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "clipboard_macos" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" +dependencies = [ + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "clipboard_wayland" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +dependencies = [ + "smithay-clipboard", +] + +[[package]] +name = "clipboard_x11" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" +dependencies = [ + "thiserror 1.0.69", + "x11rb", +] + [[package]] name = "cmake" version = "0.1.54" @@ -1362,9 +1410,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.3", - "block2", + "block2 0.6.1", "libc", - "objc2", + "objc2 0.6.3", ] [[package]] @@ -1571,6 +1619,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "euclid" version = "0.22.11" @@ -1768,10 +1822,10 @@ dependencies = [ "icu_locale_core", "linebender_resource_handle", "memmap2", - "objc2", + "objc2 0.6.3", "objc2-core-foundation", "objc2-core-text", - "objc2-foundation", + "objc2-foundation 0.3.2", "read-fonts 0.35.0", "roxmltree", "smallvec", @@ -2281,9 +2335,9 @@ dependencies = [ "graphite-desktop-embedded-resources", "graphite-desktop-wrapper", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "open", "pidlock", "rand 0.9.2", @@ -2295,6 +2349,7 @@ dependencies = [ "tracing-subscriber", "vello", "wgpu", + "window_clipboard", "windows 0.58.0", "winit", ] @@ -3466,10 +3521,10 @@ dependencies = [ "crossbeam-channel", "dpi", "keyboard-types", - "objc2", - "objc2-app-kit", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "once_cell", "png", "thiserror 2.0.16", @@ -3790,6 +3845,22 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + [[package]] name = "objc2" version = "0.6.3" @@ -3799,6 +3870,22 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.3", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + [[package]] name = "objc2-app-kit" version = "0.3.2" @@ -3806,10 +3893,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.9.3", - "block2", - "objc2", + "block2 0.6.1", + "objc2 0.6.3", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.3", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3819,9 +3918,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.3", - "block2", + "block2 0.6.1", "dispatch2", - "objc2", + "objc2 0.6.3", ] [[package]] @@ -3835,6 +3934,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + [[package]] name = "objc2-core-text" version = "0.3.2" @@ -3862,6 +3973,18 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.3", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + [[package]] name = "objc2-foundation" version = "0.3.2" @@ -3869,8 +3992,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.3", - "block2", - "objc2", + "block2 0.6.1", + "objc2 0.6.3", "objc2-core-foundation", ] @@ -3882,9 +4005,34 @@ checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.9.3", "libc", - "objc2", + "objc2 0.6.3", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.3", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.3", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", ] [[package]] @@ -3894,9 +4042,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.9.3", - "objc2", + "objc2 0.6.3", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -4969,14 +5117,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd", - "block2", + "block2 0.6.1", "dispatch2", "js-sys", "log", - "objc2", - "objc2-app-kit", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "pollster", "raw-window-handle", "urlencoding", @@ -5523,6 +5671,17 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + [[package]] name = "smol_str" version = "0.3.2" @@ -7102,6 +7261,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window_clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5793d0b08c9e6a1240fe9ab2bd8db277487bf92436fd1a6321861a90a1b0cb7e" +dependencies = [ + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle", + "thiserror 1.0.69", +] + [[package]] name = "windows" version = "0.58.0" @@ -7603,15 +7776,15 @@ version = "0.30.12" source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b" dependencies = [ "bitflags 2.9.3", - "block2", + "block2 0.6.1", "dispatch2", "dpi", - "objc2", - "objc2-app-kit", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", "objc2-core-video", - "objc2-foundation", + "objc2-foundation 0.3.2", "raw-window-handle", "smol_str", "tracing", @@ -7625,7 +7798,7 @@ version = "0.30.12" source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b" dependencies = [ "memmap2", - "objc2", + "objc2 0.6.3", "objc2-core-foundation", "smol_str", "tracing", @@ -7670,12 +7843,12 @@ version = "0.30.12" source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b" dependencies = [ "bitflags 2.9.3", - "block2", + "block2 0.6.1", "dispatch2", "dpi", - "objc2", + "objc2 0.6.3", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "objc2-ui-kit", "raw-window-handle", "serde", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 36bd0d4a05..ac45e88814 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -45,6 +45,7 @@ serde = { workspace = true } clap = { workspace = true, features = ["derive"] } pidlock = "0.2.2" ctrlc = "3.5.1" +window_clipboard = "0.5" # Windows-specific dependencies [target.'cfg(target_os = "windows")'.dependencies] @@ -64,4 +65,3 @@ objc2 = { version = "0.6.1", default-features = false } objc2-foundation = { version = "0.3.2", default-features = false } objc2-app-kit = { version = "0.3.2", default-features = false } muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false } - diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 0e46aa6998..a33bec6bda 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -259,6 +259,18 @@ impl App { window.update_menu(entries); } } + 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)); + } + } + DesktopFrontendMessage::ClipboardWrite { content } => { + if let Some(window) = &mut self.window { + window.clipboard_write(content); + } + } DesktopFrontendMessage::WindowClose => { self.app_event_scheduler.schedule(AppEvent::CloseWindow); } diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index fef0e1e430..d70550d0a5 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -91,6 +91,10 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat key_event.character = event.logical_key.to_char_representation() as u16; + if event.state == ElementState::Pressed && key_event.character != 0 { + key_event.type_ = cef_key_event_type_t::KEYEVENT_CHAR.into(); + } + // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. // // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. diff --git a/desktop/src/render/composite_shader.wgsl b/desktop/src/render/composite_shader.wgsl index f49144be74..98cecfc6cb 100644 --- a/desktop/src/render/composite_shader.wgsl +++ b/desktop/src/render/composite_shader.wgsl @@ -61,7 +61,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { } let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate); - let viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate); + var viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate); + + if (viewport_srgb.a < 0.001) { + viewport_srgb = constants.background_color; + } if (overlay_srgb.a < 0.001) { if (ui_srgb.a < 0.001) { diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 549d454d0c..27ef43ab1d 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -38,6 +38,7 @@ pub(crate) struct Window { #[allow(dead_code)] native_handle: native::NativeWindowImpl, custom_cursors: HashMap, + clipboard: window_clipboard::Clipboard, } impl Window { @@ -57,10 +58,12 @@ 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) }.expect("failed to create clipboard"); Self { winit_window: winit_window.into(), native_handle, custom_cursors: HashMap::new(), + clipboard, } } @@ -136,6 +139,22 @@ impl Window { pub(crate) fn update_menu(&self, entries: Vec) { self.native_handle.update_menu(entries); } + + pub(crate) fn clipboard_read(&self) -> Option { + match self.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) { + if let Err(e) = self.clipboard.write(data) { + tracing::error!("Failed to write to clipboard: {e}") + } + } } pub(crate) enum Cursor { diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index 4f0b5c9cf1..721bd38743 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -1,6 +1,7 @@ use graphene_std::Color; use graphene_std::raster::Image; use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform; +use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw; use graphite_editor::messages::prelude::*; use super::DesktopWrapperMessageDispatcher; @@ -156,5 +157,13 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess } #[cfg(not(target_os = "macos"))] DesktopWrapperMessage::MenuEvent { id: _ } => {} + DesktopWrapperMessage::ClipboardReadResult { content } => { + if let Some(content) = content { + let message = ClipboardMessage::ReadClipboard { + content: ClipboardContentRaw::Text(content), + }; + dispatcher.queue_editor_message(message); + } + } } } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 5b049eac34..bc6398b365 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -126,6 +126,12 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD _ => {} } } + FrontendMessage::TriggerClipboardRead => { + dispatcher.respond(DesktopFrontendMessage::ClipboardRead); + } + FrontendMessage::TriggerClipboardWrite { content } => { + dispatcher.respond(DesktopFrontendMessage::ClipboardWrite { content }); + } FrontendMessage::WindowClose => { dispatcher.respond(DesktopFrontendMessage::WindowClose); } diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 8692d6df0e..a92ab0d5a8 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -54,6 +54,10 @@ pub enum DesktopFrontendMessage { UpdateMenu { entries: Vec, }, + ClipboardRead, + ClipboardWrite { + content: String, + }, WindowClose, WindowMinimize, WindowMaximize, @@ -114,6 +118,9 @@ pub enum DesktopWrapperMessage { MenuEvent { id: String, }, + ClipboardReadResult { + content: Option, + }, } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 789180cfcd..ae437d11bf 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -18,6 +18,7 @@ pub struct DispatcherMessageHandlers { animation_message_handler: AnimationMessageHandler, app_window_message_handler: AppWindowMessageHandler, broadcast_message_handler: BroadcastMessageHandler, + clipboard_message_handler: ClipboardMessageHandler, debug_message_handler: DebugMessageHandler, defer_message_handler: DeferMessageHandler, dialog_message_handler: DialogMessageHandler, @@ -158,6 +159,7 @@ impl Dispatcher { self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ()); } Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()), + Message::Clipboard(message) => self.message_handlers.clipboard_message_handler.process_message(message, &mut queue, ()), Message::Debug(message) => { self.message_handlers.debug_message_handler.process_message(message, &mut queue, ()); } @@ -319,6 +321,7 @@ impl Dispatcher { // TODO: Reduce the number of heap allocations let mut list = Vec::new(); list.extend(self.message_handlers.app_window_message_handler.actions()); + list.extend(self.message_handlers.clipboard_message_handler.actions()); list.extend(self.message_handlers.dialog_message_handler.actions()); list.extend(self.message_handlers.animation_message_handler.actions()); list.extend(self.message_handlers.input_preprocessor_message_handler.actions()); diff --git a/editor/src/messages/clipboard/clipboard_message.rs b/editor/src/messages/clipboard/clipboard_message.rs new file mode 100644 index 0000000000..920028a748 --- /dev/null +++ b/editor/src/messages/clipboard/clipboard_message.rs @@ -0,0 +1,13 @@ +use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw}; +use crate::messages::prelude::*; + +#[impl_message(Message, Clipboard)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum ClipboardMessage { + Cut, + Copy, + Paste, + ReadClipboard { content: ClipboardContentRaw }, + ReadSelection { content: Option, cut: bool }, + Write { content: ClipboardContent }, +} diff --git a/editor/src/messages/clipboard/clipboard_message_handler.rs b/editor/src/messages/clipboard/clipboard_message_handler.rs new file mode 100644 index 0000000000..57d6f31a4a --- /dev/null +++ b/editor/src/messages/clipboard/clipboard_message_handler.rs @@ -0,0 +1,85 @@ +use crate::messages::clipboard::utility_types::{ClipboardContent, ClipboardContentRaw}; +use crate::messages::prelude::*; +use graphene_std::raster::Image; +use graphite_proc_macros::{ExtractField, message_handler_data}; + +const CLIPBOARD_PREFIX_LAYER: &str = "graphite/layer: "; +const CLIPBOARD_PREFIX_NODES: &str = "graphite/nodes: "; +const CLIPBOARD_PREFIX_VECTOR: &str = "graphite/vector: "; + +#[derive(Debug, Clone, Default, ExtractField)] +pub struct ClipboardMessageHandler {} + +#[message_handler_data] +impl MessageHandler for ClipboardMessageHandler { + fn process_message(&mut self, message: ClipboardMessage, responses: &mut std::collections::VecDeque, _: ()) { + match message { + ClipboardMessage::Cut => responses.add(FrontendMessage::TriggerSelectionRead { cut: true }), + ClipboardMessage::Copy => responses.add(FrontendMessage::TriggerSelectionRead { cut: false }), + ClipboardMessage::Paste => responses.add(FrontendMessage::TriggerClipboardRead), + ClipboardMessage::ReadClipboard { content } => match content { + ClipboardContentRaw::Text(text) => { + if let Some(layer) = text.strip_prefix(CLIPBOARD_PREFIX_LAYER) { + responses.add(PortfolioMessage::PasteSerializedData { data: layer.to_string() }); + } else if let Some(nodes) = text.strip_prefix(CLIPBOARD_PREFIX_NODES) { + responses.add(NodeGraphMessage::PasteNodes { serialized_nodes: nodes.to_string() }); + } else if let Some(vector) = text.strip_prefix(CLIPBOARD_PREFIX_VECTOR) { + responses.add(PortfolioMessage::PasteSerializedVector { data: vector.to_string() }); + } else { + responses.add(FrontendMessage::TriggerSelectionWrite { content: text }); + } + } + ClipboardContentRaw::Svg(svg) => { + responses.add(PortfolioMessage::PasteSvg { + svg, + name: None, + mouse: None, + parent_and_insert_index: None, + }); + } + ClipboardContentRaw::Image { data, width, height } => { + responses.add(PortfolioMessage::PasteImage { + image: Image::from_image_data(&data, width, height), + name: None, + mouse: None, + parent_and_insert_index: None, + }); + } + }, + ClipboardMessage::ReadSelection { content, cut } => { + use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; + if let Some(text) = content { + responses.add(ClipboardMessage::Write { + content: ClipboardContent::Text(text), + }); + } else if cut { + responses.add(PortfolioMessage::Cut { clipboard: Clipboard::Device }); + } else { + responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Device }); + } + } + ClipboardMessage::Write { content } => { + let text = match content { + ClipboardContent::Svg(_) => { + log::error!("SVG copying is not yet supported"); + return; + } + ClipboardContent::Image { .. } => { + log::error!("Image copying is not yet supported"); + return; + } + ClipboardContent::Layer(layer) => format!("{CLIPBOARD_PREFIX_LAYER}{layer}"), + ClipboardContent::Nodes(nodes) => format!("{CLIPBOARD_PREFIX_NODES}{nodes}"), + ClipboardContent::Vector(vector) => format!("{CLIPBOARD_PREFIX_VECTOR}{vector}"), + ClipboardContent::Text(text) => text, + }; + responses.add(FrontendMessage::TriggerClipboardWrite { content: text }); + } + } + } + advertise_actions!(ClipboardMessageDiscriminant; + Cut, + Copy, + Paste, + ); +} diff --git a/editor/src/messages/clipboard/mod.rs b/editor/src/messages/clipboard/mod.rs new file mode 100644 index 0000000000..e090860708 --- /dev/null +++ b/editor/src/messages/clipboard/mod.rs @@ -0,0 +1,8 @@ +mod clipboard_message; +pub mod clipboard_message_handler; +pub mod utility_types; + +#[doc(inline)] +pub use clipboard_message::{ClipboardMessage, ClipboardMessageDiscriminant}; +#[doc(inline)] +pub use clipboard_message_handler::ClipboardMessageHandler; diff --git a/editor/src/messages/clipboard/utility_types.rs b/editor/src/messages/clipboard/utility_types.rs new file mode 100644 index 0000000000..d32319098e --- /dev/null +++ b/editor/src/messages/clipboard/utility_types.rs @@ -0,0 +1,16 @@ +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum ClipboardContentRaw { + Text(String), + Svg(String), + Image { data: Vec, width: u32, height: u32 }, +} + +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum ClipboardContent { + Layer(String), + Nodes(String), + Vector(String), + Text(String), + Svg(String), + Image { data: Vec, width: u32, height: u32 }, +} diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 89165f238b..dc875f139b 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -66,7 +66,7 @@ pub enum FrontendMessage { shortcut: Option, }, - // Trigger prefix: cause a browser API to do something + // Trigger prefix: cause a frontend specific API to do something TriggerAboutGraphiteLocalizedCommitDate { #[serde(rename = "commitDate")] commit_date: String, @@ -111,7 +111,6 @@ pub enum FrontendMessage { TriggerOpenLaunchDocuments, TriggerLoadPreferences, TriggerOpenDocument, - TriggerPaste, TriggerSavePreferences { preferences: PreferencesMessageHandler, }, @@ -120,13 +119,19 @@ pub enum FrontendMessage { document_id: DocumentId, }, TriggerTextCommit, - TriggerTextCopy { - #[serde(rename = "copyText")] - copy_text: String, - }, TriggerVisitLink { url: String, }, + TriggerClipboardRead, + TriggerClipboardWrite { + content: String, + }, + TriggerSelectionRead { + cut: bool, + }, + TriggerSelectionWrite { + content: String, + }, // Update prefix: give the frontend a new value or state for it to use UpdateActiveDocument { @@ -330,12 +335,15 @@ pub enum FrontendMessage { width: f64, height: f64, }, + #[cfg(not(target_family = "wasm"))] RenderOverlays { #[serde(skip, default = "OverlayContext::default")] #[derivative(Debug = "ignore", PartialEq = "ignore")] context: OverlayContext, }, + + // Window prefix: cause the application window to do something WindowClose, WindowMinimize, WindowMaximize, diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 34d6ed0b3b..9e774b6c25 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -54,6 +54,11 @@ pub fn input_mappings() -> Mapping { // Hack to prevent Left Click + Accel + Z combo (this effectively blocks you from making a double undo with AbortTransaction) entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop), // + // ClipboardMessage + entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut), + entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=ClipboardMessage::Copy), + entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=ClipboardMessage::Paste), + // // NodeGraphMessage entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: false }), entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: false, alt_click: false, right_click: false }), @@ -433,9 +438,6 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers), entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen), // - // FrontendMessage - entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste), - // // DialogMessage entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog), entry!(KeyDown(KeyN); modifiers=[Accel], action_dispatch=DialogMessage::RequestNewDocumentDialog), diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index f4337e9660..b06b7397a7 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -1,7 +1,6 @@ use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::prelude::*; use graphene_std::path_bool::BooleanOperation; @@ -196,20 +195,20 @@ impl LayoutHolder for MenuBarMessageHandler { MenuListEntry::new("Cut") .label("Cut") .icon("Cut") - .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Cut)) - .on_commit(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()) + .tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Cut)) + .on_commit(|_| ClipboardMessage::Cut.into()) .disabled(no_active_document || !has_selected_layers), MenuListEntry::new("Copy") .label("Copy") .icon("Copy") - .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Copy)) - .on_commit(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()) + .tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Copy)) + .on_commit(|_| ClipboardMessage::Copy.into()) .disabled(no_active_document || !has_selected_layers), MenuListEntry::new("Paste") .label("Paste") .icon("Paste") - .tooltip_shortcut(action_shortcut!(FrontendMessageDiscriminant::TriggerPaste)) - .on_commit(|_| FrontendMessage::TriggerPaste.into()) + .tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Paste)) + .on_commit(|_| ClipboardMessage::Paste.into()) .disabled(no_active_document), ], vec![ diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index a561634267..4b104495ee 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -12,6 +12,8 @@ pub enum Message { #[child] Broadcast(BroadcastMessage), #[child] + Clipboard(ClipboardMessage), + #[child] Debug(DebugMessage), #[child] Defer(DeferMessage), diff --git a/editor/src/messages/mod.rs b/editor/src/messages/mod.rs index a6430e9690..7edfea806e 100644 --- a/editor/src/messages/mod.rs +++ b/editor/src/messages/mod.rs @@ -3,6 +3,7 @@ pub mod animation; pub mod app_window; pub mod broadcast; +pub mod clipboard; pub mod debug; pub mod defer; pub mod dialog; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index d27999b554..699e898de5 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1,6 +1,7 @@ use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode}; use super::{document_node_definitions, node_properties}; use crate::consts::GRID_SIZE; +use crate::messages::clipboard::utility_types::ClipboardContent; use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; @@ -237,11 +238,13 @@ impl<'a> MessageHandler> for NodeG let new_ids = &all_selected_nodes.iter().enumerate().map(|(new, old)| (*old, NodeId(new as u64))).collect(); let copied_nodes = network_interface.copy_nodes(new_ids, selection_network_path).collect::>(); - // Prefix to show that these are nodes - let mut copy_text = String::from("graphite/nodes: "); - copy_text += &serde_json::to_string(&copied_nodes).expect("Could not serialize copy"); - - responses.add(FrontendMessage::TriggerTextCopy { copy_text }); + let Ok(data) = serde_json::to_string(&copied_nodes) else { + log::error!("Failed to serialize nodes for clipboard"); + return; + }; + responses.add(ClipboardMessage::Write { + content: ClipboardContent::Nodes(data), + }); } NodeGraphMessage::CreateNodeInLayerNoTransaction { node_type, layer } => { let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) else { diff --git a/editor/src/messages/portfolio/document/utility_types/clipboards.rs b/editor/src/messages/portfolio/document/utility_types/clipboards.rs index 158372cd40..ef4b00ae7c 100644 --- a/editor/src/messages/portfolio/document/utility_types/clipboards.rs +++ b/editor/src/messages/portfolio/document/utility_types/clipboards.rs @@ -5,10 +5,9 @@ use graph_craft::document::NodeId; #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug, specta::Type)] pub enum Clipboard { Internal, + Device, _InternalClipboardCount, // Keep this as the last entry of **internal** clipboards since it is used for counting the number of enum variants - - Device, } pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index b13b75d67f..ed485a1f06 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -6469,7 +6469,7 @@ mod network_interface_tests { let serialized_nodes = frontend_messages .into_iter() .find_map(|msg| match msg { - FrontendMessage::TriggerTextCopy { copy_text } => Some(copy_text), + FrontendMessage::TriggerClipboardWrite { content } => Some(content), _ => None, }) .expect("copy message should be dispatched") diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index d1a088ba97..ee4f9acff2 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -4,6 +4,7 @@ use super::utility_types::{PanelType, PersistentData}; use crate::application::generate_uuid; use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; use crate::messages::animation::TimingInformation; +use crate::messages::clipboard::utility_types::ClipboardContent; use crate::messages::dialog::simple_dialogs; use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument}; use crate::messages::input_mapper::utility_types::input_keyboard::Key; @@ -243,11 +244,21 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::Copy { clipboard } => { + if context.current_tool == &ToolType::Path { + responses.add(PathToolMessage::Copy { clipboard }); + return; + } + // We can't use `self.active_document()` because it counts as an immutable borrow of the entirety of `self` let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else { return; }; + if active_document.graph_view_overlay_open() { + responses.add(NodeGraphMessage::Copy); + return; + } + let mut copy_val = |buffer: &mut Vec| { let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::>(); @@ -283,10 +294,13 @@ impl MessageHandler> for Portfolio if clipboard == Clipboard::Device { let mut buffer = Vec::new(); copy_val(&mut buffer); - let mut copy_text = String::from("graphite/layer: "); - copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste"); - - responses.add(FrontendMessage::TriggerTextCopy { copy_text }); + let Ok(data) = serde_json::to_string(&buffer) else { + log::error!("Failed to serialize nodes for clipboard"); + return; + }; + responses.add(ClipboardMessage::Write { + content: ClipboardContent::Layer(data), + }); } else { let copy_buffer = &mut self.copy_buffer; copy_buffer[clipboard as usize].clear(); @@ -294,6 +308,18 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::Cut { clipboard } => { + if context.current_tool == &ToolType::Path { + responses.add(PathToolMessage::Cut { clipboard }); + return; + } + + if let Some(active_document) = self.active_document() + && active_document.graph_view_overlay_open() + { + responses.add(NodeGraphMessage::Copy); + return; + } + responses.add(PortfolioMessage::Copy { clipboard }); responses.add(DocumentMessage::DeleteSelectedLayers); } diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 871334cd3e..f64cbf0ee9 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -7,6 +7,7 @@ pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscrimin pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler}; pub use crate::messages::broadcast::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler}; pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler}; +pub use crate::messages::clipboard::{ClipboardMessage, ClipboardMessageDiscriminant, ClipboardMessageHandler}; pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler}; pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler}; pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler}; diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index c976047153..ea7843a923 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -100,7 +100,7 @@ impl Fsm for EyedropperToolFsmState { // Sampling -> Sampling (EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::PointerMove) => { let mouse_position = viewport.logical(input.mouse.position); - if viewport.is_in_bounds(mouse_position) { + if viewport.is_in_bounds(mouse_position + viewport.offset()) { update_cursor_preview(responses, input, global_tool_data, None); } else { disable_cursor_preview(responses); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index be300ebe5c..5bf09bd7a3 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -4,6 +4,7 @@ use crate::consts::{ COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, }; +use crate::messages::clipboard::utility_types::ClipboardContent; use crate::messages::input_mapper::utility_types::macros::action_shortcut_manual; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; @@ -2732,10 +2733,13 @@ impl Fsm for PathToolFsmState { } if clipboard == Clipboard::Device { - let mut copy_text = String::from("graphite/vector: "); - copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste"); - - responses.add(FrontendMessage::TriggerTextCopy { copy_text }); + if let Ok(data) = serde_json::to_string(&buffer) { + responses.add(ClipboardMessage::Write { + content: ClipboardContent::Vector(data), + }); + } else { + log::error!("Failed to serialize nodes for clipboard"); + } } // TODO: Add implementation for internal clipboard diff --git a/frontend/src/io-managers/clipboard.ts b/frontend/src/io-managers/clipboard.ts index 8fa457398a..8d5db4c644 100644 --- a/frontend/src/io-managers/clipboard.ts +++ b/frontend/src/io-managers/clipboard.ts @@ -1,10 +1,97 @@ import { type Editor } from "@graphite/editor"; -import { TriggerTextCopy } from "@graphite/messages"; +import { TriggerClipboardWrite, TriggerSelectionRead, TriggerSelectionWrite } from "@graphite/messages"; export function createClipboardManager(editor: Editor) { // Subscribe to process backend event - editor.subscriptions.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => { + editor.subscriptions.subscribeJsMessage(TriggerClipboardWrite, (triggerTextCopy) => { // If the Clipboard API is supported in the browser, copy text to the clipboard - navigator.clipboard?.writeText?.(triggerTextCopy.copyText); + navigator.clipboard?.writeText?.(triggerTextCopy.content); }); + editor.subscriptions.subscribeJsMessage(TriggerSelectionRead, async (data) => { + editor.handle.readSelection(readAtCaret(data.cut), data.cut); + }); + editor.subscriptions.subscribeJsMessage(TriggerSelectionWrite, async (data) => { + insertAtCaret(data.content); + }); +} + +function readAtCaret(cut: boolean): string | undefined { + const element = window.document.activeElement; + + if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { + const start = element.selectionStart; + const end = element.selectionEnd; + + if ((!start && start !== 0) || (!end && end !== 0) || start === end) { + return undefined; + } + + const value = element.value; + const selectedText = value.slice(start, end); + + if (cut) { + element.value = value.slice(0, start) + value.slice(end); + + element.selectionStart = element.selectionEnd = start; + element.dispatchEvent(new Event("input", { bubbles: true })); + } + + return selectedText; + } + + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + return undefined; + } + + const selectedText = selection.toString(); + if (!selectedText) return undefined; + + if (cut) { + const range = selection.getRangeAt(0); + range.deleteContents(); + + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } + + return selectedText; +} + +function insertAtCaret(text: string) { + const element = window.document.activeElement; + + if (!element) return; + + if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { + const start = element.selectionStart; + const end = element.selectionEnd; + + if ((!start && start !== 0) || (!end && end !== 0)) return; + + const value = element.value; + + element.value = value.slice(0, start) + text + value.slice(end); + + const newPos = start + text.length; + element.selectionStart = element.selectionEnd = newPos; + } else if (element instanceof HTMLElement && element.isContentEditable) { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + range.deleteContents(); + + const textNode = window.document.createTextNode(text); + range.insertNode(textNode); + + range.setStartAfter(textNode); + range.collapse(true); + + selection.removeAllRanges(); + selection.addRange(range); + } + + element.dispatchEvent(new Event("input", { bubbles: true })); } diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 5054bb4525..8982f5784f 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -1,13 +1,13 @@ import { get } from "svelte/store"; import { type Editor } from "@graphite/editor"; -import { TriggerPaste } from "@graphite/messages"; +import { TriggerClipboardRead } from "@graphite/messages"; import { type DialogState } from "@graphite/state-providers/dialog"; import { type DocumentState } from "@graphite/state-providers/document"; import { type FullscreenState } from "@graphite/state-providers/fullscreen"; import { type PortfolioState } from "@graphite/state-providers/portfolio"; import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry"; -import { operatingSystem } from "@graphite/utility-functions/platform"; +import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform"; import { extractPixelData } from "@graphite/utility-functions/rasterization"; import { stripIndents } from "@graphite/utility-functions/strip-indents"; @@ -82,10 +82,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // TODO: Switch to a system where everything is sent to the backend, then the input preprocessor makes decisions and kicks some inputs back to the frontend const accelKey = operatingSystem() === "Mac" ? e.metaKey : e.ctrlKey; + // Cut, copy, and paste is handled in the backend on desktop + if (isDesktop() && accelKey && ["KeyX", "KeyC", "KeyV"].includes(key)) return true; + // Don't redirect user input from text entry into HTML elements if (targetIsTextField(e.target || undefined) && key !== "Escape" && !(accelKey && ["Enter", "NumpadEnter"].includes(key))) return false; - // Don't redirect paste + // Don't redirect paste in web if (key === "KeyV" && accelKey) return false; // Don't redirect a fullscreen request @@ -306,20 +309,10 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (!dataTransfer || targetIsTextField(e.target || undefined)) return; e.preventDefault(); - const LAYER_DATA = "graphite/layer: "; - const NODES_DATA = "graphite/nodes: "; - const VECTOR_DATA = "graphite/vector: "; - Array.from(dataTransfer.items).forEach(async (item) => { if (item.type === "text/plain") { item.getAsString((text) => { - if (text.startsWith(LAYER_DATA)) { - editor.handle.pasteSerializedData(text.substring(LAYER_DATA.length, text.length)); - } else if (text.startsWith(NODES_DATA)) { - editor.handle.pasteSerializedNodes(text.substring(NODES_DATA.length, text.length)); - } else if (text.startsWith(VECTOR_DATA)) { - editor.handle.pasteSerializedVector(text.substring(VECTOR_DATA.length, text.length)); - } + editor.handle.pasteText(text); }); } @@ -413,7 +406,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Frontend message subscriptions - editor.subscriptions.subscribeJsMessage(TriggerPaste, async () => { + editor.subscriptions.subscribeJsMessage(TriggerClipboardRead, async () => { // In the try block, attempt to read from the Clipboard API, which may not have permission and may not be supported in all browsers // In the catch block, explain to the user why the paste failed and how to fix or work around the problem try { @@ -437,10 +430,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const reader = new FileReader(); reader.onload = () => { const text = reader.result as string; - - if (text.startsWith("graphite/layer: ")) { - editor.handle.pasteSerializedData(text.substring(16, text.length)); - } + editor.handle.pasteText(text); }; reader.readAsText(blob); return true; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index de3df14cf7..e7fa5efca2 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -725,7 +725,7 @@ export class TriggerOpenDocument extends JsMessage {} export class TriggerImport extends JsMessage {} -export class TriggerPaste extends JsMessage {} +export class TriggerClipboardRead extends JsMessage {} export class TriggerSaveDocument extends JsMessage { readonly documentId!: bigint; @@ -868,8 +868,16 @@ export class TriggerVisitLink extends JsMessage { export class TriggerTextCommit extends JsMessage {} -export class TriggerTextCopy extends JsMessage { - readonly copyText!: string; +export class TriggerClipboardWrite extends JsMessage { + readonly content!: string; +} + +export class TriggerSelectionRead extends JsMessage { + readonly cut!: boolean; +} + +export class TriggerSelectionWrite extends JsMessage { + readonly content!: string; } export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage { @@ -1695,7 +1703,6 @@ export const messageMakers: Record = { TriggerLoadRestAutoSaveDocuments, TriggerOpenDocument, TriggerOpenLaunchDocuments, - TriggerPaste, TriggerPersistenceRemoveDocument, TriggerPersistenceWriteDocument, TriggerSaveActiveDocument, @@ -1703,7 +1710,10 @@ export const messageMakers: Record = { TriggerSaveFile, TriggerSavePreferences, TriggerTextCommit, - TriggerTextCopy, + TriggerClipboardRead, + TriggerClipboardWrite, + TriggerSelectionRead, + TriggerSelectionWrite, TriggerVisitLink, UpdateActiveDocument, UpdateBox, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index b020b09aa8..05d02bf20f 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -7,6 +7,7 @@ use crate::helpers::translate_key; use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER}; use editor::consts::FILE_EXTENSION; +use editor::messages::clipboard::utility_types::ClipboardContentRaw; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta}; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -616,20 +617,6 @@ impl EditorHandle { Ok(()) } - /// Paste layers from a serialized JSON representation - #[wasm_bindgen(js_name = pasteSerializedData)] - pub fn paste_serialized_data(&self, data: String) { - let message = PortfolioMessage::PasteSerializedData { data }; - self.dispatch(message); - } - - /// Paste vector into a new layer from a serialized JSON representation - #[wasm_bindgen(js_name = pasteSerializedVector)] - pub fn paste_serialized_vector(&self, data: String) { - let message = PortfolioMessage::PasteSerializedVector { data }; - self.dispatch(message); - } - #[wasm_bindgen(js_name = clipLayer)] pub fn clip_layer(&self, id: u64) { let id = NodeId(id); @@ -726,10 +713,19 @@ impl EditorHandle { self.dispatch(message); } - /// Pastes the nodes based on serialized data - #[wasm_bindgen(js_name = pasteSerializedNodes)] - pub fn paste_serialized_nodes(&self, serialized_nodes: String) { - let message = NodeGraphMessage::PasteNodes { serialized_nodes }; + /// Respond to selection read + #[wasm_bindgen(js_name = readSelection)] + pub fn read_selection(&self, content: Option, cut: bool) { + let message = ClipboardMessage::ReadSelection { content, cut }; + self.dispatch(message); + } + + /// Paste from a serialized JSON representation + #[wasm_bindgen(js_name = pasteText)] + pub fn paste_text(&self, data: String) { + let message = ClipboardMessage::ReadClipboard { + content: ClipboardContentRaw::Text(data), + }; self.dispatch(message); }