From 493a21fdbe064b85f13230fc58b8aeae258e8b58 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Tue, 18 Apr 2023 23:35:55 +0200 Subject: [PATCH 01/16] wip clipboard example --- .cargo/config.toml | 13 ++++++++++ Cargo.toml | 17 ++++++++++++- examples/clipboard.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ run-wasm/Cargo.toml | 9 +++++++ run-wasm/src/main.rs | 3 +++ 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 .cargo/config.toml create mode 100644 examples/clipboard.rs create mode 100644 run-wasm/Cargo.toml create mode 100644 run-wasm/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..2d35b1973 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,13 @@ +[alias] +run-wasm = ["run", "--release", "--package", "run-wasm", "--"] + +# Credits to https://github.com/emilk/egui + +# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work +# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html +# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility +#[build] +#target = "wasm32-unknown-unknown" + +[target.wasm32-unknown-unknown] +rustflags = ["--cfg=web_sys_unstable_apis"] diff --git a/Cargo.toml b/Cargo.toml index f296ef149..26565966c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,10 @@ default_fonts = ["egui/default_fonts"] serde = ["egui/serde"] [dependencies] -bevy = { version = "0.10", default-features = false, features = ["bevy_render", "bevy_asset"] } +bevy = { version = "0.10", default-features = false, features = [ + "bevy_render", + "bevy_asset", +] } egui = { version = "0.21.0", default-features = false, features = ["bytemuck"] } webbrowser = { version = "0.8.2", optional = true } @@ -39,3 +42,15 @@ bevy = { version = "0.10", default-features = false, features = [ "bevy_pbr", "bevy_core_pipeline", ] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "*", features = [ + "Clipboard", + "Window", + "Navigator", + "Permissions", +] } +wasm-bindgen-futures = "*" + +[workspace] +members = ["run-wasm"] diff --git a/examples/clipboard.rs b/examples/clipboard.rs new file mode 100644 index 000000000..2b94c3242 --- /dev/null +++ b/examples/clipboard.rs @@ -0,0 +1,57 @@ +use bevy::{input::common_conditions::input_just_pressed, prelude::*}; +use bevy_egui::{egui, EguiContexts, EguiPlugin}; +use egui::{text_edit::CursorRange, TextEdit, Widget}; +use wasm_bindgen_futures::spawn_local; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(EguiPlugin) + .init_resource::() + // Systems that create Egui widgets should be run during the `CoreSet::Update` set, + // or after the `EguiSet::BeginFrame` system (which belongs to the `CoreSet::PreUpdate` set). + .add_system(ui_edit) + .add_system(clipboard_copy.run_if(input_just_pressed(KeyCode::C))) + .add_system(clipboard_paste.run_if(input_just_pressed(KeyCode::V))) + .run(); +} + +#[derive(Resource, Default)] +struct CustomText(pub String, pub Option); + +fn ui_edit(mut contexts: EguiContexts, mut text: ResMut) { + egui::Window::new("Hello").show(contexts.ctx_mut(), |ui| { + let edit = TextEdit::multiline(&mut text.0).show(ui); + text.1 = edit.cursor_range; + }); +} + +fn clipboard_copy(mut text: ResMut) { + //text.0 = "copy".into(); + + let text = if let Some(selected) = text.1 { + text.0[selected.primary.ccursor.index..selected.secondary.ccursor.index].to_string() + } else { + "".into() + }; + let _task = spawn_local(async move { + let window = web_sys::window().expect("window"); // { obj: val }; + let nav = window.navigator().clipboard(); + match nav { + Some(a) => { + let p = a.write_text(&text); + let result = wasm_bindgen_futures::JsFuture::from(p) + .await + .expect("clipboard populated"); + info!("clippyboy worked"); + } + None => { + warn!("failed to copy clippyboy"); + } + }; + }); +} + +fn clipboard_paste(mut text: ResMut) { + text.0 = "paste".into(); +} diff --git a/run-wasm/Cargo.toml b/run-wasm/Cargo.toml new file mode 100644 index 000000000..3ac77b26d --- /dev/null +++ b/run-wasm/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "run-wasm" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cargo-run-wasm = "0.2.0" diff --git a/run-wasm/src/main.rs b/run-wasm/src/main.rs new file mode 100644 index 000000000..6961358d9 --- /dev/null +++ b/run-wasm/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + cargo_run_wasm::run_wasm_with_css("body { margin: 0px; }"); +} From 205cddee81d1212f56c881375049d61b73c5a501 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Sat, 22 Apr 2023 13:40:07 +0200 Subject: [PATCH 02/16] wip read clipboard, fail :( --- Cargo.toml | 3 +++ examples/clipboard.rs | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26565966c..3a9e18b04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,10 @@ web-sys = { version = "*", features = [ "Window", "Navigator", "Permissions", + "ClipboardPermissionDescriptor", + "PermissionName", ] } +js-sys = "*" wasm-bindgen-futures = "*" [workspace] diff --git a/examples/clipboard.rs b/examples/clipboard.rs index 2b94c3242..a0d499a3a 100644 --- a/examples/clipboard.rs +++ b/examples/clipboard.rs @@ -2,6 +2,7 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*}; use bevy_egui::{egui, EguiContexts, EguiPlugin}; use egui::{text_edit::CursorRange, TextEdit, Widget}; use wasm_bindgen_futures::spawn_local; +use web_sys::ClipboardPermissionDescriptor; fn main() { App::new() @@ -36,8 +37,11 @@ fn clipboard_copy(mut text: ResMut) { }; let _task = spawn_local(async move { let window = web_sys::window().expect("window"); // { obj: val }; - let nav = window.navigator().clipboard(); - match nav { + + let nav = window.navigator(); + + let clipboard = nav.clipboard(); + match clipboard { Some(a) => { let p = a.write_text(&text); let result = wasm_bindgen_futures::JsFuture::from(p) @@ -46,12 +50,43 @@ fn clipboard_copy(mut text: ResMut) { info!("clippyboy worked"); } None => { - warn!("failed to copy clippyboy"); + warn!("failed to write clippyboy"); } }; }); } fn clipboard_paste(mut text: ResMut) { - text.0 = "paste".into(); + let _task = spawn_local(async move { + let window = web_sys::window().expect("window"); // { obj: val }; + + let nav = window.navigator(); + let clipboard = nav.clipboard(); + + let Ok(permissions) = nav.permissions() else { + return; + }; + let clipboard_permission_desc = js_sys::Object::new(); + js_sys::Reflect::set( + &clipboard_permission_desc, + &"name".into(), + // 'clipboard-read' fails on firefox because it's not implemented. + // more info: https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API#browser_compatibility + &"clipboard-read".into(), + ); + dbg!(permissions.query(&clipboard_permission_desc.into())); + match clipboard { + Some(a) => { + let p = a.read(); + let result = wasm_bindgen_futures::JsFuture::from(p) + .await + .expect("clipboard populated"); + dbg!("result: ", &result); + info!("clippyboy worked"); + } + None => { + warn!("failed to read clippyboy"); + } + }; + }); } From 9e85a902c51eb145929e6e52119d79c57380fb94 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Sun, 7 May 2023 15:02:58 +0200 Subject: [PATCH 03/16] wip attempt to use web_sys paste event --- Cargo.toml | 7 +-- examples/clipboard.rs | 100 ++++++++++++++++++++++++++++-------------- 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a9e18b04..decfeb418 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,15 +44,16 @@ bevy = { version = "0.10", default-features = false, features = [ ] } [target.'cfg(target_arch = "wasm32")'.dependencies] +winit = "0.28" web-sys = { version = "*", features = [ "Clipboard", + "ClipboardEvent", + "DataTransfer", "Window", "Navigator", - "Permissions", - "ClipboardPermissionDescriptor", - "PermissionName", ] } js-sys = "*" +wasm-bindgen = "0.2.84" wasm-bindgen-futures = "*" [workspace] diff --git a/examples/clipboard.rs b/examples/clipboard.rs index a0d499a3a..cc4578baf 100644 --- a/examples/clipboard.rs +++ b/examples/clipboard.rs @@ -1,19 +1,28 @@ -use bevy::{input::common_conditions::input_just_pressed, prelude::*}; +use std::sync::mpsc::{self, Receiver, Sender}; + +use bevy::{input::common_conditions::input_just_pressed, prelude::*, winit::WinitWindows}; use bevy_egui::{egui, EguiContexts, EguiPlugin}; use egui::{text_edit::CursorRange, TextEdit, Widget}; use wasm_bindgen_futures::spawn_local; -use web_sys::ClipboardPermissionDescriptor; fn main() { App::new() - .add_plugins(DefaultPlugins) + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + prevent_default_event_handling: false, + ..default() + }), + ..default() + })) .add_plugin(EguiPlugin) .init_resource::() + .init_non_send_resource::() // Systems that create Egui widgets should be run during the `CoreSet::Update` set, // or after the `EguiSet::BeginFrame` system (which belongs to the `CoreSet::PreUpdate` set). + .add_startup_system(setup_clipboard_paste) .add_system(ui_edit) .add_system(clipboard_copy.run_if(input_just_pressed(KeyCode::C))) - .add_system(clipboard_paste.run_if(input_just_pressed(KeyCode::V))) + .add_system(read_clipboard_channel) .run(); } @@ -56,37 +65,64 @@ fn clipboard_copy(mut text: ResMut) { }); } -fn clipboard_paste(mut text: ResMut) { - let _task = spawn_local(async move { - let window = web_sys::window().expect("window"); // { obj: val }; +#[derive(Default)] +struct ClipboardChannel { + pub rx: Option>, +} - let nav = window.navigator(); - let clipboard = nav.clipboard(); +fn setup_clipboard_paste( + mut commands: Commands, + windows: Query>, + winit_windows: NonSendMut, + mut clipboardChannel: NonSendMut, +) { + let Some(first_window) = windows.iter().next() else { + return; + }; + let Some(winit_window_instance) = winit_windows.get_window(first_window) else { + return; + }; + let (tx, rx): (Sender, Receiver) = mpsc::channel(); - let Ok(permissions) = nav.permissions() else { - return; - }; - let clipboard_permission_desc = js_sys::Object::new(); - js_sys::Reflect::set( - &clipboard_permission_desc, - &"name".into(), - // 'clipboard-read' fails on firefox because it's not implemented. - // more info: https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API#browser_compatibility - &"clipboard-read".into(), - ); - dbg!(permissions.query(&clipboard_permission_desc.into())); - match clipboard { - Some(a) => { - let p = a.read(); - let result = wasm_bindgen_futures::JsFuture::from(p) - .await - .expect("clipboard populated"); - dbg!("result: ", &result); - info!("clippyboy worked"); + use wasm_bindgen::closure::Closure; + use wasm_bindgen::prelude::*; + use winit::platform::web::WindowExtWebSys; + let canvas = winit_window_instance.canvas(); + info!("canvas found"); + let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { + tx.send("test".to_string()); + match event + .clipboard_data() + .expect("could not get clipboard data.") + .get_data("text/plain") + { + Ok(data) => { + tx.send(data); } - None => { - warn!("failed to read clippyboy"); + _ => { + info!("Not implemented."); } - }; + } + info!("{:?}", event.clipboard_data()) }); + + canvas + .add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref()) + .expect("Could not edd paste event listener."); + closure.forget(); + *clipboardChannel = ClipboardChannel { rx: Some(rx) }; + + // winit_window_instance.can + info!("setup_clipboard_paste OK"); +} + +fn read_clipboard_channel(mut clipboardChannel: NonSendMut) { + match &mut clipboardChannel.rx { + Some(rx) => { + if let Ok(clipboard_string) = rx.try_recv() { + info!("received: {}", clipboard_string); + } + } + None => {} + } } From 69c447d2158cd4fbbbf3244272da0b68179d245a Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 17 May 2023 04:05:14 +0200 Subject: [PATCH 04/16] progress, paste with bugs implemented :) --- Cargo.toml | 4 ++++ examples/clipboard.rs | 56 +++++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index decfeb418..cd242d6fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,12 +49,16 @@ web-sys = { version = "*", features = [ "Clipboard", "ClipboardEvent", "DataTransfer", + 'Document', + 'EventTarget', "Window", "Navigator", ] } js-sys = "*" wasm-bindgen = "0.2.84" wasm-bindgen-futures = "*" +console_log = "*" +log = "0.4" [workspace] members = ["run-wasm"] diff --git a/examples/clipboard.rs b/examples/clipboard.rs index cc4578baf..cccd013ea 100644 --- a/examples/clipboard.rs +++ b/examples/clipboard.rs @@ -1,8 +1,8 @@ use std::sync::mpsc::{self, Receiver, Sender}; -use bevy::{input::common_conditions::input_just_pressed, prelude::*, winit::WinitWindows}; +use bevy::{input::common_conditions::input_just_pressed, prelude::*}; use bevy_egui::{egui, EguiContexts, EguiPlugin}; -use egui::{text_edit::CursorRange, TextEdit, Widget}; +use egui::{text_edit::CursorRange, TextEdit}; use wasm_bindgen_futures::spawn_local; fn main() { @@ -40,7 +40,7 @@ fn clipboard_copy(mut text: ResMut) { //text.0 = "copy".into(); let text = if let Some(selected) = text.1 { - text.0[selected.primary.ccursor.index..selected.secondary.ccursor.index].to_string() + text.0[selected.as_sorted_char_range()].to_string() } else { "".into() }; @@ -56,10 +56,10 @@ fn clipboard_copy(mut text: ResMut) { let result = wasm_bindgen_futures::JsFuture::from(p) .await .expect("clipboard populated"); - info!("clippyboy worked"); + info!("copy to clipboard worked"); } None => { - warn!("failed to write clippyboy"); + warn!("failed to write clipboard data"); } }; }); @@ -70,27 +70,15 @@ struct ClipboardChannel { pub rx: Option>, } -fn setup_clipboard_paste( - mut commands: Commands, - windows: Query>, - winit_windows: NonSendMut, - mut clipboardChannel: NonSendMut, -) { - let Some(first_window) = windows.iter().next() else { - return; - }; - let Some(winit_window_instance) = winit_windows.get_window(first_window) else { - return; - }; +fn setup_clipboard_paste(mut clipboard_channel: NonSendMut) { let (tx, rx): (Sender, Receiver) = mpsc::channel(); use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; - use winit::platform::web::WindowExtWebSys; - let canvas = winit_window_instance.canvas(); - info!("canvas found"); + let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { - tx.send("test".to_string()); + // TODO: maybe we should check if current canvas is selected ? not sure it's possible, + // but reacting to event at the document level will lead to problems if multiple games are on the samge page. match event .clipboard_data() .expect("could not get clipboard data.") @@ -106,21 +94,37 @@ fn setup_clipboard_paste( info!("{:?}", event.clipboard_data()) }); - canvas + // TODO: a lot of unwraps ; it's using documents because paste event set on a canvas do not trigger (also tested on firefox in vanilla javascript) + web_sys::window() + .unwrap() + .document() + .unwrap() .add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref()) .expect("Could not edd paste event listener."); closure.forget(); - *clipboardChannel = ClipboardChannel { rx: Some(rx) }; + *clipboard_channel = ClipboardChannel { rx: Some(rx) }; - // winit_window_instance.can info!("setup_clipboard_paste OK"); } -fn read_clipboard_channel(mut clipboardChannel: NonSendMut) { - match &mut clipboardChannel.rx { +fn read_clipboard_channel( + mut clipboard_channel: NonSendMut, + mut text: ResMut, +) { + match &mut clipboard_channel.rx { Some(rx) => { if let Ok(clipboard_string) = rx.try_recv() { + // TODO: a global res is not the way to go, we should detect which element is focused when the paste is triggered. info!("received: {}", clipboard_string); + // TODO: sometime I receive a single string, and it's printed twice in the edit field, + // I guess some concurrency problem with ui_edit but I'm not sure. + if let Some(selected) = text.1 { + text.0 + .replace_range(selected.as_sorted_char_range(), &clipboard_string); + } else { + text.0 = clipboard_string; + }; + // TODO: set cursor to the end of the copied text. } } None => {} From 0ec3ed92725960092d2d1b929fa87d727d8b1472 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Tue, 23 May 2023 04:04:16 +0200 Subject: [PATCH 05/16] integrate clipboard code into bevy_egui --- examples/clipboard.rs | 132 ------------------------------------------ examples/ui.rs | 8 ++- src/lib.rs | 26 +++++++-- src/systems.rs | 39 ++++++++----- src/web_clipboard.rs | 106 +++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 153 deletions(-) delete mode 100644 examples/clipboard.rs create mode 100644 src/web_clipboard.rs diff --git a/examples/clipboard.rs b/examples/clipboard.rs deleted file mode 100644 index cccd013ea..000000000 --- a/examples/clipboard.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::sync::mpsc::{self, Receiver, Sender}; - -use bevy::{input::common_conditions::input_just_pressed, prelude::*}; -use bevy_egui::{egui, EguiContexts, EguiPlugin}; -use egui::{text_edit::CursorRange, TextEdit}; -use wasm_bindgen_futures::spawn_local; - -fn main() { - App::new() - .add_plugins(DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - prevent_default_event_handling: false, - ..default() - }), - ..default() - })) - .add_plugin(EguiPlugin) - .init_resource::() - .init_non_send_resource::() - // Systems that create Egui widgets should be run during the `CoreSet::Update` set, - // or after the `EguiSet::BeginFrame` system (which belongs to the `CoreSet::PreUpdate` set). - .add_startup_system(setup_clipboard_paste) - .add_system(ui_edit) - .add_system(clipboard_copy.run_if(input_just_pressed(KeyCode::C))) - .add_system(read_clipboard_channel) - .run(); -} - -#[derive(Resource, Default)] -struct CustomText(pub String, pub Option); - -fn ui_edit(mut contexts: EguiContexts, mut text: ResMut) { - egui::Window::new("Hello").show(contexts.ctx_mut(), |ui| { - let edit = TextEdit::multiline(&mut text.0).show(ui); - text.1 = edit.cursor_range; - }); -} - -fn clipboard_copy(mut text: ResMut) { - //text.0 = "copy".into(); - - let text = if let Some(selected) = text.1 { - text.0[selected.as_sorted_char_range()].to_string() - } else { - "".into() - }; - let _task = spawn_local(async move { - let window = web_sys::window().expect("window"); // { obj: val }; - - let nav = window.navigator(); - - let clipboard = nav.clipboard(); - match clipboard { - Some(a) => { - let p = a.write_text(&text); - let result = wasm_bindgen_futures::JsFuture::from(p) - .await - .expect("clipboard populated"); - info!("copy to clipboard worked"); - } - None => { - warn!("failed to write clipboard data"); - } - }; - }); -} - -#[derive(Default)] -struct ClipboardChannel { - pub rx: Option>, -} - -fn setup_clipboard_paste(mut clipboard_channel: NonSendMut) { - let (tx, rx): (Sender, Receiver) = mpsc::channel(); - - use wasm_bindgen::closure::Closure; - use wasm_bindgen::prelude::*; - - let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { - // TODO: maybe we should check if current canvas is selected ? not sure it's possible, - // but reacting to event at the document level will lead to problems if multiple games are on the samge page. - match event - .clipboard_data() - .expect("could not get clipboard data.") - .get_data("text/plain") - { - Ok(data) => { - tx.send(data); - } - _ => { - info!("Not implemented."); - } - } - info!("{:?}", event.clipboard_data()) - }); - - // TODO: a lot of unwraps ; it's using documents because paste event set on a canvas do not trigger (also tested on firefox in vanilla javascript) - web_sys::window() - .unwrap() - .document() - .unwrap() - .add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref()) - .expect("Could not edd paste event listener."); - closure.forget(); - *clipboard_channel = ClipboardChannel { rx: Some(rx) }; - - info!("setup_clipboard_paste OK"); -} - -fn read_clipboard_channel( - mut clipboard_channel: NonSendMut, - mut text: ResMut, -) { - match &mut clipboard_channel.rx { - Some(rx) => { - if let Ok(clipboard_string) = rx.try_recv() { - // TODO: a global res is not the way to go, we should detect which element is focused when the paste is triggered. - info!("received: {}", clipboard_string); - // TODO: sometime I receive a single string, and it's printed twice in the edit field, - // I guess some concurrency problem with ui_edit but I'm not sure. - if let Some(selected) = text.1 { - text.0 - .replace_range(selected.as_sorted_char_range(), &clipboard_string); - } else { - text.0 = clipboard_string; - }; - // TODO: set cursor to the end of the copied text. - } - } - None => {} - } -} diff --git a/examples/ui.rs b/examples/ui.rs index b06a843fc..c51306a89 100644 --- a/examples/ui.rs +++ b/examples/ui.rs @@ -25,7 +25,13 @@ fn main() { .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))) .insert_resource(Msaa::Sample4) .init_resource::() - .add_plugins(DefaultPlugins) + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + prevent_default_event_handling: false, + ..default() + }), + ..default() + })) .add_plugin(EguiPlugin) .add_startup_system(configure_visuals_system) .add_startup_system(configure_ui_state_system) diff --git a/src/lib.rs b/src/lib.rs index 2d8645e3c..9299aa89e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,10 @@ pub mod systems; /// Egui render node. pub mod egui_node; +/// Clipboard management for web +#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] +pub mod web_clipboard; + pub use egui; use crate::{ @@ -147,7 +151,7 @@ pub struct EguiClipboard { #[cfg(not(target_arch = "wasm32"))] clipboard: ThreadLocal>>, #[cfg(target_arch = "wasm32")] - clipboard: String, + clipboard: web_clipboard::WebClipboardPaste, } #[cfg(feature = "manage_clipboard")] @@ -159,12 +163,20 @@ impl EguiClipboard { /// Gets clipboard contents. Returns [`None`] if clipboard provider is unavailable or returns an error. #[must_use] + #[cfg(not(target_arch = "wasm32"))] pub fn get_contents(&self) -> Option { self.get_contents_impl() } + /// Gets clipboard contents. Returns [`None`] if clipboard provider is unavailable or returns an error. + #[must_use] + #[cfg(target_arch = "wasm32")] + pub fn get_contents(&mut self) -> Option { + self.get_contents_impl() + } + #[cfg(not(target_arch = "wasm32"))] - fn set_contents_impl(&self, contents: &str) { + fn set_contents_impl(&mut self, contents: &str) { if let Some(mut clipboard) = self.get() { if let Err(err) = clipboard.set_text(contents.to_owned()) { log::error!("Failed to set clipboard contents: {:?}", err); @@ -173,8 +185,8 @@ impl EguiClipboard { } #[cfg(target_arch = "wasm32")] - fn set_contents_impl(&mut self, contents: &str) { - self.clipboard = contents.to_owned(); + fn set_contents_impl(&self, contents: &str) { + web_clipboard::clipboard_copy(contents.to_owned()); } #[cfg(not(target_arch = "wasm32"))] @@ -190,8 +202,8 @@ impl EguiClipboard { #[cfg(target_arch = "wasm32")] #[allow(clippy::unnecessary_wraps)] - fn get_contents_impl(&self) -> Option { - Some(self.clipboard.clone()) + fn get_contents_impl(&mut self) -> Option { + self.clipboard.try_read_clipboard_event() } #[cfg(not(target_arch = "wasm32"))] @@ -533,6 +545,8 @@ impl Plugin for EguiPlugin { world.init_resource::(); world.init_resource::(); + #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] + app.add_startup_system(web_clipboard::startup); app.add_startup_systems( ( setup_new_windows_system, diff --git a/src/systems.rs b/src/systems.rs index 884411978..3544953e2 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -53,7 +53,9 @@ impl<'w, 's> InputEvents<'w, 's> { #[allow(missing_docs)] #[derive(SystemParam)] pub struct InputResources<'w, 's> { - #[cfg(feature = "manage_clipboard")] + #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] + pub egui_clipboard: ResMut<'w, crate::EguiClipboard>, + #[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))] pub egui_clipboard: Res<'w, crate::EguiClipboard>, pub keyboard_input: Res<'w, Input>, #[system_param(ignore)] @@ -72,7 +74,7 @@ pub struct ContextSystemParams<'w, 's> { /// Processes Bevy input and feeds it to Egui. pub fn process_input_system( mut input_events: InputEvents, - input_resources: InputResources, + mut input_resources: InputResources, mut context_params: ContextSystemParams, egui_settings: Res, mut egui_mouse_position: ResMut, @@ -250,20 +252,29 @@ pub fn process_input_system( // We also check that it's an `ButtonState::Pressed` event, as we don't want to // copy, cut or paste on the key release. #[cfg(feature = "manage_clipboard")] - if command && pressed { - match key { - egui::Key::C => { - focused_input.events.push(egui::Event::Copy); - } - egui::Key::X => { - focused_input.events.push(egui::Event::Cut); - } - egui::Key::V => { - if let Some(contents) = input_resources.egui_clipboard.get_contents() { - focused_input.events.push(egui::Event::Text(contents)) + { + if command && pressed { + match key { + egui::Key::C => { + focused_input.events.push(egui::Event::Copy); + } + egui::Key::X => { + focused_input.events.push(egui::Event::Cut); } + egui::Key::V => { + #[cfg(not(target_arch = "wasm32"))] + if let Some(contents) = + input_resources.egui_clipboard.get_contents() + { + focused_input.events.push(egui::Event::Text(contents)) + } + } + _ => {} } - _ => {} + } + #[cfg(target_arch = "wasm32")] + if let Some(contents) = input_resources.egui_clipboard.get_contents() { + focused_input.events.push(egui::Event::Text(contents)); } } } diff --git a/src/web_clipboard.rs b/src/web_clipboard.rs new file mode 100644 index 000000000..3f96e9cd2 --- /dev/null +++ b/src/web_clipboard.rs @@ -0,0 +1,106 @@ +use std::sync::{ + mpsc::{self, Receiver, Sender}, + Arc, Mutex, +}; + +use bevy::prelude::*; +use wasm_bindgen_futures::spawn_local; + +use crate::EguiClipboard; + +/// startup system for bevy to initialize clipboard. +pub fn startup(mut clipboard_channel: ResMut) { + setup_clipboard_paste(&mut clipboard_channel.clipboard) +} + +fn setup_clipboard_paste(clipboard_channel: &mut WebClipboardPaste) { + let (tx, rx): (Sender, Receiver) = mpsc::channel(); + + use wasm_bindgen::closure::Closure; + use wasm_bindgen::prelude::*; + + let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { + // TODO: maybe we should check if current canvas is selected ? not sure it's possible, + // but reacting to event at the document level will lead to problems if multiple games are on the same page. + match event + .clipboard_data() + .expect("could not get clipboard data.") + .get_data("text/plain") + { + Ok(data) => { + tx.send(data); + } + _ => { + info!("Not implemented."); + } + } + info!("{:?}", event.clipboard_data()) + }); + + // TODO: a lot of unwraps ; it's using documents because paste event set on a canvas do not trigger (also tested on firefox in vanilla javascript) + web_sys::window() + .unwrap() + .document() + .unwrap() + .add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref()) + .expect("Could not edd paste event listener."); + closure.forget(); + *clipboard_channel = WebClipboardPaste { + rx: Some(Arc::new(Mutex::new(rx))), + }; + + info!("setup_clipboard_paste OK"); +} + +/// To get data from web paste events +#[derive(Default)] +pub struct WebClipboardPaste { + rx: Option>>>, +} + +impl WebClipboardPaste { + /// Only returns Some if user explicitly triggered a paste event. + /// We are not querying the clipboard data without user input here (it would require permissions). + pub fn try_read_clipboard_event(&mut self) -> Option { + match &mut self.rx { + Some(rx) => { + let Ok(unlock) = rx.try_lock() else { + info!("fail lock"); + return None; + }; + if let Ok(clipboard_string) = unlock.try_recv() { + info!("received: {}", clipboard_string); + return Some(clipboard_string); + } + None + } + None => { + info!("no arc"); + None + } + } + } +} + +/// Puts argument string to the web clipboard +pub fn clipboard_copy(text: String) { + spawn_local(async move { + let window = web_sys::window().expect("window"); // { obj: val }; + + let nav = window.navigator(); + + let clipboard = nav.clipboard(); + match clipboard { + Some(a) => { + let p = a.write_text(&text); + let _result = wasm_bindgen_futures::JsFuture::from(p) + .await + .expect("clipboard populated"); + info!("copy to clipboard worked"); + } + None => { + warn!("failed to write clipboard data"); + } + }; + }); +} From 9032a4e3cf92adc89ab993e6f04c0179f303bf5d Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Sun, 28 May 2023 07:35:35 +0200 Subject: [PATCH 06/16] apply review; remove bad comment --- src/web_clipboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web_clipboard.rs b/src/web_clipboard.rs index 3f96e9cd2..e2f231a14 100644 --- a/src/web_clipboard.rs +++ b/src/web_clipboard.rs @@ -85,7 +85,7 @@ impl WebClipboardPaste { /// Puts argument string to the web clipboard pub fn clipboard_copy(text: String) { spawn_local(async move { - let window = web_sys::window().expect("window"); // { obj: val }; + let window = web_sys::window().expect("window"); let nav = window.navigator(); From ea933e7b97181783f6ff3a6d8033e5af632f18df Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 05:02:45 +0200 Subject: [PATCH 07/16] unified mut api for clipboard --- src/lib.rs | 6 +++--- src/systems.rs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9299aa89e..0c5b36318 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,7 +164,7 @@ impl EguiClipboard { /// Gets clipboard contents. Returns [`None`] if clipboard provider is unavailable or returns an error. #[must_use] #[cfg(not(target_arch = "wasm32"))] - pub fn get_contents(&self) -> Option { + pub fn get_contents(&mut self) -> Option { self.get_contents_impl() } @@ -185,12 +185,12 @@ impl EguiClipboard { } #[cfg(target_arch = "wasm32")] - fn set_contents_impl(&self, contents: &str) { + fn set_contents_impl(&mut self, contents: &str) { web_clipboard::clipboard_copy(contents.to_owned()); } #[cfg(not(target_arch = "wasm32"))] - fn get_contents_impl(&self) -> Option { + fn get_contents_impl(&mut self) -> Option { if let Some(mut clipboard) = self.get() { match clipboard.get_text() { Ok(contents) => return Some(contents), diff --git a/src/systems.rs b/src/systems.rs index 3544953e2..5e6f8dc52 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -53,10 +53,8 @@ impl<'w, 's> InputEvents<'w, 's> { #[allow(missing_docs)] #[derive(SystemParam)] pub struct InputResources<'w, 's> { - #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] + #[cfg(feature = "manage_clipboard")] pub egui_clipboard: ResMut<'w, crate::EguiClipboard>, - #[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))] - pub egui_clipboard: Res<'w, crate::EguiClipboard>, pub keyboard_input: Res<'w, Input>, #[system_param(ignore)] _marker: PhantomData<&'s ()>, From b45bea22508b0f6fafa8440077af4ed09ea88dc5 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 05:26:43 +0200 Subject: [PATCH 08/16] use crossbeam-channel to avoid explicit mutex --- Cargo.toml | 7 ++++--- src/lib.rs | 8 ++++---- src/{web_clipboard.rs => web.rs} | 23 +++++++---------------- 3 files changed, 15 insertions(+), 23 deletions(-) rename src/{web_clipboard.rs => web.rs} (81%) diff --git a/Cargo.toml b/Cargo.toml index cd242d6fd..1139cb007 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,11 +54,12 @@ web-sys = { version = "*", features = [ "Window", "Navigator", ] } -js-sys = "*" +js-sys = "0.3.63" wasm-bindgen = "0.2.84" -wasm-bindgen-futures = "*" -console_log = "*" +wasm-bindgen-futures = "0.4.36" +console_log = "1.0.0" log = "0.4" +crossbeam-channel = "0.5.8" [workspace] members = ["run-wasm"] diff --git a/src/lib.rs b/src/lib.rs index 0c5b36318..1c194189b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ pub mod egui_node; /// Clipboard management for web #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] -pub mod web_clipboard; +pub mod web; pub use egui; @@ -151,7 +151,7 @@ pub struct EguiClipboard { #[cfg(not(target_arch = "wasm32"))] clipboard: ThreadLocal>>, #[cfg(target_arch = "wasm32")] - clipboard: web_clipboard::WebClipboardPaste, + clipboard: web::WebClipboardPaste, } #[cfg(feature = "manage_clipboard")] @@ -186,7 +186,7 @@ impl EguiClipboard { #[cfg(target_arch = "wasm32")] fn set_contents_impl(&mut self, contents: &str) { - web_clipboard::clipboard_copy(contents.to_owned()); + web::clipboard_copy(contents.to_owned()); } #[cfg(not(target_arch = "wasm32"))] @@ -546,7 +546,7 @@ impl Plugin for EguiPlugin { world.init_resource::(); #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] - app.add_startup_system(web_clipboard::startup); + app.add_startup_system(web::startup_setup_web_events); app.add_startup_systems( ( setup_new_windows_system, diff --git a/src/web_clipboard.rs b/src/web.rs similarity index 81% rename from src/web_clipboard.rs rename to src/web.rs index e2f231a14..8eedbde5f 100644 --- a/src/web_clipboard.rs +++ b/src/web.rs @@ -1,20 +1,17 @@ -use std::sync::{ - mpsc::{self, Receiver, Sender}, - Arc, Mutex, -}; +use crossbeam_channel::{Receiver, Sender}; use bevy::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::EguiClipboard; -/// startup system for bevy to initialize clipboard. -pub fn startup(mut clipboard_channel: ResMut) { +/// startup system for bevy to initialize web events. +pub fn startup_setup_web_events(mut clipboard_channel: ResMut) { setup_clipboard_paste(&mut clipboard_channel.clipboard) } fn setup_clipboard_paste(clipboard_channel: &mut WebClipboardPaste) { - let (tx, rx): (Sender, Receiver) = mpsc::channel(); + let (tx, rx): (Sender, Receiver) = crossbeam_channel::bounded(1); use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; @@ -45,9 +42,7 @@ fn setup_clipboard_paste(clipboard_channel: &mut WebClipboardPaste) { .add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref()) .expect("Could not edd paste event listener."); closure.forget(); - *clipboard_channel = WebClipboardPaste { - rx: Some(Arc::new(Mutex::new(rx))), - }; + *clipboard_channel = WebClipboardPaste { rx: Some(rx) }; info!("setup_clipboard_paste OK"); } @@ -55,7 +50,7 @@ fn setup_clipboard_paste(clipboard_channel: &mut WebClipboardPaste) { /// To get data from web paste events #[derive(Default)] pub struct WebClipboardPaste { - rx: Option>>>, + rx: Option>, } impl WebClipboardPaste { @@ -64,11 +59,7 @@ impl WebClipboardPaste { pub fn try_read_clipboard_event(&mut self) -> Option { match &mut self.rx { Some(rx) => { - let Ok(unlock) = rx.try_lock() else { - info!("fail lock"); - return None; - }; - if let Ok(clipboard_string) = unlock.try_recv() { + if let Ok(clipboard_string) = rx.try_recv() { info!("received: {}", clipboard_string); return Some(clipboard_string); } From 0e7dd2410f0b91738bcdfdab70bc728121cb6a20 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 23:04:02 +0200 Subject: [PATCH 09/16] add copy and cut events --- src/lib.rs | 19 ++++-- src/systems.rs | 24 ++++++- src/web.rs | 97 -------------------------- src/web_clipboard.rs | 157 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 105 deletions(-) delete mode 100644 src/web.rs create mode 100644 src/web_clipboard.rs diff --git a/src/lib.rs b/src/lib.rs index 1c194189b..ef7a9bcb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,9 +60,11 @@ pub mod egui_node; /// Clipboard management for web #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] -pub mod web; +pub mod web_clipboard; pub use egui; +#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] +use web_clipboard::{WebEventCopy, WebEventCut, WebEventPaste}; use crate::{ egui_node::{EguiPipeline, EGUI_SHADER_HANDLE}, @@ -150,8 +152,15 @@ pub struct EguiInput(pub egui::RawInput); pub struct EguiClipboard { #[cfg(not(target_arch = "wasm32"))] clipboard: ThreadLocal>>, + /// for copy events. + #[cfg(target_arch = "wasm32")] + pub web_copy: web_clipboard::WebChannel, + /// for copy events. + #[cfg(target_arch = "wasm32")] + pub web_cut: web_clipboard::WebChannel, + /// for paste events, only supporting strings. #[cfg(target_arch = "wasm32")] - clipboard: web::WebClipboardPaste, + pub web_paste: web_clipboard::WebChannel, } #[cfg(feature = "manage_clipboard")] @@ -186,7 +195,7 @@ impl EguiClipboard { #[cfg(target_arch = "wasm32")] fn set_contents_impl(&mut self, contents: &str) { - web::clipboard_copy(contents.to_owned()); + web_clipboard::clipboard_copy(contents.to_owned()); } #[cfg(not(target_arch = "wasm32"))] @@ -203,7 +212,7 @@ impl EguiClipboard { #[cfg(target_arch = "wasm32")] #[allow(clippy::unnecessary_wraps)] fn get_contents_impl(&mut self) -> Option { - self.clipboard.try_read_clipboard_event() + self.web_paste.try_read_clipboard_event().map(|e| e.0) } #[cfg(not(target_arch = "wasm32"))] @@ -546,7 +555,7 @@ impl Plugin for EguiPlugin { world.init_resource::(); #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] - app.add_startup_system(web::startup_setup_web_events); + app.add_startup_system(web_clipboard::startup_setup_web_events); app.add_startup_systems( ( setup_new_windows_system, diff --git a/src/systems.rs b/src/systems.rs index 5e6f8dc52..d88f2ed1c 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -251,6 +251,7 @@ pub fn process_input_system( // copy, cut or paste on the key release. #[cfg(feature = "manage_clipboard")] { + #[cfg(not(target_arch = "wasm32"))] if command && pressed { match key { egui::Key::C => { @@ -260,7 +261,6 @@ pub fn process_input_system( focused_input.events.push(egui::Event::Cut); } egui::Key::V => { - #[cfg(not(target_arch = "wasm32"))] if let Some(contents) = input_resources.egui_clipboard.get_contents() { @@ -271,8 +271,26 @@ pub fn process_input_system( } } #[cfg(target_arch = "wasm32")] - if let Some(contents) = input_resources.egui_clipboard.get_contents() { - focused_input.events.push(egui::Event::Text(contents)); + { + if input_resources + .egui_clipboard + .web_copy + .try_read_clipboard_event() + .is_some() + { + focused_input.events.push(egui::Event::Copy); + } + if input_resources + .egui_clipboard + .web_cut + .try_read_clipboard_event() + .is_some() + { + focused_input.events.push(egui::Event::Cut); + } + if let Some(contents) = input_resources.egui_clipboard.get_contents() { + focused_input.events.push(egui::Event::Text(contents)); + } } } } diff --git a/src/web.rs b/src/web.rs deleted file mode 100644 index 8eedbde5f..000000000 --- a/src/web.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crossbeam_channel::{Receiver, Sender}; - -use bevy::prelude::*; -use wasm_bindgen_futures::spawn_local; - -use crate::EguiClipboard; - -/// startup system for bevy to initialize web events. -pub fn startup_setup_web_events(mut clipboard_channel: ResMut) { - setup_clipboard_paste(&mut clipboard_channel.clipboard) -} - -fn setup_clipboard_paste(clipboard_channel: &mut WebClipboardPaste) { - let (tx, rx): (Sender, Receiver) = crossbeam_channel::bounded(1); - - use wasm_bindgen::closure::Closure; - use wasm_bindgen::prelude::*; - - let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { - // TODO: maybe we should check if current canvas is selected ? not sure it's possible, - // but reacting to event at the document level will lead to problems if multiple games are on the same page. - match event - .clipboard_data() - .expect("could not get clipboard data.") - .get_data("text/plain") - { - Ok(data) => { - tx.send(data); - } - _ => { - info!("Not implemented."); - } - } - info!("{:?}", event.clipboard_data()) - }); - - // TODO: a lot of unwraps ; it's using documents because paste event set on a canvas do not trigger (also tested on firefox in vanilla javascript) - web_sys::window() - .unwrap() - .document() - .unwrap() - .add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref()) - .expect("Could not edd paste event listener."); - closure.forget(); - *clipboard_channel = WebClipboardPaste { rx: Some(rx) }; - - info!("setup_clipboard_paste OK"); -} - -/// To get data from web paste events -#[derive(Default)] -pub struct WebClipboardPaste { - rx: Option>, -} - -impl WebClipboardPaste { - /// Only returns Some if user explicitly triggered a paste event. - /// We are not querying the clipboard data without user input here (it would require permissions). - pub fn try_read_clipboard_event(&mut self) -> Option { - match &mut self.rx { - Some(rx) => { - if let Ok(clipboard_string) = rx.try_recv() { - info!("received: {}", clipboard_string); - return Some(clipboard_string); - } - None - } - None => { - info!("no arc"); - None - } - } - } -} - -/// Puts argument string to the web clipboard -pub fn clipboard_copy(text: String) { - spawn_local(async move { - let window = web_sys::window().expect("window"); - - let nav = window.navigator(); - - let clipboard = nav.clipboard(); - match clipboard { - Some(a) => { - let p = a.write_text(&text); - let _result = wasm_bindgen_futures::JsFuture::from(p) - .await - .expect("clipboard populated"); - info!("copy to clipboard worked"); - } - None => { - warn!("failed to write clipboard data"); - } - }; - }); -} diff --git a/src/web_clipboard.rs b/src/web_clipboard.rs new file mode 100644 index 000000000..a81dad15a --- /dev/null +++ b/src/web_clipboard.rs @@ -0,0 +1,157 @@ +use crossbeam_channel::{Receiver, Sender}; + +use bevy::prelude::*; +use wasm_bindgen_futures::spawn_local; + +use crate::EguiClipboard; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::*; + +/// startup system for bevy to initialize web events. +pub fn startup_setup_web_events(mut clipboard_channel: ResMut) { + setup_clipboard_copy(&mut clipboard_channel.web_copy); + setup_clipboard_cut(&mut clipboard_channel.web_cut); + setup_clipboard_paste(&mut clipboard_channel.web_paste); + + let window = web_sys::window().expect("window"); + + let nav = window.navigator(); + let platform = nav.platform(); + match platform { + Ok(p) => info!(p), + Err(e) => , + } + info!("{:?}", platform) +} + +/// To get data from web paste events +#[derive(Default)] +pub struct WebChannel { + rx: Option>, +} + +impl WebChannel { + /// Only returns Some if user explicitly triggered a paste event. + /// We are not querying the clipboard data without user input here (it would require permissions). + pub fn try_read_clipboard_event(&mut self) -> Option { + match &mut self.rx { + Some(rx) => { + if let Ok(clipboard_string) = rx.try_recv() { + return Some(clipboard_string); + } + None + } + None => None, + } + } +} + +/// User provided a string to paste +#[derive(Debug, Default)] +pub struct WebEventPaste(pub String); +/// User asked to cut +#[derive(Default)] +pub struct WebEventCut; +/// Used asked to copy +#[derive(Default)] +pub struct WebEventCopy; + +fn setup_clipboard_copy(clipboard_channel: &mut WebChannel) { + let (tx, rx): (Sender, Receiver) = crossbeam_channel::bounded(1); + + let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { + // TODO: maybe we should check if current canvas is selected ? not sure it's possible, + // but reacting to event at the document level will lead to problems if multiple games are on the same page. + tx.try_send(WebEventCopy); + }); + + // TODO: a lot of unwraps ; it's using documents because paste event set on a canvas do not trigger (also tested on firefox in vanilla javascript) + let listener = closure.as_ref().unchecked_ref(); + web_sys::window() + .unwrap() + .document() + .unwrap() + .add_event_listener_with_callback("copy", listener) + .expect("Could not add copy event listener."); + closure.forget(); + *clipboard_channel = WebChannel:: { rx: Some(rx) }; +} + +fn setup_clipboard_cut(clipboard_channel: &mut WebChannel) { + let (tx, rx): (Sender, Receiver) = crossbeam_channel::bounded(1); + + let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { + // TODO: maybe we should check if current canvas is selected ? not sure it's possible, + // but reacting to event at the document level will lead to problems if multiple games are on the same page. + tx.try_send(WebEventCut); + }); + + // TODO: a lot of unwraps ; it's using documents because paste event set on a canvas do not trigger (also tested on firefox in vanilla javascript) + let listener = closure.as_ref().unchecked_ref(); + web_sys::window() + .unwrap() + .document() + .unwrap() + .add_event_listener_with_callback("cut", listener) + .expect("Could not add cut event listener."); + closure.forget(); + *clipboard_channel = WebChannel:: { rx: Some(rx) }; +} + +fn setup_clipboard_paste(clipboard_channel: &mut WebChannel) { + let (tx, rx): (Sender, Receiver) = crossbeam_channel::bounded(1); + + let closure = Closure::::new(move |event: web_sys::ClipboardEvent| { + // TODO: maybe we should check if current canvas is selected ? not sure it's possible, + // but reacting to event at the document level will lead to problems if multiple games are on the same page. + match event + .clipboard_data() + .expect("could not get clipboard data.") + .get_data("text/plain") + { + Ok(data) => { + tx.try_send(WebEventPaste(data)); + } + _ => { + info!("Not implemented."); + } + } + info!("{:?}", event.clipboard_data()) + }); + + // TODO: a lot of unwraps ; it's using documents because paste event set on a canvas do not trigger (also tested on firefox in vanilla javascript) + let listener = closure.as_ref().unchecked_ref(); + web_sys::window() + .unwrap() + .document() + .unwrap() + .add_event_listener_with_callback("paste", listener) + .expect("Could not add paste event listener."); + closure.forget(); + *clipboard_channel = WebChannel:: { rx: Some(rx) }; + + info!("setup_clipboard_paste OK"); +} + +/// Puts argument string to the web clipboard +pub fn clipboard_copy(text: String) { + spawn_local(async move { + let window = web_sys::window().expect("window"); + + let nav = window.navigator(); + + let clipboard = nav.clipboard(); + match clipboard { + Some(a) => { + let p = a.write_text(&text); + let _result = wasm_bindgen_futures::JsFuture::from(p) + .await + .expect("clipboard populated"); + info!("copy to clipboard worked"); + } + None => { + warn!("failed to write clipboard data"); + } + }; + }); +} From e3fa6e39a287dee18e406db37c82d5673ced8eba Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 11:29:39 +0200 Subject: [PATCH 10/16] Update src/web_clipboard.rs --- src/web_clipboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web_clipboard.rs b/src/web_clipboard.rs index a81dad15a..8d1d774be 100644 --- a/src/web_clipboard.rs +++ b/src/web_clipboard.rs @@ -24,7 +24,7 @@ pub fn startup_setup_web_events(mut clipboard_channel: ResMut) { info!("{:?}", platform) } -/// To get data from web paste events +/// To get data from web events #[derive(Default)] pub struct WebChannel { rx: Option>, From 43af68ad09615941b797bd2d42e00807adb95223 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 23:33:56 +0200 Subject: [PATCH 11/16] comments and naming improvements --- src/web_clipboard.rs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/web_clipboard.rs b/src/web_clipboard.rs index 8d1d774be..a5b5b63f7 100644 --- a/src/web_clipboard.rs +++ b/src/web_clipboard.rs @@ -12,16 +12,6 @@ pub fn startup_setup_web_events(mut clipboard_channel: ResMut) { setup_clipboard_copy(&mut clipboard_channel.web_copy); setup_clipboard_cut(&mut clipboard_channel.web_cut); setup_clipboard_paste(&mut clipboard_channel.web_paste); - - let window = web_sys::window().expect("window"); - - let nav = window.navigator(); - let platform = nav.platform(); - match platform { - Ok(p) => info!(p), - Err(e) => , - } - info!("{:?}", platform) } /// To get data from web events @@ -31,13 +21,12 @@ pub struct WebChannel { } impl WebChannel { - /// Only returns Some if user explicitly triggered a paste event. - /// We are not querying the clipboard data without user input here (it would require permissions). + /// Only returns Some if user explicitly triggered an event. Should be called each frame to react as soon as the event is fired. pub fn try_read_clipboard_event(&mut self) -> Option { match &mut self.rx { Some(rx) => { - if let Ok(clipboard_string) = rx.try_recv() { - return Some(clipboard_string); + if let Ok(data) = rx.try_recv() { + return Some(data); } None } From 9d743fd03cd2c52d5b2bac74deb4c71b0fd724f4 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 12:33:27 +0200 Subject: [PATCH 12/16] store new resource to detect if we're on a mac --- src/lib.rs | 19 +++++++++++++++++++ src/systems.rs | 11 ++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ef7a9bcb8..1eb5f77a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -144,6 +144,10 @@ impl Default for EguiSettings { #[derive(Component, Clone, Debug, Default, Deref, DerefMut)] pub struct EguiInput(pub egui::RawInput); +/// A resource to check if we're on a mac, correctly detects on web too. +#[derive(Resource)] +pub struct IsMac(pub bool); + /// A resource for accessing clipboard. /// /// The resource is available only if `manage_clipboard` feature is enabled. @@ -553,6 +557,21 @@ impl Plugin for EguiPlugin { world.init_resource::(); world.init_resource::(); world.init_resource::(); + if !world.contains_resource::() { + let mut is_mac = cfg!(target_os = "macos"); + #[cfg(target_arch = "wasm32")] + { + let window = web_sys::window().expect("window"); + + let nav = window.navigator(); + let platform = nav.platform(); + if let Ok(platform) = platform { + is_mac = platform.starts_with("Mac"); + } + } + log::info!(is_mac); + world.insert_resource(IsMac(is_mac)); + } #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] app.add_startup_system(web_clipboard::startup_setup_web_events); diff --git a/src/systems.rs b/src/systems.rs index d88f2ed1c..7b5def730 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,5 +1,5 @@ use crate::{ - EguiContext, EguiContextQuery, EguiInput, EguiMousePosition, EguiSettings, WindowSize, + EguiContext, EguiContextQuery, EguiInput, EguiMousePosition, EguiSettings, IsMac, WindowSize, }; #[cfg(feature = "open_url")] use bevy::log; @@ -71,6 +71,7 @@ pub struct ContextSystemParams<'w, 's> { /// Processes Bevy input and feeds it to Egui. pub fn process_input_system( + mut is_mac: Res, mut input_events: InputEvents, mut input_resources: InputResources, mut context_params: ContextSystemParams, @@ -101,12 +102,8 @@ pub fn process_input_system( let win = input_resources.keyboard_input.pressed(KeyCode::LWin) || input_resources.keyboard_input.pressed(KeyCode::RWin); - let mac_cmd = if cfg!(target_os = "macos") { - win - } else { - false - }; - let command = if cfg!(target_os = "macos") { win } else { ctrl }; + let mac_cmd = if is_mac.0 { win } else { false }; + let command = if is_mac.0 { win } else { ctrl }; let modifiers = egui::Modifiers { alt, From 070f1085377c9fe20a25282a9b275364febef420 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 21:13:25 +0200 Subject: [PATCH 13/16] Update .cargo/config.toml Co-authored-by: Vladyslav Batyrenko --- .cargo/config.toml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 2d35b1973..37505ddfb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,13 +1,6 @@ [alias] run-wasm = ["run", "--release", "--package", "run-wasm", "--"] -# Credits to https://github.com/emilk/egui - -# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work -# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html -# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility -#[build] -#target = "wasm32-unknown-unknown" - +# Using unstable APIs is required for writing to clipboard [target.wasm32-unknown-unknown] rustflags = ["--cfg=web_sys_unstable_apis"] From 8768a9cf50968264ac1edbf5973832b044600efe Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 31 May 2023 23:53:38 +0200 Subject: [PATCH 14/16] res IsMac now a local, init via Once; removed comments, use user-agent --- Cargo.toml | 2 +- src/lib.rs | 19 ------------------- src/systems.rs | 27 +++++++++++++++++++++++---- src/web_clipboard.rs | 21 ++++++--------------- 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1139cb007..16341302c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ bevy = { version = "0.10", default-features = false, features = [ [target.'cfg(target_arch = "wasm32")'.dependencies] winit = "0.28" -web-sys = { version = "*", features = [ +web-sys = { version = "0.3.63", features = [ "Clipboard", "ClipboardEvent", "DataTransfer", diff --git a/src/lib.rs b/src/lib.rs index 1eb5f77a6..ef7a9bcb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -144,10 +144,6 @@ impl Default for EguiSettings { #[derive(Component, Clone, Debug, Default, Deref, DerefMut)] pub struct EguiInput(pub egui::RawInput); -/// A resource to check if we're on a mac, correctly detects on web too. -#[derive(Resource)] -pub struct IsMac(pub bool); - /// A resource for accessing clipboard. /// /// The resource is available only if `manage_clipboard` feature is enabled. @@ -557,21 +553,6 @@ impl Plugin for EguiPlugin { world.init_resource::(); world.init_resource::(); world.init_resource::(); - if !world.contains_resource::() { - let mut is_mac = cfg!(target_os = "macos"); - #[cfg(target_arch = "wasm32")] - { - let window = web_sys::window().expect("window"); - - let nav = window.navigator(); - let platform = nav.platform(); - if let Ok(platform) = platform { - is_mac = platform.starts_with("Mac"); - } - } - log::info!(is_mac); - world.insert_resource(IsMac(is_mac)); - } #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))] app.add_startup_system(web_clipboard::startup_setup_web_events); diff --git a/src/systems.rs b/src/systems.rs index 7b5def730..c3aa20314 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,5 +1,5 @@ use crate::{ - EguiContext, EguiContextQuery, EguiInput, EguiMousePosition, EguiSettings, IsMac, WindowSize, + EguiContext, EguiContextQuery, EguiInput, EguiMousePosition, EguiSettings, WindowSize, }; #[cfg(feature = "open_url")] use bevy::log; @@ -71,14 +71,33 @@ pub struct ContextSystemParams<'w, 's> { /// Processes Bevy input and feeds it to Egui. pub fn process_input_system( - mut is_mac: Res, mut input_events: InputEvents, mut input_resources: InputResources, mut context_params: ContextSystemParams, egui_settings: Res, mut egui_mouse_position: ResMut, time: Res