-
Notifications
You must be signed in to change notification settings - Fork 252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
web clipboard handling #178
Changes from 5 commits
493a21f
205cdde
9e85a90
69c447d
0ec3ed9
9032a4e
ea933e7
b45bea2
070f108
0e7dd24
e3fa6e3
43af68a
9d743fd
8768a9c
5c4f550
fe877d7
f97ae8d
d6b726b
feb884c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
|
||
Vrixyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[target.wasm32-unknown-unknown] | ||
rustflags = ["--cfg=web_sys_unstable_apis"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
fn main() { | ||
cargo_run_wasm::run_wasm_with_css("body { margin: 0px; }"); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see #178 (comment) ; to be noted we can have mut only for wasm if we want. |
||
#[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))] | ||
pub egui_clipboard: Res<'w, crate::EguiClipboard>, | ||
pub keyboard_input: Res<'w, Input<KeyCode>>, | ||
#[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<EguiSettings>, | ||
mut egui_mouse_position: ResMut<EguiMousePosition>, | ||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (to be clear, this is not a but introduced in this PR) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, good catch. This makes me wonder how we can detect running on MacOS in wasm. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. eframe uses events for all those, deferring the responsibility to the browser: https://github.com/emilk/egui/blob/307565efa55158cfa6b82d2e8fdc4c4914b954ed/crates/eframe/src/web/events.rs#L184 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels like we should adopt the same approach. Let's keep the whole block enabled only for non-wasm targets, whereas for wasm we'll just read from channels (which in turn will get messages on web_sys events). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is important to fix, as atm on MacOS, users need to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I feel like it means we would bypass winit/bevy key inputs in wasm :/ could this possibly fixed upstream 🤔 ? for the record, ctrl-A works (kinda) on wasm; on azerty it's mapped to In all hindsight, I'd suggest a simpler approach where we detect if we run on mac or not through https://docs.rs/web-sys/latest/web_sys/struct.Navigator.html#method.platform, and adapt our meta key from there in case of Is it ok for you? |
||
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)); | ||
} | ||
} | ||
} | ||
|
Vrixyz marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<EguiClipboard>) { | ||
setup_clipboard_paste(&mut clipboard_channel.clipboard) | ||
} | ||
|
||
fn setup_clipboard_paste(clipboard_channel: &mut WebClipboardPaste) { | ||
let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel(); | ||
|
||
use wasm_bindgen::closure::Closure; | ||
use wasm_bindgen::prelude::*; | ||
|
||
let closure = Closure::<dyn FnMut(_)>::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. | ||
Vrixyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are these logs useful ? I suspect they would be noisy for end user. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed most and/or changed for errors/warn |
||
}); | ||
|
||
// 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<Arc<Mutex<Receiver<String>>>>, | ||
Vrixyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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<String> { | ||
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 }; | ||
Vrixyz marked this conversation as resolved.
Show resolved
Hide resolved
Vrixyz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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"); | ||
} | ||
}; | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think cargo run-wasm is quite handy to easily test a wasm version ; I can make another PR if interested