Skip to content

Commit 7dfc36c

Browse files
n0samuDinnerbone
authored andcommitted
web: Support pasting from clipboard
1 parent ee95692 commit 7dfc36c

File tree

7 files changed

+79
-12
lines changed

7 files changed

+79
-12
lines changed

.cargo/config.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
[target.'cfg(all())']
22
# NOTE that the web build overrides this setting in package.json via the RUSTFLAGS environment variable
33
rustflags = [
4+
# We need to specify this flag for all targets because Clippy checks all of our code against all targets
5+
# and our web code does not compile without this flag
6+
"--cfg=web_sys_unstable_apis",
7+
48
# CLIPPY LINT SETTINGS
59
# This is a workaround to configure lints for the entire workspace, pending the ability to configure this via TOML.
610
# See: https://github.com/rust-lang/cargo/issues/5034
711
# https://github.com/EmbarkStudios/rust-ecosystem/issues/22#issuecomment-947011395
12+
# TODO: Move these to the root Cargo.toml once support is merged and stable
13+
# See: https://github.com/rust-lang/cargo/pull/12148
814

915
# Clippy nightly often adds new/buggy lints that we want to ignore.
1016
# Don't warn about these new lints on stable.

core/src/backend/ui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ impl UiBackend for NullUiBackend {
159159
fn set_mouse_cursor(&mut self, _cursor: MouseCursor) {}
160160

161161
fn clipboard_content(&mut self) -> String {
162-
"".to_string()
162+
"".into()
163163
}
164164

165165
fn set_clipboard_content(&mut self, _content: String) {}

desktop/src/ui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl UiBackend for DesktopUiBackend {
6363
}
6464

6565
fn clipboard_content(&mut self) -> String {
66-
self.clipboard.get_text().unwrap_or_else(|_| "".to_string())
66+
self.clipboard.get_text().unwrap_or_default()
6767
}
6868

6969
fn set_clipboard_content(&mut self, content: String) {

web/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ version = "0.3.63"
6060
features = [
6161
"AddEventListenerOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioContext",
6262
"AudioDestinationNode", "AudioNode", "AudioParam", "Blob", "BlobPropertyBag",
63-
"ChannelMergerNode", "ChannelSplitterNode", "Element", "Event", "EventTarget", "GainNode",
64-
"HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement", "HtmlInputElement",
65-
"HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent", "Request", "RequestInit",
66-
"Response", "Storage", "WheelEvent", "Window",
63+
"ChannelMergerNode", "ChannelSplitterNode", "ClipboardEvent", "DataTransfer", "Element", "Event",
64+
"EventTarget", "GainNode", "HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement",
65+
"HtmlInputElement", "HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent",
66+
"Request", "RequestInit", "Response", "Storage", "WheelEvent", "Window",
6767
]

web/packages/core/src/ruffle-player.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,9 @@ export class RufflePlayer extends HTMLElement {
11831183
this.virtualKeyboard.focus({ preventScroll: true });
11841184
}
11851185
}
1186+
protected isVirtualKeyboardFocused(): boolean {
1187+
return this.shadow.activeElement === this.virtualKeyboard;
1188+
}
11861189

11871190
private contextMenuItems(isTouch: boolean): Array<ContextMenuItem | null> {
11881191
const CHECKMARK = String.fromCharCode(0x2713);

web/src/lib.rs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ use tracing_wasm::{WASMLayer, WASMLayerConfigBuilder};
3838
use url::Url;
3939
use wasm_bindgen::{prelude::*, JsCast, JsValue};
4040
use web_sys::{
41-
AddEventListenerOptions, Element, Event, EventTarget, HtmlCanvasElement, HtmlElement,
42-
KeyboardEvent, PointerEvent, WheelEvent, Window,
41+
AddEventListenerOptions, ClipboardEvent, Element, Event, EventTarget, HtmlCanvasElement,
42+
HtmlElement, KeyboardEvent, PointerEvent, WheelEvent, Window,
4343
};
4444

4545
static RUFFLE_GLOBAL_PANIC: Once = Once::new();
@@ -78,6 +78,7 @@ struct RuffleInstance {
7878
mouse_wheel_callback: Option<Closure<dyn FnMut(WheelEvent)>>,
7979
key_down_callback: Option<Closure<dyn FnMut(KeyboardEvent)>>,
8080
key_up_callback: Option<Closure<dyn FnMut(KeyboardEvent)>>,
81+
paste_callback: Option<Closure<dyn FnMut(ClipboardEvent)>>,
8182
unload_callback: Option<Closure<dyn FnMut(Event)>>,
8283
has_focus: bool,
8384
trace_observer: Arc<RefCell<JsValue>>,
@@ -119,6 +120,9 @@ extern "C" {
119120

120121
#[wasm_bindgen(method, js_name = "openVirtualKeyboard")]
121122
fn open_virtual_keyboard(this: &JavascriptPlayer);
123+
124+
#[wasm_bindgen(method, js_name = "isVirtualKeyboardFocused")]
125+
fn is_virtual_keyboard_focused(this: &JavascriptPlayer) -> bool;
122126
}
123127

124128
struct JavascriptInterface {
@@ -619,6 +623,7 @@ impl Ruffle {
619623
mouse_wheel_callback: None,
620624
key_down_callback: None,
621625
key_up_callback: None,
626+
paste_callback: None,
622627
unload_callback: None,
623628
timestamp: None,
624629
has_focus: false,
@@ -837,6 +842,7 @@ impl Ruffle {
837842
let key_down_callback = Closure::new(move |js_event: KeyboardEvent| {
838843
let _ = ruffle.with_instance(|instance| {
839844
if instance.has_focus {
845+
let mut paste_event = false;
840846
let _ = instance.with_core_mut(|core| {
841847
let key_code = web_to_ruffle_key_code(&js_event.code());
842848
let key_char = web_key_to_codepoint(&js_event.key());
@@ -848,13 +854,23 @@ impl Ruffle {
848854
is_ctrl_cmd,
849855
js_event.shift_key(),
850856
) {
851-
core.handle_event(PlayerEvent::TextControl { code: control_code });
857+
paste_event = control_code == TextControlCode::Paste;
858+
// The JS paste event fires separately and the clipboard text is not available until then,
859+
// so we need to wait before handling it
860+
if !paste_event {
861+
core.handle_event(PlayerEvent::TextControl {
862+
code: control_code,
863+
});
864+
}
852865
} else if let Some(codepoint) = key_char {
853866
core.handle_event(PlayerEvent::TextInput { codepoint });
854867
}
855868
});
856869

857-
js_event.prevent_default();
870+
// Don't prevent the JS paste event from firing
871+
if !paste_event {
872+
js_event.prevent_default();
873+
}
858874
}
859875
});
860876
});
@@ -867,6 +883,31 @@ impl Ruffle {
867883
.warn_on_error();
868884
instance.key_down_callback = Some(key_down_callback);
869885

886+
let paste_callback = Closure::new(move |js_event: ClipboardEvent| {
887+
let _ = ruffle.with_instance(|instance| {
888+
if instance.has_focus {
889+
let _ = instance.with_core_mut(|core| {
890+
let clipboard_content = if let Some(content) = js_event.clipboard_data()
891+
{
892+
content.get_data("text/plain").unwrap_or_default()
893+
} else {
894+
"".into()
895+
};
896+
core.ui_mut().set_clipboard_content(clipboard_content);
897+
core.handle_event(PlayerEvent::TextControl {
898+
code: TextControlCode::Paste,
899+
});
900+
});
901+
js_event.prevent_default();
902+
}
903+
});
904+
});
905+
906+
window
907+
.add_event_listener_with_callback("paste", paste_callback.as_ref().unchecked_ref())
908+
.warn_on_error();
909+
instance.paste_callback = Some(paste_callback);
910+
870911
// Create keyup event handler.
871912
let key_up_callback = Closure::new(move |js_event: KeyboardEvent| {
872913
let _ = ruffle.with_instance(|instance| {
@@ -1258,6 +1299,14 @@ impl Drop for RuffleInstance {
12581299
)
12591300
.warn_on_error();
12601301
}
1302+
if let Some(paste_callback) = self.paste_callback.take() {
1303+
self.window
1304+
.remove_event_listener_with_callback(
1305+
"paste",
1306+
paste_callback.as_ref().unchecked_ref(),
1307+
)
1308+
.warn_on_error();
1309+
}
12611310
if let Some(key_up_callback) = self.key_up_callback.take() {
12621311
self.window
12631312
.remove_event_listener_with_callback(

web/src/ui.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub struct WebUiBackend {
1414
cursor_visible: bool,
1515
cursor: MouseCursor,
1616
language: LanguageIdentifier,
17+
clipboard_content: String,
1718
}
1819

1920
impl WebUiBackend {
@@ -29,6 +30,7 @@ impl WebUiBackend {
2930
cursor_visible: true,
3031
cursor: MouseCursor::Arrow,
3132
language,
33+
clipboard_content: "".into(),
3234
}
3335
}
3436

@@ -66,11 +68,13 @@ impl UiBackend for WebUiBackend {
6668
}
6769

6870
fn clipboard_content(&mut self) -> String {
69-
tracing::warn!("get clipboard not implemented");
70-
"".to_string()
71+
// On web, clipboard content is not directly accessible due to security restrictions,
72+
// but pasting from the clipboard is supported via the JS `paste` event
73+
self.clipboard_content.to_owned()
7174
}
7275

7376
fn set_clipboard_content(&mut self, content: String) {
77+
self.clipboard_content = content.to_owned();
7478
// We use `document.execCommand("copy")` as `navigator.clipboard.writeText("string")`
7579
// is available only in secure contexts (HTTPS).
7680
if let Some(element) = self.canvas.parent_element() {
@@ -86,6 +90,7 @@ impl UiBackend for WebUiBackend {
8690
.dyn_into()
8791
.expect("create_element(\"textarea\") didn't give us a textarea");
8892

93+
let editing_text = self.js_player.is_virtual_keyboard_focused();
8994
textarea.set_value(&content);
9095
let _ = element.append_child(&textarea);
9196
textarea.select();
@@ -102,6 +107,10 @@ impl UiBackend for WebUiBackend {
102107
}
103108

104109
let _ = element.remove_child(&textarea);
110+
if editing_text {
111+
// Return focus to the text area
112+
self.js_player.open_virtual_keyboard();
113+
}
105114
}
106115
}
107116

0 commit comments

Comments
 (0)