Skip to content
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

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[alias]
run-wasm = ["run", "--release", "--package", "run-wasm", "--"]

[workspace]
"run-wasm"
6 changes: 6 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[alias]
run-wasm = ["run", "--release", "--package", "run-wasm", "--"]
Copy link
Contributor Author

@Vrixyz Vrixyz May 22, 2023

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


# Using unstable APIs is required for writing to clipboard
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=web_sys_unstable_apis"]
29 changes: 23 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,14 @@ name = "ui"
required-features = ["render"]

[dependencies]
bevy = { version = "0.13", default-features = false, features = [
"bevy_asset",
] }
bevy = { version = "0.13", default-features = false, features = ["bevy_asset"] }
egui = { version = "0.26", default-features = false, features = ["bytemuck"] }
webbrowser = { version = "0.8.2", optional = true }

[target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies]
arboard = { version = "3.2.0", optional = true }
thread_local = { version = "1.1.0", optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3.63", features = ["Navigator"] }

[dev-dependencies]
version-sync = "0.9.4"
bevy = { version = "0.13", default-features = false, features = [
Expand All @@ -60,4 +55,26 @@ bevy = { version = "0.13", default-features = false, features = [
"bevy_pbr",
"bevy_core_pipeline",
"tonemapping_luts",
"webgl2",
] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
winit = "0.29"
web-sys = { version = "0.3.63", features = [
"Clipboard",
"ClipboardEvent",
"DataTransfer",
'Document',
'EventTarget',
"Window",
"Navigator",
] }
js-sys = "0.3.63"
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.36"
console_log = "1.0.0"
log = "0.4"
crossbeam-channel = "0.5.8"
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved

[workspace]
members = ["run-wasm"]
8 changes: 7 additions & 1 deletion examples/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ fn main() {
.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
.insert_resource(Msaa::Sample4)
.init_resource::<UiState>()
.add_plugins(DefaultPlugins)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
prevent_default_event_handling: false,
..default()
}),
..default()
}))
.add_plugins(EguiPlugin)
.add_systems(Startup, configure_visuals_system)
.add_systems(Startup, configure_ui_state_system)
Expand Down
9 changes: 9 additions & 0 deletions run-wasm/Cargo.toml
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"
3 changes: 3 additions & 0 deletions run-wasm/src/main.rs
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; }");
}
41 changes: 32 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ pub mod systems;
#[cfg(feature = "render")]
pub mod egui_node;

/// Clipboard management for web
#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
pub mod web_clipboard;

pub use egui;
#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
use web_clipboard::{WebEventCopy, WebEventCut, WebEventPaste};

use crate::systems::*;
#[cfg(feature = "render")]
Expand Down Expand Up @@ -179,8 +185,15 @@ pub struct EguiInput(pub egui::RawInput);
pub struct EguiClipboard {
#[cfg(not(target_arch = "wasm32"))]
clipboard: ThreadLocal<Option<RefCell<Clipboard>>>,
/// for copy events.
#[cfg(target_arch = "wasm32")]
pub web_copy: web_clipboard::WebChannel<WebEventCopy>,
/// for copy events.
#[cfg(target_arch = "wasm32")]
pub web_cut: web_clipboard::WebChannel<WebEventCut>,
/// for paste events, only supporting strings.
#[cfg(target_arch = "wasm32")]
clipboard: String,
pub web_paste: web_clipboard::WebChannel<WebEventPaste>,
}

#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
Expand All @@ -192,12 +205,20 @@ impl EguiClipboard {

/// Gets clipboard contents. Returns [`None`] if clipboard provider is unavailable or returns an error.
#[must_use]
pub fn get_contents(&self) -> Option<String> {
#[cfg(not(target_arch = "wasm32"))]
pub fn get_contents(&mut self) -> Option<String> {
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<String> {
self.get_contents_impl()
}

#[cfg(not(target_arch = "wasm32"))]
fn set_contents_impl(&self, contents: &str) {
fn set_contents_impl(&mut self, contents: &str) {
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand All @@ -207,24 +228,24 @@ impl EguiClipboard {

#[cfg(target_arch = "wasm32")]
fn set_contents_impl(&mut self, contents: &str) {
self.clipboard = contents.to_owned();
web_clipboard::clipboard_copy(contents.to_owned());
}

#[cfg(not(target_arch = "wasm32"))]
fn get_contents_impl(&self) -> Option<String> {
fn get_contents_impl(&mut self) -> Option<String> {
if let Some(mut clipboard) = self.get() {
match clipboard.get_text() {
Ok(contents) => return Some(contents),
Err(err) => log::info!("Failed to get clipboard contents: {:?}", err),
Err(err) => log::error!("Failed to get clipboard contents: {:?}", err),
}
};
None
}

#[cfg(target_arch = "wasm32")]
#[allow(clippy::unnecessary_wraps)]
fn get_contents_impl(&self) -> Option<String> {
Some(self.clipboard.clone())
fn get_contents_impl(&mut self) -> Option<String> {
self.web_paste.try_read_clipboard_event().map(|e| e.0)
}

#[cfg(not(target_arch = "wasm32"))]
Expand All @@ -234,7 +255,7 @@ impl EguiClipboard {
Clipboard::new()
.map(RefCell::new)
.map_err(|err| {
log::info!("Failed to initialize clipboard: {:?}", err);
log::error!("Failed to initialize clipboard: {:?}", err);
})
.ok()
})
Expand Down Expand Up @@ -597,6 +618,8 @@ impl Plugin for EguiPlugin {
#[cfg(feature = "render")]
app.add_plugins(ExtractComponentPlugin::<EguiRenderOutput>::default());

#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
app.add_systems(PreStartup, web_clipboard::startup_setup_web_events);
app.add_systems(
PreStartup,
(
Expand Down
52 changes: 39 additions & 13 deletions src/systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub struct ModifierKeysState {
#[derive(SystemParam)]
pub struct InputResources<'w, 's> {
#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
pub egui_clipboard: Res<'w, crate::EguiClipboard>,
pub egui_clipboard: ResMut<'w, crate::EguiClipboard>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

pub modifier_keys_state: Local<'s, ModifierKeysState>,
#[system_param(ignore)]
_marker: PhantomData<&'w ()>,
Expand Down Expand Up @@ -110,7 +110,7 @@ pub fn process_input_system(
if let Some(window) = web_sys::window() {
let nav = window.navigator();
if let Ok(user_agent) = nav.user_agent() {
if user_agent.to_ascii_lowercase().contains("Mac") {
if user_agent.to_ascii_lowercase().contains("mac") {
*context_params.is_macos = true;
}
}
Expand All @@ -130,7 +130,6 @@ pub fn process_input_system(
None
};
}

let mut keyboard_input_events = Vec::new();
for event in input_events.ev_keyboard_input.read() {
// Copy the events as we might want to pass them to an Egui context later.
Expand All @@ -149,7 +148,7 @@ pub fn process_input_system(
Key::Alt => {
input_resources.modifier_keys_state.alt = state.is_pressed();
}
Key::Super => {
Key::Super | Key::Meta => {
input_resources.modifier_keys_state.win = state.is_pressed();
}
_ => {}
Expand Down Expand Up @@ -306,20 +305,47 @@ 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(all(feature = "manage_clipboard", not(target_os = "android")))]
if command && ev.state.is_pressed() {
match key {
egui::Key::C => {
{
#[cfg(not(target_arch = "wasm32"))]
if command && ev.state.is_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))
}
}
_ => {}
}
}
#[cfg(target_arch = "wasm32")]
{
if input_resources
.egui_clipboard
.web_copy
.try_read_clipboard_event()
.is_some()
{
focused_input.events.push(egui::Event::Copy);
}
egui::Key::X => {
if input_resources
.egui_clipboard
.web_cut
.try_read_clipboard_event()
.is_some()
{
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 let Some(contents) = input_resources.egui_clipboard.get_contents() {
focused_input.events.push(egui::Event::Text(contents));
}
_ => {}
}
}
}
Expand Down
Loading
Loading