web: Support pasting from clipboard
This commit is contained in:
parent
ee956927b7
commit
7dfc36c6fc
|
@ -1,10 +1,16 @@
|
|||
[target.'cfg(all())']
|
||||
# NOTE that the web build overrides this setting in package.json via the RUSTFLAGS environment variable
|
||||
rustflags = [
|
||||
# We need to specify this flag for all targets because Clippy checks all of our code against all targets
|
||||
# and our web code does not compile without this flag
|
||||
"--cfg=web_sys_unstable_apis",
|
||||
|
||||
# CLIPPY LINT SETTINGS
|
||||
# This is a workaround to configure lints for the entire workspace, pending the ability to configure this via TOML.
|
||||
# See: https://github.com/rust-lang/cargo/issues/5034
|
||||
# https://github.com/EmbarkStudios/rust-ecosystem/issues/22#issuecomment-947011395
|
||||
# TODO: Move these to the root Cargo.toml once support is merged and stable
|
||||
# See: https://github.com/rust-lang/cargo/pull/12148
|
||||
|
||||
# Clippy nightly often adds new/buggy lints that we want to ignore.
|
||||
# Don't warn about these new lints on stable.
|
||||
|
|
|
@ -159,7 +159,7 @@ impl UiBackend for NullUiBackend {
|
|||
fn set_mouse_cursor(&mut self, _cursor: MouseCursor) {}
|
||||
|
||||
fn clipboard_content(&mut self) -> String {
|
||||
"".to_string()
|
||||
"".into()
|
||||
}
|
||||
|
||||
fn set_clipboard_content(&mut self, _content: String) {}
|
||||
|
|
|
@ -63,7 +63,7 @@ impl UiBackend for DesktopUiBackend {
|
|||
}
|
||||
|
||||
fn clipboard_content(&mut self) -> String {
|
||||
self.clipboard.get_text().unwrap_or_else(|_| "".to_string())
|
||||
self.clipboard.get_text().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_clipboard_content(&mut self, content: String) {
|
||||
|
|
|
@ -60,8 +60,8 @@ version = "0.3.63"
|
|||
features = [
|
||||
"AddEventListenerOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioContext",
|
||||
"AudioDestinationNode", "AudioNode", "AudioParam", "Blob", "BlobPropertyBag",
|
||||
"ChannelMergerNode", "ChannelSplitterNode", "Element", "Event", "EventTarget", "GainNode",
|
||||
"HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement", "HtmlInputElement",
|
||||
"HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent", "Request", "RequestInit",
|
||||
"Response", "Storage", "WheelEvent", "Window",
|
||||
"ChannelMergerNode", "ChannelSplitterNode", "ClipboardEvent", "DataTransfer", "Element", "Event",
|
||||
"EventTarget", "GainNode", "HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement",
|
||||
"HtmlInputElement", "HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent",
|
||||
"Request", "RequestInit", "Response", "Storage", "WheelEvent", "Window",
|
||||
]
|
||||
|
|
|
@ -1183,6 +1183,9 @@ export class RufflePlayer extends HTMLElement {
|
|||
this.virtualKeyboard.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
protected isVirtualKeyboardFocused(): boolean {
|
||||
return this.shadow.activeElement === this.virtualKeyboard;
|
||||
}
|
||||
|
||||
private contextMenuItems(isTouch: boolean): Array<ContextMenuItem | null> {
|
||||
const CHECKMARK = String.fromCharCode(0x2713);
|
||||
|
|
|
@ -38,8 +38,8 @@ use tracing_wasm::{WASMLayer, WASMLayerConfigBuilder};
|
|||
use url::Url;
|
||||
use wasm_bindgen::{prelude::*, JsCast, JsValue};
|
||||
use web_sys::{
|
||||
AddEventListenerOptions, Element, Event, EventTarget, HtmlCanvasElement, HtmlElement,
|
||||
KeyboardEvent, PointerEvent, WheelEvent, Window,
|
||||
AddEventListenerOptions, ClipboardEvent, Element, Event, EventTarget, HtmlCanvasElement,
|
||||
HtmlElement, KeyboardEvent, PointerEvent, WheelEvent, Window,
|
||||
};
|
||||
|
||||
static RUFFLE_GLOBAL_PANIC: Once = Once::new();
|
||||
|
@ -78,6 +78,7 @@ struct RuffleInstance {
|
|||
mouse_wheel_callback: Option<Closure<dyn FnMut(WheelEvent)>>,
|
||||
key_down_callback: Option<Closure<dyn FnMut(KeyboardEvent)>>,
|
||||
key_up_callback: Option<Closure<dyn FnMut(KeyboardEvent)>>,
|
||||
paste_callback: Option<Closure<dyn FnMut(ClipboardEvent)>>,
|
||||
unload_callback: Option<Closure<dyn FnMut(Event)>>,
|
||||
has_focus: bool,
|
||||
trace_observer: Arc<RefCell<JsValue>>,
|
||||
|
@ -119,6 +120,9 @@ extern "C" {
|
|||
|
||||
#[wasm_bindgen(method, js_name = "openVirtualKeyboard")]
|
||||
fn open_virtual_keyboard(this: &JavascriptPlayer);
|
||||
|
||||
#[wasm_bindgen(method, js_name = "isVirtualKeyboardFocused")]
|
||||
fn is_virtual_keyboard_focused(this: &JavascriptPlayer) -> bool;
|
||||
}
|
||||
|
||||
struct JavascriptInterface {
|
||||
|
@ -619,6 +623,7 @@ impl Ruffle {
|
|||
mouse_wheel_callback: None,
|
||||
key_down_callback: None,
|
||||
key_up_callback: None,
|
||||
paste_callback: None,
|
||||
unload_callback: None,
|
||||
timestamp: None,
|
||||
has_focus: false,
|
||||
|
@ -837,6 +842,7 @@ impl Ruffle {
|
|||
let key_down_callback = Closure::new(move |js_event: KeyboardEvent| {
|
||||
let _ = ruffle.with_instance(|instance| {
|
||||
if instance.has_focus {
|
||||
let mut paste_event = false;
|
||||
let _ = instance.with_core_mut(|core| {
|
||||
let key_code = web_to_ruffle_key_code(&js_event.code());
|
||||
let key_char = web_key_to_codepoint(&js_event.key());
|
||||
|
@ -848,13 +854,23 @@ impl Ruffle {
|
|||
is_ctrl_cmd,
|
||||
js_event.shift_key(),
|
||||
) {
|
||||
core.handle_event(PlayerEvent::TextControl { code: control_code });
|
||||
paste_event = control_code == TextControlCode::Paste;
|
||||
// The JS paste event fires separately and the clipboard text is not available until then,
|
||||
// so we need to wait before handling it
|
||||
if !paste_event {
|
||||
core.handle_event(PlayerEvent::TextControl {
|
||||
code: control_code,
|
||||
});
|
||||
}
|
||||
} else if let Some(codepoint) = key_char {
|
||||
core.handle_event(PlayerEvent::TextInput { codepoint });
|
||||
}
|
||||
});
|
||||
|
||||
js_event.prevent_default();
|
||||
// Don't prevent the JS paste event from firing
|
||||
if !paste_event {
|
||||
js_event.prevent_default();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -867,6 +883,31 @@ impl Ruffle {
|
|||
.warn_on_error();
|
||||
instance.key_down_callback = Some(key_down_callback);
|
||||
|
||||
let paste_callback = Closure::new(move |js_event: ClipboardEvent| {
|
||||
let _ = ruffle.with_instance(|instance| {
|
||||
if instance.has_focus {
|
||||
let _ = instance.with_core_mut(|core| {
|
||||
let clipboard_content = if let Some(content) = js_event.clipboard_data()
|
||||
{
|
||||
content.get_data("text/plain").unwrap_or_default()
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
core.ui_mut().set_clipboard_content(clipboard_content);
|
||||
core.handle_event(PlayerEvent::TextControl {
|
||||
code: TextControlCode::Paste,
|
||||
});
|
||||
});
|
||||
js_event.prevent_default();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window
|
||||
.add_event_listener_with_callback("paste", paste_callback.as_ref().unchecked_ref())
|
||||
.warn_on_error();
|
||||
instance.paste_callback = Some(paste_callback);
|
||||
|
||||
// Create keyup event handler.
|
||||
let key_up_callback = Closure::new(move |js_event: KeyboardEvent| {
|
||||
let _ = ruffle.with_instance(|instance| {
|
||||
|
@ -1258,6 +1299,14 @@ impl Drop for RuffleInstance {
|
|||
)
|
||||
.warn_on_error();
|
||||
}
|
||||
if let Some(paste_callback) = self.paste_callback.take() {
|
||||
self.window
|
||||
.remove_event_listener_with_callback(
|
||||
"paste",
|
||||
paste_callback.as_ref().unchecked_ref(),
|
||||
)
|
||||
.warn_on_error();
|
||||
}
|
||||
if let Some(key_up_callback) = self.key_up_callback.take() {
|
||||
self.window
|
||||
.remove_event_listener_with_callback(
|
||||
|
|
|
@ -14,6 +14,7 @@ pub struct WebUiBackend {
|
|||
cursor_visible: bool,
|
||||
cursor: MouseCursor,
|
||||
language: LanguageIdentifier,
|
||||
clipboard_content: String,
|
||||
}
|
||||
|
||||
impl WebUiBackend {
|
||||
|
@ -29,6 +30,7 @@ impl WebUiBackend {
|
|||
cursor_visible: true,
|
||||
cursor: MouseCursor::Arrow,
|
||||
language,
|
||||
clipboard_content: "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,11 +68,13 @@ impl UiBackend for WebUiBackend {
|
|||
}
|
||||
|
||||
fn clipboard_content(&mut self) -> String {
|
||||
tracing::warn!("get clipboard not implemented");
|
||||
"".to_string()
|
||||
// On web, clipboard content is not directly accessible due to security restrictions,
|
||||
// but pasting from the clipboard is supported via the JS `paste` event
|
||||
self.clipboard_content.to_owned()
|
||||
}
|
||||
|
||||
fn set_clipboard_content(&mut self, content: String) {
|
||||
self.clipboard_content = content.to_owned();
|
||||
// We use `document.execCommand("copy")` as `navigator.clipboard.writeText("string")`
|
||||
// is available only in secure contexts (HTTPS).
|
||||
if let Some(element) = self.canvas.parent_element() {
|
||||
|
@ -86,6 +90,7 @@ impl UiBackend for WebUiBackend {
|
|||
.dyn_into()
|
||||
.expect("create_element(\"textarea\") didn't give us a textarea");
|
||||
|
||||
let editing_text = self.js_player.is_virtual_keyboard_focused();
|
||||
textarea.set_value(&content);
|
||||
let _ = element.append_child(&textarea);
|
||||
textarea.select();
|
||||
|
@ -102,6 +107,10 @@ impl UiBackend for WebUiBackend {
|
|||
}
|
||||
|
||||
let _ = element.remove_child(&textarea);
|
||||
if editing_text {
|
||||
// Return focus to the text area
|
||||
self.js_player.open_virtual_keyboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue