Desktop: Text clipboard support (#3461)

* gray background for viewport texture

* cust copy paste support

* connect clipboard read on web

* fix eyedropper bounds

* cleanup

* add missing char events for some named keys like enter
This commit is contained in:
Timon 2025-12-12 00:16:35 +00:00 committed by GitHub
parent d6c06da878
commit 6d852f11af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 602 additions and 115 deletions

243
Cargo.lock generated
View File

@ -533,13 +533,22 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "block2" name = "block2"
version = "0.6.1" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.3",
] ]
[[package]] [[package]]
@ -760,7 +769,7 @@ dependencies = [
"libloading 0.9.0", "libloading 0.9.0",
"metal", "metal",
"objc", "objc",
"objc2", "objc2 0.6.3",
"objc2-io-surface", "objc2-io-surface",
"thiserror 2.0.16", "thiserror 2.0.16",
"tracing", "tracing",
@ -890,6 +899,45 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 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]] [[package]]
name = "cmake" name = "cmake"
version = "0.1.54" version = "0.1.54"
@ -1362,9 +1410,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"block2", "block2 0.6.1",
"libc", "libc",
"objc2", "objc2 0.6.3",
] ]
[[package]] [[package]]
@ -1571,6 +1619,12 @@ dependencies = [
"windows-sys 0.60.2", "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]] [[package]]
name = "euclid" name = "euclid"
version = "0.22.11" version = "0.22.11"
@ -1768,10 +1822,10 @@ dependencies = [
"icu_locale_core", "icu_locale_core",
"linebender_resource_handle", "linebender_resource_handle",
"memmap2", "memmap2",
"objc2", "objc2 0.6.3",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-text", "objc2-core-text",
"objc2-foundation", "objc2-foundation 0.3.2",
"read-fonts 0.35.0", "read-fonts 0.35.0",
"roxmltree", "roxmltree",
"smallvec", "smallvec",
@ -2281,9 +2335,9 @@ dependencies = [
"graphite-desktop-embedded-resources", "graphite-desktop-embedded-resources",
"graphite-desktop-wrapper", "graphite-desktop-wrapper",
"muda", "muda",
"objc2", "objc2 0.6.3",
"objc2-app-kit", "objc2-app-kit 0.3.2",
"objc2-foundation", "objc2-foundation 0.3.2",
"open", "open",
"pidlock", "pidlock",
"rand 0.9.2", "rand 0.9.2",
@ -2295,6 +2349,7 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"vello", "vello",
"wgpu", "wgpu",
"window_clipboard",
"windows 0.58.0", "windows 0.58.0",
"winit", "winit",
] ]
@ -3466,10 +3521,10 @@ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"dpi", "dpi",
"keyboard-types", "keyboard-types",
"objc2", "objc2 0.6.3",
"objc2-app-kit", "objc2-app-kit 0.3.2",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.2",
"once_cell", "once_cell",
"png", "png",
"thiserror 2.0.16", "thiserror 2.0.16",
@ -3790,6 +3845,22 @@ dependencies = [
"malloc_buf", "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]] [[package]]
name = "objc2" name = "objc2"
version = "0.6.3" version = "0.6.3"
@ -3799,6 +3870,22 @@ dependencies = [
"objc2-encode", "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]] [[package]]
name = "objc2-app-kit" name = "objc2-app-kit"
version = "0.3.2" version = "0.3.2"
@ -3806,10 +3893,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"block2", "block2 0.6.1",
"objc2", "objc2 0.6.3",
"objc2-core-foundation", "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]] [[package]]
@ -3819,9 +3918,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"block2", "block2 0.6.1",
"dispatch2", "dispatch2",
"objc2", "objc2 0.6.3",
] ]
[[package]] [[package]]
@ -3835,6 +3934,18 @@ dependencies = [
"objc2-core-foundation", "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]] [[package]]
name = "objc2-core-text" name = "objc2-core-text"
version = "0.3.2" version = "0.3.2"
@ -3862,6 +3973,18 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" 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]] [[package]]
name = "objc2-foundation" name = "objc2-foundation"
version = "0.3.2" version = "0.3.2"
@ -3869,8 +3992,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"block2", "block2 0.6.1",
"objc2", "objc2 0.6.3",
"objc2-core-foundation", "objc2-core-foundation",
] ]
@ -3882,9 +4005,34 @@ checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"libc", "libc",
"objc2", "objc2 0.6.3",
"objc2-core-foundation", "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]] [[package]]
@ -3894,9 +4042,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"objc2", "objc2 0.6.3",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.2",
] ]
[[package]] [[package]]
@ -4969,14 +5117,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
dependencies = [ dependencies = [
"ashpd", "ashpd",
"block2", "block2 0.6.1",
"dispatch2", "dispatch2",
"js-sys", "js-sys",
"log", "log",
"objc2", "objc2 0.6.3",
"objc2-app-kit", "objc2-app-kit 0.3.2",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.2",
"pollster", "pollster",
"raw-window-handle", "raw-window-handle",
"urlencoding", "urlencoding",
@ -5523,6 +5671,17 @@ dependencies = [
"xkeysym", "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]] [[package]]
name = "smol_str" name = "smol_str"
version = "0.3.2" version = "0.3.2"
@ -7102,6 +7261,20 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows" name = "windows"
version = "0.58.0" version = "0.58.0"
@ -7603,15 +7776,15 @@ version = "0.30.12"
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b" source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"block2", "block2 0.6.1",
"dispatch2", "dispatch2",
"dpi", "dpi",
"objc2", "objc2 0.6.3",
"objc2-app-kit", "objc2-app-kit 0.3.2",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-core-video", "objc2-core-video",
"objc2-foundation", "objc2-foundation 0.3.2",
"raw-window-handle", "raw-window-handle",
"smol_str", "smol_str",
"tracing", "tracing",
@ -7625,7 +7798,7 @@ version = "0.30.12"
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b" source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
dependencies = [ dependencies = [
"memmap2", "memmap2",
"objc2", "objc2 0.6.3",
"objc2-core-foundation", "objc2-core-foundation",
"smol_str", "smol_str",
"tracing", "tracing",
@ -7670,12 +7843,12 @@ version = "0.30.12"
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b" source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
dependencies = [ dependencies = [
"bitflags 2.9.3", "bitflags 2.9.3",
"block2", "block2 0.6.1",
"dispatch2", "dispatch2",
"dpi", "dpi",
"objc2", "objc2 0.6.3",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.2",
"objc2-ui-kit", "objc2-ui-kit",
"raw-window-handle", "raw-window-handle",
"serde", "serde",

View File

@ -45,6 +45,7 @@ serde = { workspace = true }
clap = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"] }
pidlock = "0.2.2" pidlock = "0.2.2"
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]
@ -64,4 +65,3 @@ objc2 = { version = "0.6.1", default-features = false }
objc2-foundation = { version = "0.3.2", default-features = false } objc2-foundation = { version = "0.3.2", default-features = false }
objc2-app-kit = { 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 } muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false }

View File

@ -259,6 +259,18 @@ impl App {
window.update_menu(entries); 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 => { DesktopFrontendMessage::WindowClose => {
self.app_event_scheduler.schedule(AppEvent::CloseWindow); self.app_event_scheduler.schedule(AppEvent::CloseWindow);
} }

View File

@ -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; 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. // 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. // CEF converts the key event into an `NSEvent` internally and passes that to Chromium.

View File

@ -61,7 +61,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
} }
let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate); 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 (overlay_srgb.a < 0.001) {
if (ui_srgb.a < 0.001) { if (ui_srgb.a < 0.001) {

View File

@ -38,6 +38,7 @@ 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: window_clipboard::Clipboard,
} }
impl Window { impl Window {
@ -57,10 +58,12 @@ 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) }.expect("failed to create clipboard");
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,
} }
} }
@ -136,6 +139,22 @@ 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> {
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 { pub(crate) enum Cursor {

View File

@ -1,6 +1,7 @@
use graphene_std::Color; use graphene_std::Color;
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform; 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 graphite_editor::messages::prelude::*;
use super::DesktopWrapperMessageDispatcher; use super::DesktopWrapperMessageDispatcher;
@ -156,5 +157,13 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
DesktopWrapperMessage::MenuEvent { id: _ } => {} DesktopWrapperMessage::MenuEvent { id: _ } => {}
DesktopWrapperMessage::ClipboardReadResult { content } => {
if let Some(content) = content {
let message = ClipboardMessage::ReadClipboard {
content: ClipboardContentRaw::Text(content),
};
dispatcher.queue_editor_message(message);
}
}
} }
} }

