From bc02ec5a0b00ce89f164dd2550a19105f0b59c72 Mon Sep 17 00:00:00 2001 From: sam <43527203+bytemunch@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:02:19 +0100 Subject: [PATCH] implement copy/paste for wasm (#75) * implement copy/paste for wasm * ignore wasm specific "dead" code * fix ctrl instead of command on mac * fix character input if clipboard shortcut held --- Cargo.lock | 4 ++ Cargo.toml | 7 ++- src/input.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 9 ++++ 4 files changed, 160 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a81e041..f630092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,10 +393,14 @@ dependencies = [ "arboard", "bevy", "cosmic-text", + "crossbeam-channel", "image", "insta", "js-sys", "sys-locale", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5fb3fba..618ecea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ bevy = { version = "0.11", default-features = false, features = [ "x11", ] } cosmic-text = { version = "0.9" } +# TODO: move crossbeam to wasm32, once input.rs has separate wasm copy/paste fn +crossbeam-channel = "0.5.8" image = "0.24.6" sys-locale = "0.3.0" @@ -41,9 +43,12 @@ arboard = "3.2.0" [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.61" +wasm-bindgen = "0.2.87" +wasm-bindgen-futures = "0.4.37" +web-sys = { version = "0.3.64", features = ["Clipboard", "Navigator", "Window"] } [dev-dependencies] insta = "1.29.0" [[example]] -name = "text_input" \ No newline at end of file +name = "text_input" diff --git a/src/input.rs b/src/input.rs index 8d44101..4110a3f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -2,6 +2,9 @@ use std::time::Duration; +#[cfg(target_arch = "wasm32")] +use bevy::tasks::AsyncComputeTaskPool; + use bevy::{ input::mouse::{MouseMotion, MouseScrollUnit, MouseWheel}, prelude::*, @@ -9,6 +12,13 @@ use bevy::{ }; use cosmic_text::{Action, AttrsList, BufferLine, Cursor, Edit, Shaping}; +#[cfg(target_arch = "wasm32")] +use js_sys::Promise; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::JsFuture; + use crate::{ get_node_cursor_pos, get_timestamp, get_x_offset_center, get_y_offset_center, save_edit_history, CosmicAttrs, CosmicEditHistory, CosmicEditor, CosmicFontSystem, @@ -19,6 +29,19 @@ use crate::{ #[derive(Resource)] pub struct ClickTimer(pub Timer); +// TODO: hide this behind #cfg wasm, depends on wasm having own copy/paste fn +#[allow(dead_code)] +pub struct WasmPaste { + text: String, + entity: Entity, +} + +#[derive(Resource)] +pub struct WasmPasteAsyncChannel { + pub tx: crossbeam_channel::Sender<WasmPaste>, + pub rx: crossbeam_channel::Receiver<WasmPaste>, +} + pub(crate) fn input_mouse( windows: Query<&Window, With<PrimaryWindow>>, active_editor: Res<Focus>, @@ -189,6 +212,8 @@ pub(crate) fn input_mouse( } } +// TODO: split copy/paste into own fn, separate fn for wasm +// Maybe split undo/redo too, just drop inputs from this fn when pressed /// Handles undo/redo, copy/paste and char input pub(crate) fn input_kb( active_editor: Res<Focus>, @@ -208,6 +233,7 @@ pub(crate) fn input_kb( mut is_deleting: Local<bool>, mut edits_duration: Local<Option<Duration>>, mut undoredo_duration: Local<Option<Duration>>, + _channel: Option<Res<WasmPasteAsyncChannel>>, ) { for (mut editor, mut edit_history, attrs, max_lines, max_chars, entity, readonly_opt) in &mut cosmic_edit_query.iter_mut() @@ -228,6 +254,19 @@ pub(crate) fn input_kb( #[cfg(not(target_os = "macos"))] let command = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]); + #[cfg(target_arch = "wasm32")] + let command = if web_sys::window() + .unwrap() + .navigator() + .user_agent() + .unwrap_or("NoUA".into()) + .contains("Macintosh") + { + keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]) + } else { + command + }; + let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]); #[cfg(target_os = "macos")] @@ -479,6 +518,40 @@ pub(crate) fn input_kb( } } + #[cfg(target_arch = "wasm32")] + { + if command && keys.just_pressed(KeyCode::C) { + if let Some(text) = editor.0.copy_selection() { + write_clipboard_wasm(text.as_str()); + return; + } + } + + if command && keys.just_pressed(KeyCode::X) && !readonly { + if let Some(text) = editor.0.copy_selection() { + write_clipboard_wasm(text.as_str()); + editor.0.delete_selection(); + } + is_clipboard = true; + } + if command && keys.just_pressed(KeyCode::V) && !readonly { + let tx = _channel.unwrap().tx.clone(); + let _task = AsyncComputeTaskPool::get().spawn(async move { + let promise = read_clipboard_wasm(); + + let result = JsFuture::from(promise).await; + + if let Ok(js_text) = result { + if let Some(text) = js_text.as_string() { + let _ = tx.try_send(WasmPaste { text, entity }); + } + } + }); + + return; + } + } + let mut is_edit = is_clipboard; let mut is_return = false; if keys.just_pressed(KeyCode::Return) && !readonly { @@ -505,7 +578,7 @@ pub(crate) fn input_kb( } } editor.0.action(&mut font_system.0, Action::Backspace); - } else if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 { + } else if !command && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0) { editor .0 .action(&mut font_system.0, Action::Insert(char_ev.char)); @@ -532,3 +605,70 @@ pub(crate) fn input_kb( } } } + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub fn write_clipboard_wasm(text: &str) { + let clipboard = web_sys::window() + .unwrap() + .navigator() + .clipboard() + .expect("Clipboard not found!"); + let _result = clipboard.write_text(text); +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub fn read_clipboard_wasm() -> Promise { + let clipboard = web_sys::window() + .unwrap() + .navigator() + .clipboard() + .expect("Clipboard not found!"); + clipboard.read_text() +} + +#[cfg(target_arch = "wasm32")] +pub fn poll_wasm_paste( + channel: Res<WasmPasteAsyncChannel>, + mut editor_q: Query< + ( + &mut CosmicEditor, + &CosmicAttrs, + &CosmicMaxChars, + &CosmicMaxChars, + &mut CosmicEditHistory, + ), + Without<ReadOnly>, + >, + mut evw_changed: EventWriter<CosmicTextChanged>, + mut font_system: ResMut<CosmicFontSystem>, +) { + let inlet = channel.rx.try_recv(); + match inlet { + Ok(inlet) => { + let entity = inlet.entity; + if let Ok((mut editor, attrs, max_chars, max_lines, mut edit_history)) = + editor_q.get_mut(entity) + { + let text = inlet.text; + let attrs = &attrs.0; + for c in text.chars() { + if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 { + if c == 0xA as char { + if max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0 { + editor.0.action(&mut font_system.0, Action::Insert(c)); + } + } else { + editor.0.action(&mut font_system.0, Action::Insert(c)); + } + } + } + + evw_changed.send(CosmicTextChanged((entity, editor.get_text()))); + save_edit_history(&mut editor.0, attrs, &mut edit_history); + } + } + Err(_) => {} + } +} diff --git a/src/lib.rs b/src/lib.rs index cb9113f..eff2c36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,8 @@ use cursor::{change_cursor, hover_sprites, hover_ui}; pub use cursor::{TextHoverIn, TextHoverOut}; use image::{imageops::FilterType, GenericImageView}; use input::{input_kb, input_mouse, ClickTimer}; +#[cfg(target_arch = "wasm32")] +use input::{poll_wasm_paste, WasmPaste, WasmPasteAsyncChannel}; #[derive(Clone, Component, PartialEq, Debug)] pub enum CosmicText { @@ -457,6 +459,13 @@ impl Plugin for CosmicEditPlugin { } CursorConfig::None => {} } + + #[cfg(target_arch = "wasm32")] + { + let (tx, rx) = crossbeam_channel::bounded::<WasmPaste>(1); + app.insert_resource(WasmPasteAsyncChannel { tx, rx }) + .add_systems(Update, poll_wasm_paste); + } } } -- GitLab