@@ -17,6 +17,7 @@ use external_interface::{external_to_js_value, js_to_external_value};
1717use input:: { web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control} ;
1818use js_sys:: { Error as JsError , Uint8Array } ;
1919use ruffle_core:: context:: UpdateContext ;
20+ use ruffle_core:: context_menu:: ContextMenuCallback ;
2021use ruffle_core:: events:: { MouseButton , MouseWheelDelta , TextControlCode } ;
2122use ruffle_core:: tag_utils:: SwfMovie ;
2223use 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