View File

@ -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 => { FrontendMessage::WindowClose => {
dispatcher.respond(DesktopFrontendMessage::WindowClose); dispatcher.respond(DesktopFrontendMessage::WindowClose);
} }

View File

@ -54,6 +54,10 @@ pub enum DesktopFrontendMessage {
UpdateMenu { UpdateMenu {
entries: Vec<MenuItem>, entries: Vec<MenuItem>,
}, },
ClipboardRead,
ClipboardWrite {
content: String,
},
WindowClose, WindowClose,
WindowMinimize, WindowMinimize,
WindowMaximize, WindowMaximize,
@ -114,6 +118,9 @@ pub enum DesktopWrapperMessage {
MenuEvent { MenuEvent {
id: String, id: String,
}, },
ClipboardReadResult {
content: Option<String>,
},
} }
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]

View File

@ -18,6 +18,7 @@ pub struct DispatcherMessageHandlers {
animation_message_handler: AnimationMessageHandler, animation_message_handler: AnimationMessageHandler,
app_window_message_handler: AppWindowMessageHandler, app_window_message_handler: AppWindowMessageHandler,
broadcast_message_handler: BroadcastMessageHandler, broadcast_message_handler: BroadcastMessageHandler,
clipboard_message_handler: ClipboardMessageHandler,
debug_message_handler: DebugMessageHandler, debug_message_handler: DebugMessageHandler,
defer_message_handler: DeferMessageHandler, defer_message_handler: DeferMessageHandler,
dialog_message_handler: DialogMessageHandler, dialog_message_handler: DialogMessageHandler,
@ -158,6 +159,7 @@ impl Dispatcher {
self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ()); 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::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) => { Message::Debug(message) => {
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ()); self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());
} }
@ -319,6 +321,7 @@ impl Dispatcher {
// TODO: Reduce the number of heap allocations // TODO: Reduce the number of heap allocations
let mut list = Vec::new(); let mut list = Vec::new();
list.extend(self.message_handlers.app_window_message_handler.actions()); 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.dialog_message_handler.actions());
list.extend(self.message_handlers.animation_message_handler.actions()); list.extend(self.message_handlers.animation_message_handler.actions());
list.extend(self.message_handlers.input_preprocessor_message_handler.actions()); list.extend(self.message_handlers.input_preprocessor_message_handler.actions());

View File

@ -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<String>, cut: bool },
Write { content: ClipboardContent },
}

View File

@ -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<ClipboardMessage, ()> for ClipboardMessageHandler {
fn process_message(&mut self, message: ClipboardMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
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,
);
}

View File

@ -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;

View File

@ -0,0 +1,16 @@
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ClipboardContentRaw {
Text(String),
Svg(String),
Image { data: Vec<u8>, 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<u8>, width: u32, height: u32 },
}

View File

@ -66,7 +66,7 @@ pub enum FrontendMessage {
shortcut: Option<ActionShortcut>, shortcut: Option<ActionShortcut>,
}, },
// Trigger prefix: cause a browser API to do something // Trigger prefix: cause a frontend specific API to do something
TriggerAboutGraphiteLocalizedCommitDate { TriggerAboutGraphiteLocalizedCommitDate {
#[serde(rename = "commitDate")] #[serde(rename = "commitDate")]
commit_date: String, commit_date: String,
@ -111,7 +111,6 @@ pub enum FrontendMessage {
TriggerOpenLaunchDocuments, TriggerOpenLaunchDocuments,
TriggerLoadPreferences, TriggerLoadPreferences,
TriggerOpenDocument, TriggerOpenDocument,
TriggerPaste,
TriggerSavePreferences { TriggerSavePreferences {
preferences: PreferencesMessageHandler, preferences: PreferencesMessageHandler,
}, },
@ -120,13 +119,19 @@ pub enum FrontendMessage {
document_id: DocumentId, document_id: DocumentId,
}, },
TriggerTextCommit, TriggerTextCommit,
TriggerTextCopy {
#[serde(rename = "copyText")]
copy_text: String,
},
TriggerVisitLink { TriggerVisitLink {
url: String, 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 // Update prefix: give the frontend a new value or state for it to use
UpdateActiveDocument { UpdateActiveDocument {
@ -330,12 +335,15 @@ pub enum FrontendMessage {
width: f64, width: f64,
height: f64, height: f64,
}, },
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
RenderOverlays { RenderOverlays {
#[serde(skip, default = "OverlayContext::default")] #[serde(skip, default = "OverlayContext::default")]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
context: OverlayContext, context: OverlayContext,
}, },
// Window prefix: cause the application window to do something
WindowClose, WindowClose,
WindowMinimize, WindowMinimize,
WindowMaximize, WindowMaximize,

View File

@ -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) // 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), 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 // NodeGraphMessage
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: false }), 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 }), 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(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers),
entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen), entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen),
// //
// FrontendMessage
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste),
//
// DialogMessage // DialogMessage
entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog), entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog),
entry!(KeyDown(KeyN); modifiers=[Accel], action_dispatch=DialogMessage::RequestNewDocumentDialog), entry!(KeyDown(KeyN); modifiers=[Accel], action_dispatch=DialogMessage::RequestNewDocumentDialog),

