Skip to content

Commit 883971f

Browse files
committed
web: Implement pasting text using Clipboard API
Ruffle does not have direct clipboard access on web, so the current paste implementation from the context menu does not work. This patch intercepts the paste context menu callback, and uses the Clipboard API to ask the browser for the clipboard and update it before calling the callback. When the Clipboard API is not available, a modal informing the user about cut, copy, paste shortcuts is displayed.
1 parent 9370c64 commit 883971f

File tree

2 files changed

+70
-13
lines changed

2 files changed

+70
-13
lines changed

web/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ features = [
7474
"EventTarget", "GainNode", "Headers", "HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement",
7575
"HtmlInputElement", "HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent",
7676
"Request", "RequestInit", "Response", "Storage", "WheelEvent", "Window", "ReadableStream", "RequestCredentials",
77-
"Url",
77+
"Url", "Clipboard",
7878
]
7979

8080
[package.metadata.cargo-machete]

web/src/lib.rs

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use external_interface::{external_to_js_value, js_to_external_value};
1717
use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control};
1818
use js_sys::{Error as JsError, Uint8Array};
1919
use ruffle_core::context::UpdateContext;
20+
use ruffle_core::context_menu::ContextMenuCallback;
2021
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
2122
use ruffle_core::tag_utils::SwfMovie;
2223
use ruffle_core::{Player, PlayerEvent, StaticCallstack, ViewportDimensions};
@@ -215,7 +216,7 @@ impl RuffleHandle {
215216
///
216217
/// `parameters` are *extra* parameters to set on the LoaderInfo -
217218
/// parameters from `movie_url` query parameters will be automatically added.
218-
pub fn stream_from(&mut self, movie_url: String, parameters: JsValue) -> Result<(), JsValue> {
219+
pub fn stream_from(&self, movie_url: String, parameters: JsValue) -> Result<(), JsValue> {
219220
let _ = self.with_core_mut(|core| {
220221
let parameters_to_load = parse_movie_parameters(&parameters);
221222

@@ -233,7 +234,7 @@ impl RuffleHandle {
233234
///
234235
/// This method should only be called once per player.
235236
pub fn load_data(
236-
&mut self,
237+
&self,
237238
swf_data: Uint8Array,
238239
parameters: JsValue,
239240
swf_name: String,
@@ -269,27 +270,27 @@ impl RuffleHandle {
269270
Ok(())
270271
}
271272

272-
pub fn play(&mut self) {
273+
pub fn play(&self) {
273274
let _ = self.with_core_mut(|core| {
274275
core.set_is_playing(true);
275276
});
276277
}
277278

278-
pub fn pause(&mut self) {
279+
pub fn pause(&self) {
279280
let _ = self.with_core_mut(|core| {
280281
core.set_is_playing(false);
281282
});
282283
}
283284

284-
pub fn is_playing(&mut self) -> bool {
285+
pub fn is_playing(&self) -> bool {
285286
self.with_core(|core| core.is_playing()).unwrap_or_default()
286287
}
287288

288289
pub fn volume(&self) -> f32 {
289290
self.with_core(|core| core.volume()).unwrap_or_default()
290291
}
291292

292-
pub fn set_volume(&mut self, value: f32) {
293+
pub fn set_volume(&self, value: f32) {
293294
let _ = self.with_core_mut(|core| core.set_volume(value));
294295
}
295296

@@ -304,27 +305,83 @@ impl RuffleHandle {
304305
}
305306

306307
// after the context menu is closed, remember to call `clear_custom_menu_items`!
307-
pub fn prepare_context_menu(&mut self) -> JsValue {
308+
pub fn prepare_context_menu(&self) -> JsValue {
308309
self.with_core_mut(|core| {
309310
let info = core.prepare_context_menu();
310311
serde_wasm_bindgen::to_value(&info).unwrap_or(JsValue::UNDEFINED)
311312
})
312313
.unwrap_or(JsValue::UNDEFINED)
313314
}
314315

315-
pub fn run_context_menu_callback(&mut self, index: usize) {
316-
let _ = self.with_core_mut(|core| core.run_context_menu_callback(index));
316+
pub async fn run_context_menu_callback(&self, index: usize) {
317+
let is_paste = self
318+
.with_core_mut(|core| {
319+
let is_paste = core.mutate_with_update_context(|context| {
320+
matches!(
321+
context
322+
.current_context_menu
323+
.as_ref()
324+
.map(|menu| menu.callback(index)),
325+
Some(ContextMenuCallback::TextControl {
326+
code: TextControlCode::Paste,
327+
..
328+
})
329+
)
330+
});
331+
if !is_paste {
332+
core.run_context_menu_callback(index)
333+
}
334+
is_paste
335+
})
336+
.unwrap_or_default();
337+
338+
// When the user selects paste, we need to use the Clipboard API which
339+
// requests the clipboard asynchronously, so that the browser can ask for permission.
340+
if is_paste {
341+
self.run_context_menu_callback_paste(index).await;
342+
}
343+
}
344+
345+
async fn run_context_menu_callback_paste(&self, index: usize) {
346+
let window = web_sys::window().expect("Missing window");
347+
let Some(clipboard) = window.navigator().clipboard() else {
348+
// Clipboard not available, display a message
349+
let _ = self.with_instance(|instance| instance.js_player.display_clipboard_modal());
350+
return;
351+
};
352+
353+
let promise = clipboard.read_text();
354+
tracing::debug!("Requested text from clipboard");
355+
let clipboard = wasm_bindgen_futures::JsFuture::from(promise)
356+
.await
357+
.ok()
358+
.and_then(|v| v.as_string());
359+
let Some(clipboard) = clipboard else {
360+
tracing::warn!("Clipboard permission denied");
361+
return;
362+
};
363+
364+
if !clipboard.is_empty() {
365+
let _ = self.with_core_mut(|core| {
366+
core.mutate_with_update_context(|context| {
367+
context.ui.set_clipboard_content(clipboard);
368+
});
369+
core.run_context_menu_callback(index);
370+
});
371+
} else {
372+
tracing::info!("Clipboard was empty");
373+
}
317374
}
318375

319-
pub fn set_fullscreen(&mut self, is_fullscreen: bool) {
376+
pub fn set_fullscreen(&self, is_fullscreen: bool) {
320377
let _ = self.with_core_mut(|core| core.set_fullscreen(is_fullscreen));
321378
}
322379

323-
pub fn clear_custom_menu_items(&mut self) {
380+
pub fn clear_custom_menu_items(&self) {
324381
let _ = self.with_core_mut(Player::clear_custom_menu_items);
325382
}
326383

327-
pub fn destroy(&mut self) {
384+
pub fn destroy(&self) {
328385
// Remove instance from the active list.
329386
let _ = self.remove_instance();
330387
// Instance is dropped at this point.

0 commit comments

Comments
 (0)