View File

@ -1,7 +1,6 @@
use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::input_mapper::utility_types::macros::action_shortcut;
use crate::messages::layout::utility_types::widget_prelude::*; 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::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_std::path_bool::BooleanOperation; use graphene_std::path_bool::BooleanOperation;
@ -196,20 +195,20 @@ impl LayoutHolder for MenuBarMessageHandler {
MenuListEntry::new("Cut") MenuListEntry::new("Cut")
.label("Cut") .label("Cut")
.icon("Cut") .icon("Cut")
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Cut)) .tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Cut))
.on_commit(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()) .on_commit(|_| ClipboardMessage::Cut.into())
.disabled(no_active_document || !has_selected_layers), .disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Copy") MenuListEntry::new("Copy")
.label("Copy") .label("Copy")
.icon("Copy") .icon("Copy")
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::Copy)) .tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Copy))
.on_commit(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()) .on_commit(|_| ClipboardMessage::Copy.into())
.disabled(no_active_document || !has_selected_layers), .disabled(no_active_document || !has_selected_layers),
MenuListEntry::new("Paste") MenuListEntry::new("Paste")
.label("Paste") .label("Paste")
.icon("Paste") .icon("Paste")
.tooltip_shortcut(action_shortcut!(FrontendMessageDiscriminant::TriggerPaste)) .tooltip_shortcut(action_shortcut!(ClipboardMessageDiscriminant::Paste))
.on_commit(|_| FrontendMessage::TriggerPaste.into()) .on_commit(|_| ClipboardMessage::Paste.into())
.disabled(no_active_document), .disabled(no_active_document),
], ],
vec![ vec![

View File

@ -12,6 +12,8 @@ pub enum Message {
#[child] #[child]
Broadcast(BroadcastMessage), Broadcast(BroadcastMessage),
#[child] #[child]
Clipboard(ClipboardMessage),
#[child]
Debug(DebugMessage), Debug(DebugMessage),
#[child] #[child]
Defer(DeferMessage), Defer(DeferMessage),

View File

@ -3,6 +3,7 @@
pub mod animation; pub mod animation;
pub mod app_window; pub mod app_window;
pub mod broadcast; pub mod broadcast;
pub mod clipboard;
pub mod debug; pub mod debug;
pub mod defer; pub mod defer;
pub mod dialog; pub mod dialog;

View File

@ -1,6 +1,7 @@
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode}; use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode};
use super::{document_node_definitions, node_properties}; use super::{document_node_definitions, node_properties};
use crate::consts::GRID_SIZE; 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::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::document_message_handler::navigation_controls;
@ -237,11 +238,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let new_ids = &all_selected_nodes.iter().enumerate().map(|(new, old)| (*old, NodeId(new as u64))).collect(); 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::<Vec<_>>(); let copied_nodes = network_interface.copy_nodes(new_ids, selection_network_path).collect::<Vec<_>>();
// Prefix to show that these are nodes let Ok(data) = serde_json::to_string(&copied_nodes) else {
let mut copy_text = String::from("graphite/nodes: "); log::error!("Failed to serialize nodes for clipboard");
copy_text += &serde_json::to_string(&copied_nodes).expect("Could not serialize copy"); return;
};
responses.add(FrontendMessage::TriggerTextCopy { copy_text }); responses.add(ClipboardMessage::Write {
content: ClipboardContent::Nodes(data),
});
} }
NodeGraphMessage::CreateNodeInLayerNoTransaction { node_type, layer } => { NodeGraphMessage::CreateNodeInLayerNoTransaction { node_type, layer } => {
let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) else { let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) else {

View File

@ -5,10 +5,9 @@ use graph_craft::document::NodeId;
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug, specta::Type)] #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug, specta::Type)]
pub enum Clipboard { pub enum Clipboard {
Internal, Internal,
Device,
_InternalClipboardCount, // Keep this as the last entry of **internal** clipboards since it is used for counting the number of enum variants _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; pub const INTERNAL_CLIPBOARD_COUNT: u8 = Clipboard::_InternalClipboardCount as u8;

View File

@ -6469,7 +6469,7 @@ mod network_interface_tests {
let serialized_nodes = frontend_messages let serialized_nodes = frontend_messages
.into_iter() .into_iter()
.find_map(|msg| match msg { .find_map(|msg| match msg {
FrontendMessage::TriggerTextCopy { copy_text } => Some(copy_text), FrontendMessage::TriggerClipboardWrite { content } => Some(content),
_ => None, _ => None,
}) })
.expect("copy message should be dispatched") .expect("copy message should be dispatched")

View File

@ -4,6 +4,7 @@ use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid; use crate::application::generate_uuid;
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
use crate::messages::animation::TimingInformation; use crate::messages::animation::TimingInformation;
use crate::messages::clipboard::utility_types::ClipboardContent;
use crate::messages::dialog::simple_dialogs; use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument}; use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument};
use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::input_mapper::utility_types::input_keyboard::Key;
@ -243,11 +244,21 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
} }
PortfolioMessage::Copy { clipboard } => { 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` // 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 { let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else {
return; return;
}; };
if active_document.graph_view_overlay_open() {
responses.add(NodeGraphMessage::Copy);
return;
}
let mut copy_val = |buffer: &mut Vec<CopyBufferEntry>| { let mut copy_val = |buffer: &mut Vec<CopyBufferEntry>| {
let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::<Vec<_>>(); let mut ordered_last_elements = active_document.network_interface.shallowest_unique_layers(&[]).collect::<Vec<_>>();
@ -283,10 +294,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if clipboard == Clipboard::Device { if clipboard == Clipboard::Device {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
copy_val(&mut buffer); copy_val(&mut buffer);
let mut copy_text = String::from("graphite/layer: "); let Ok(data) = serde_json::to_string(&buffer) else {
copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste"); log::error!("Failed to serialize nodes for clipboard");
return;
responses.add(FrontendMessage::TriggerTextCopy { copy_text }); };
responses.add(ClipboardMessage::Write {
content: ClipboardContent::Layer(data),
});
} else { } else {
let copy_buffer = &mut self.copy_buffer; let copy_buffer = &mut self.copy_buffer;
copy_buffer[clipboard as usize].clear(); copy_buffer[clipboard as usize].clear();
@ -294,6 +308,18 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
} }
PortfolioMessage::Cut { clipboard } => { 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(PortfolioMessage::Copy { clipboard });
responses.add(DocumentMessage::DeleteSelectedLayers); responses.add(DocumentMessage::DeleteSelectedLayers);
} }

View File

@ -7,6 +7,7 @@ pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscrimin
pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler}; pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler};
pub use crate::messages::broadcast::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler}; pub use crate::messages::broadcast::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler};
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler}; 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::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler}; pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler};
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler}; pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};

View File

@ -100,7 +100,7 @@ impl Fsm for EyedropperToolFsmState {
// Sampling -> Sampling // Sampling -> Sampling
(EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::PointerMove) => { (EyedropperToolFsmState::SamplingPrimary | EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::PointerMove) => {
let mouse_position = viewport.logical(input.mouse.position); 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); update_cursor_preview(responses, input, global_tool_data, None);
} else { } else {
disable_cursor_preview(responses); disable_cursor_preview(responses);

View File

@ -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, 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, 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::input_mapper::utility_types::macros::action_shortcut_manual;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; 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 { if clipboard == Clipboard::Device {
let mut copy_text = String::from("graphite/vector: "); if let Ok(data) = serde_json::to_string(&buffer) {
copy_text += &serde_json::to_string(&buffer).expect("Could not serialize paste"); responses.add(ClipboardMessage::Write {
content: ClipboardContent::Vector(data),
responses.add(FrontendMessage::TriggerTextCopy { copy_text }); });
} else {
log::error!("Failed to serialize nodes for clipboard");
}
} }
// TODO: Add implementation for internal clipboard // TODO: Add implementation for internal clipboard

View File

@ -1,10 +1,97 @@
import { type Editor } from "@graphite/editor"; import { type Editor } from "@graphite/editor";
import { TriggerTextCopy } from "@graphite/messages"; import { TriggerClipboardWrite, TriggerSelectionRead, TriggerSelectionWrite } from "@graphite/messages";
export function createClipboardManager(editor: Editor) { export function createClipboardManager(editor: Editor) {
// Subscribe to process backend event // 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 // 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 }));
}

View File

@ -1,13 +1,13 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import { type Editor } from "@graphite/editor"; 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 DialogState } from "@graphite/state-providers/dialog";
import { type DocumentState } from "@graphite/state-providers/document"; import { type DocumentState } from "@graphite/state-providers/document";
import { type FullscreenState } from "@graphite/state-providers/fullscreen"; import { type FullscreenState } from "@graphite/state-providers/fullscreen";
import { type PortfolioState } from "@graphite/state-providers/portfolio"; import { type PortfolioState } from "@graphite/state-providers/portfolio";
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry"; 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 { extractPixelData } from "@graphite/utility-functions/rasterization";
import { stripIndents } from "@graphite/utility-functions/strip-indents"; 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 // 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; 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 // 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; 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; if (key === "KeyV" && accelKey) return false;
// Don't redirect a fullscreen request // 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; if (!dataTransfer || targetIsTextField(e.target || undefined)) return;
e.preventDefault(); e.preventDefault();
const LAYER_DATA = "graphite/layer: ";
const NODES_DATA = "graphite/nodes: ";
const VECTOR_DATA = "graphite/vector: ";
Array.from(dataTransfer.items).forEach(async (item) => { Array.from(dataTransfer.items).forEach(async (item) => {
if (item.type === "text/plain") { if (item.type === "text/plain") {
item.getAsString((text) => { item.getAsString((text) => {
if (text.startsWith(LAYER_DATA)) { editor.handle.pasteText(text);
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));
}
}); });
} }
@ -413,7 +406,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
// Frontend message subscriptions // 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 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 // In the catch block, explain to the user why the paste failed and how to fix or work around the problem
try { try {
@ -437,10 +430,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
const text = reader.result as string; const text = reader.result as string;
editor.handle.pasteText(text);
if (text.startsWith("graphite/layer: ")) {
editor.handle.pasteSerializedData(text.substring(16, text.length));
}
}; };
reader.readAsText(blob); reader.readAsText(blob);
return true; return true;

View File

@ -725,7 +725,7 @@ export class TriggerOpenDocument extends JsMessage {}
export class TriggerImport extends JsMessage {} export class TriggerImport extends JsMessage {}
export class TriggerPaste extends JsMessage {} export class TriggerClipboardRead extends JsMessage {}
export class TriggerSaveDocument extends JsMessage { export class TriggerSaveDocument extends JsMessage {
readonly documentId!: bigint; readonly documentId!: bigint;
@ -868,8 +868,16 @@ export class TriggerVisitLink extends JsMessage {
export class TriggerTextCommit extends JsMessage {} export class TriggerTextCommit extends JsMessage {}
export class TriggerTextCopy extends JsMessage { export class TriggerClipboardWrite extends JsMessage {
readonly copyText!: string; readonly content!: string;
}
export class TriggerSelectionRead extends JsMessage {
readonly cut!: boolean;
}
export class TriggerSelectionWrite extends JsMessage {
readonly content!: string;
} }
export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage { export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
@ -1695,7 +1703,6 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerLoadRestAutoSaveDocuments, TriggerLoadRestAutoSaveDocuments,
TriggerOpenDocument, TriggerOpenDocument,
TriggerOpenLaunchDocuments, TriggerOpenLaunchDocuments,
TriggerPaste,
TriggerPersistenceRemoveDocument, TriggerPersistenceRemoveDocument,
TriggerPersistenceWriteDocument, TriggerPersistenceWriteDocument,
TriggerSaveActiveDocument, TriggerSaveActiveDocument,
@ -1703,7 +1710,10 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerSaveFile, TriggerSaveFile,
TriggerSavePreferences, TriggerSavePreferences,
TriggerTextCommit, TriggerTextCommit,
TriggerTextCopy, TriggerClipboardRead,
TriggerClipboardWrite,
TriggerSelectionRead,
TriggerSelectionWrite,
TriggerVisitLink, TriggerVisitLink,
UpdateActiveDocument, UpdateActiveDocument,
UpdateBox, UpdateBox,

View File

@ -7,6 +7,7 @@
use crate::helpers::translate_key; use crate::helpers::translate_key;
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER}; use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER};
use editor::consts::FILE_EXTENSION; 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_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta}; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@ -616,20 +617,6 @@ impl EditorHandle {
Ok(()) 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)] #[wasm_bindgen(js_name = clipLayer)]
pub fn clip_layer(&self, id: u64) { pub fn clip_layer(&self, id: u64) {
let id = NodeId(id); let id = NodeId(id);
@ -726,10 +713,19 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
/// Pastes the nodes based on serialized data /// Respond to selection read
#[wasm_bindgen(js_name = pasteSerializedNodes)] #[wasm_bindgen(js_name = readSelection)]
pub fn paste_serialized_nodes(&self, serialized_nodes: String) { pub fn read_selection(&self, content: Option<String>, cut: bool) {
let message = NodeGraphMessage::PasteNodes { serialized_nodes }; 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); self.dispatch(message);
} }