From 6cf8b660d5674185ace6ce266736241de881fa50 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Mon, 26 Apr 2021 22:17:47 -0700 Subject: [PATCH] web: Remove unwraps from Rust glue * Remove all unwraps from web/lib.rs. * Add convenience methods for grabbing the Ruffle web instance. These methods also avoid panics/unwraps when borrowing `RefCell`/`Mutex`. * Use `warn_on_error` to avoid unwraps from web APIs. --- Cargo.lock | 1 + web/Cargo.toml | 1 + web/src/lib.rs | 1190 +++++++++++++++++++++++++----------------------- 3 files changed, 615 insertions(+), 577 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 272bfdef8..9371ff56d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3037,6 +3037,7 @@ dependencies = [ "ruffle_render_webgl", "ruffle_web_common", "serde", + "thiserror", "url", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/web/Cargo.toml b/web/Cargo.toml index 98ec0e307..a450b915f 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -37,6 +37,7 @@ wasm-bindgen-futures = "0.4.19" chrono = { version = "0.4", features = ["wasmbind"] } getrandom = { version = "0.2", features = ["js"] } serde = { version = "1.0.125", features = ["derive"] } +thiserror = "1.0" [dependencies.ruffle_core] path = "../core" diff --git a/web/src/lib.rs b/web/src/lib.rs index 9bc280a64..8a3ee9cc1 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -29,7 +29,7 @@ use ruffle_core::external::{ }; use ruffle_core::property_map::PropertyMap; use ruffle_core::tag_utils::SwfMovie; -use ruffle_core::{Color, PlayerEvent}; +use ruffle_core::{Color, Player, PlayerEvent}; use ruffle_web_common::JsResult; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -40,7 +40,7 @@ use std::{cell::RefCell, error::Error, num::NonZeroI32}; use wasm_bindgen::{prelude::*, JsCast, JsValue}; use web_sys::{ AddEventListenerOptions, Element, Event, EventTarget, HtmlCanvasElement, HtmlElement, - KeyboardEvent, PointerEvent, WheelEvent, + KeyboardEvent, PointerEvent, WheelEvent, Window, }; static RUFFLE_GLOBAL_PANIC: Once = Once::new(); @@ -57,12 +57,13 @@ thread_local! { type AnimationHandler = Closure; struct RuffleInstance { - core: Arc>, + core: Arc>, js_player: JavascriptPlayer, canvas: HtmlCanvasElement, canvas_width: i32, canvas_height: i32, device_pixel_ratio: f64, + window: Window, timestamp: Option, animation_handler: Option, // requestAnimationFrame callback animation_handler_id: Option, // requestAnimationFrame id @@ -176,7 +177,7 @@ struct MovieMetadata { /// /// This type is exported to JS, and is used to interact with the library. #[wasm_bindgen] -#[derive(Clone)] +#[derive(Clone, Copy)] pub struct Ruffle(Index); #[wasm_bindgen] @@ -203,24 +204,18 @@ impl Ruffle { /// /// This method should only be called once per player. pub fn stream_from(&mut self, movie_url: &str, parameters: &JsValue) -> Result<(), JsValue> { - let ruffle = self.clone(); - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(ruffle.0).unwrap().borrow(); + let _ = self.with_core_mut(|core| { let mut parameters_to_load = PropertyMap::new(); populate_movie_parameters(¶meters, &mut parameters_to_load); + let ruffle = *self; let on_metadata = move |swf_header: &ruffle_core::swf::Header| { ruffle.on_metadata(swf_header); }; - instance.core.lock().unwrap().fetch_root_movie( - movie_url, - parameters_to_load, - Box::new(on_metadata), - ); - Ok(()) - }) + core.fetch_root_movie(movie_url, parameters_to_load, Box::new(on_metadata)); + }); + Ok(()) } /// Play an arbitrary movie on this instance. @@ -238,197 +233,156 @@ impl Ruffle { self.on_metadata(movie.header()); - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap(); - instance.borrow().core.lock().unwrap().set_root_movie(movie); + let _ = self.with_core_mut(move |core| { + core.set_root_movie(movie); }); Ok(()) } pub fn play(&mut self) { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap(); - instance.borrow().core.lock().unwrap().set_is_playing(true); + let _ = self.with_core_mut(|core| { + core.set_is_playing(true); }); } pub fn pause(&mut self) { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap(); - instance.borrow().core.lock().unwrap().set_is_playing(false); + let _ = self.with_core_mut(|core| { + core.set_is_playing(false); }); } pub fn is_playing(&mut self) -> bool { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap(); - let is_playing = instance.borrow().core.lock().unwrap().is_playing(); - is_playing - }) + self.with_core(|core| core.is_playing()).unwrap_or_default() } // after the context menu is closed, remember to call `clear_custom_menu_items`! pub fn prepare_context_menu(&mut self) -> JsValue { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap(); - let info = instance - .borrow() - .core - .lock() - .unwrap() - .prepare_context_menu(); - - JsValue::from_serde(&info).unwrap() + self.with_core_mut(|core| { + let info = core.prepare_context_menu(); + JsValue::from_serde(&info).unwrap_or(JsValue::UNDEFINED) }) + .unwrap_or(JsValue::UNDEFINED) } pub fn run_context_menu_callback(&mut self, index: usize) { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap(); - instance - .borrow() - .core - .lock() - .unwrap() - .run_context_menu_callback(index); - }) + let _ = self.with_core_mut(|core| core.run_context_menu_callback(index)); } pub fn clear_custom_menu_items(&mut self) { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap(); - instance - .borrow() - .core - .lock() - .unwrap() - .clear_custom_menu_items(); - }) + let _ = self.with_core_mut(Player::clear_custom_menu_items); } pub fn destroy(&mut self) { // Remove instance from the active list. - if let Some(instance) = INSTANCES.with(|instances| { - if let Ok(mut instances) = instances.try_borrow_mut() { - instances.remove(self.0) - } else { - // If we're being destroyed mid-panic, we won't mind not being able to remove this. - None - } - }) { - let mut instance = instance.borrow_mut(); + if let Ok(mut instance) = self.remove_instance() { instance.canvas.remove(); // Stop all audio playing from the instance. - let mut player = instance.core.lock().unwrap(); - player.audio_mut().stop_all_sounds(); - player.flush_shared_objects(); - drop(player); + let _ = instance.with_core_mut(|core| { + core.audio_mut().stop_all_sounds(); + core.flush_shared_objects(); + }); // Clean up all event listeners. - if let Some(window) = web_sys::window() { - if let Some(mouse_move_callback) = &instance.mouse_move_callback { - let canvas_events: &EventTarget = instance.canvas.as_ref(); - canvas_events - .remove_event_listener_with_callback( - "pointermove", - mouse_move_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.mouse_move_callback = None; - } - if let Some(mouse_down_callback) = &instance.mouse_down_callback { - let canvas_events: &EventTarget = instance.canvas.as_ref(); - canvas_events - .remove_event_listener_with_callback( - "pointerdown", - mouse_down_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.mouse_down_callback = None; - } - if let Some(player_mouse_down_callback) = &instance.player_mouse_down_callback { - let js_player_events: &EventTarget = instance.js_player.as_ref(); - js_player_events - .remove_event_listener_with_callback( - "pointerdown", - player_mouse_down_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.player_mouse_down_callback = None; - } - if let Some(window_mouse_down_callback) = &instance.window_mouse_down_callback { - window - .remove_event_listener_with_callback_and_bool( - "pointerdown", - window_mouse_down_callback.as_ref().unchecked_ref(), - true, - ) - .unwrap(); - instance.window_mouse_down_callback = None; - } - if let Some(mouse_up_callback) = &instance.mouse_up_callback { - let canvas_events: &EventTarget = instance.canvas.as_ref(); - canvas_events - .remove_event_listener_with_callback( - "pointerup", - mouse_up_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.mouse_up_callback = None; - } - if let Some(mouse_wheel_callback) = &instance.mouse_wheel_callback { - let canvas_events: &EventTarget = instance.canvas.as_ref(); - canvas_events - .remove_event_listener_with_callback( - "wheel", - mouse_wheel_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.mouse_wheel_callback = None; - } - if let Some(key_down_callback) = &instance.key_down_callback { - window - .remove_event_listener_with_callback( - "keydown", - key_down_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.key_down_callback = None; - } - if let Some(key_up_callback) = &instance.key_up_callback { - window - .remove_event_listener_with_callback( - "keyup", - key_up_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.key_up_callback = None; - } - if let Some(unload_callback) = &instance.unload_callback { - window - .remove_event_listener_with_callback( - "unload", - unload_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - instance.unload_callback = None; - } + if let Some(mouse_move_callback) = &instance.mouse_move_callback { + let canvas_events: &EventTarget = instance.canvas.as_ref(); + canvas_events + .remove_event_listener_with_callback( + "pointermove", + mouse_move_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.mouse_move_callback = None; + } + if let Some(mouse_down_callback) = &instance.mouse_down_callback { + let canvas_events: &EventTarget = instance.canvas.as_ref(); + canvas_events + .remove_event_listener_with_callback( + "pointerdown", + mouse_down_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.mouse_down_callback = None; + } + if let Some(player_mouse_down_callback) = &instance.player_mouse_down_callback { + let js_player_events: &EventTarget = instance.js_player.as_ref(); + js_player_events + .remove_event_listener_with_callback( + "pointerdown", + player_mouse_down_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.player_mouse_down_callback = None; + } + if let Some(window_mouse_down_callback) = &instance.window_mouse_down_callback { + instance + .window + .remove_event_listener_with_callback_and_bool( + "pointerdown", + window_mouse_down_callback.as_ref().unchecked_ref(), + true, + ) + .warn_on_error(); + instance.window_mouse_down_callback = None; + } + if let Some(mouse_up_callback) = &instance.mouse_up_callback { + let canvas_events: &EventTarget = instance.canvas.as_ref(); + canvas_events + .remove_event_listener_with_callback( + "pointerup", + mouse_up_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.mouse_up_callback = None; + } + if let Some(mouse_wheel_callback) = &instance.mouse_wheel_callback { + let canvas_events: &EventTarget = instance.canvas.as_ref(); + canvas_events + .remove_event_listener_with_callback( + "wheel", + mouse_wheel_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.mouse_wheel_callback = None; + } + if let Some(key_down_callback) = &instance.key_down_callback { + instance + .window + .remove_event_listener_with_callback( + "keydown", + key_down_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.key_down_callback = None; + } + if let Some(key_up_callback) = &instance.key_up_callback { + instance + .window + .remove_event_listener_with_callback( + "keyup", + key_up_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.key_up_callback = None; + } + if let Some(unload_callback) = &instance.unload_callback { + instance + .window + .remove_event_listener_with_callback( + "unload", + unload_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.unload_callback = None; } // Cancel the animation handler, if it's still active. if let Some(id) = instance.animation_handler_id { - if let Some(window) = web_sys::window() { - let _ = window.cancel_animation_frame(id.into()); - } + instance + .window + .cancel_animation_frame(id.into()) + .warn_on_error(); } } @@ -451,44 +405,25 @@ impl Ruffle { } } - INSTANCES.with(move |instances| { - if let Ok(instances) = instances.try_borrow() { - if let Some(instance) = instances.get(self.0) { - if let Ok(mut player) = instance.borrow().core.try_lock() { - return external_to_js_value(player.call_internal_interface(name, args)); - } - } - } - JsValue::NULL - }) + self.with_core_mut(|core| external_to_js_value(core.call_internal_interface(name, args))) + .unwrap_or(JsValue::NULL) } pub fn set_trace_observer(&self, observer: JsValue) { - INSTANCES.with(move |instances| { - if let Ok(instances) = instances.try_borrow() { - if let Some(instance) = instances.get(self.0) { - *instance.borrow_mut().trace_observer.borrow_mut() = observer; - } - } - }) + let _ = self.with_instance(|instance| { + *instance.trace_observer.borrow_mut() = observer; + }); } /// Returns the web AudioContext used by this player. /// Returns `None` if the audio backend does not use Web Audio. pub fn audio_context(&self) -> Option { - INSTANCES.with(move |instances| { - if let Ok(instances) = instances.try_borrow() { - if let Some(instance) = instances.get(self.0) { - let instance = instance.borrow_mut(); - let player = instance.core.lock().unwrap(); - return player - .audio() - .downcast_ref::() - .map(|audio| audio.audio_context().clone()); - } - } - None + self.with_core_mut(|core| { + core.audio() + .downcast_ref::() + .map(|audio| audio.audio_context().clone()) }) + .unwrap_or_default() } } @@ -532,16 +467,22 @@ impl Ruffle { let video = Box::new(SoftwareVideoBackend::new()); let log = Box::new(log_adapter::WebLogBackend::new(trace_observer.clone())); let ui = Box::new(ui::WebUiBackend::new(js_player.clone(), &canvas)); + let core = ruffle_core::Player::new(renderer, audio, navigator, storage, locale, video, log, ui)?; - { - let mut core = core.lock().unwrap(); + if let Ok(mut core) = core.try_lock() { + // Set config parameters. if let Some(color) = config.background_color.and_then(parse_html_color) { core.set_background_color(Some(color)); } core.set_letterbox(config.letterbox); core.set_warn_on_unsupported_content(config.warn_on_unsupported_content); core.set_max_execution_duration(config.max_execution_duration); + + // Create the external interface. + if allow_script_access { + core.add_external_interface(Box::new(JavascriptInterface::new(js_player.clone()))); + } } // Create instance. @@ -552,6 +493,7 @@ impl Ruffle { canvas_width: 0, // Initialize canvas width and height to 0 to force an initial canvas resize. canvas_height: 0, device_pixel_ratio: window.device_pixel_ratio(), + window: window.clone(), animation_handler: None, animation_handler_id: None, mouse_move_callback: None, @@ -569,334 +511,263 @@ impl Ruffle { }; // Prevent touch-scrolling on canvas. - canvas.style().set_property("touch-action", "none").unwrap(); + canvas + .style() + .set_property("touch-action", "none") + .warn_on_error(); // Register the instance and create the animation frame closure. - let mut ruffle = INSTANCES.with(move |instances| { - let index = instances.borrow_mut().insert(RefCell::new(instance)); - let instances = instances.borrow(); - let ruffle = Ruffle(index); + let mut ruffle = Ruffle::add_instance(instance)?; - // Create the external interface - if allow_script_access { - let instance = instances.get(index).unwrap(); - let player = instance.borrow().js_player.clone(); - instance - .borrow() - .core - .lock() - .unwrap() - .add_external_interface(Box::new(JavascriptInterface::new(player))); - } - - // Create the animation frame closure. - { - let mut ruffle = ruffle.clone(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().animation_handler = - Some(Closure::wrap(Box::new(move |timestamp: f64| { - ruffle.tick(timestamp); - }) as Box)); - } + // Create the animation frame closure. + ruffle.with_instance_mut(|instance| { + instance.animation_handler = Some(Closure::wrap(Box::new(move |timestamp: f64| { + ruffle.tick(timestamp); + }) as Box)); // Create mouse move handler. - { - let mouse_move_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| { - INSTANCES.with(move |instances| { - let instances = instances.borrow(); - if let Some(instance) = instances.get(index) { - let instance = instance.borrow(); - let event = PlayerEvent::MouseMove { - x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio, - y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio, - }; - instance.core.lock().unwrap().handle_event(event); - if instance.has_focus { - js_event.prevent_default(); - } - } + let mouse_move_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| { + let _ = ruffle.with_instance(move |instance| { + let event = PlayerEvent::MouseMove { + x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio, + y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio, + }; + let _ = instance.with_core_mut(|core| { + core.handle_event(event); }); - }) - as Box); + if instance.has_focus { + js_event.prevent_default(); + } + }); + }) as Box); - let canvas_events: &EventTarget = canvas.as_ref(); - canvas_events - .add_event_listener_with_callback( - "pointermove", - mouse_move_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().mouse_move_callback = Some(mouse_move_callback); - } + let canvas_events: &EventTarget = canvas.as_ref(); + canvas_events + .add_event_listener_with_callback( + "pointermove", + mouse_move_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + + instance.mouse_move_callback = Some(mouse_move_callback); // Create mouse down handler. - { - let mouse_down_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| { - INSTANCES.with(move |instances| { - let instances = instances.borrow(); - if let Some(instance) = instances.get(index) { - // Only fire player mouse event for left clicks. - if js_event.button() == 0 { - if let Some(target) = js_event.current_target() { - let _ = target - .unchecked_ref::() - .set_pointer_capture(js_event.pointer_id()); - } - let device_pixel_ratio = instance.borrow().device_pixel_ratio; - let event = PlayerEvent::MouseDown { - x: f64::from(js_event.offset_x()) * device_pixel_ratio, - y: f64::from(js_event.offset_y()) * device_pixel_ratio, - }; - instance.borrow().core.lock().unwrap().handle_event(event); - } + let mouse_down_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| { + let _ = ruffle.with_instance(move |instance| { + // Only fire player mouse event for left clicks. + if js_event.button() == 0 { + if let Some(target) = js_event.current_target() { + let _ = target + .unchecked_ref::() + .set_pointer_capture(js_event.pointer_id()); + } + let device_pixel_ratio = instance.device_pixel_ratio; + let event = PlayerEvent::MouseDown { + x: f64::from(js_event.offset_x()) * device_pixel_ratio, + y: f64::from(js_event.offset_y()) * device_pixel_ratio, + }; + let _ = instance.with_core_mut(|core| { + core.handle_event(event); + }); + } + js_event.prevent_default(); + }); + }) as Box); + + let canvas_events: &EventTarget = canvas.as_ref(); + canvas_events + .add_event_listener_with_callback( + "pointerdown", + mouse_down_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.mouse_down_callback = Some(mouse_down_callback); + + // Create player mouse down handler. + let player_mouse_down_callback = + Closure::wrap(Box::new(move |_js_event: PointerEvent| { + let _ = ruffle.with_instance_mut(|instance| { + instance.has_focus = true; + // Ensure the parent window gets focus. This is necessary for events + // to be received when the player is inside a frame. + instance.window.focus().warn_on_error(); + }); + }) as Box); + + let js_player_events: &EventTarget = js_player.as_ref(); + js_player_events + .add_event_listener_with_callback( + "pointerdown", + player_mouse_down_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.player_mouse_down_callback = Some(player_mouse_down_callback); + + // Create window mouse down handler. + let window_mouse_down_callback = + Closure::wrap(Box::new(move |_js_event: PointerEvent| { + let _ = ruffle.with_instance_mut(|instance| { + // If we actually clicked on the player, this will be reset to true + // after the event bubbles down to the player. + instance.has_focus = false; + }); + }) as Box); + + window + .add_event_listener_with_callback_and_bool( + "pointerdown", + window_mouse_down_callback.as_ref().unchecked_ref(), + true, // Use capture so this first *before* the player mouse down handler. + ) + .warn_on_error(); + instance.window_mouse_down_callback = Some(window_mouse_down_callback); + + // Create mouse up handler. + let mouse_up_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| { + let _ = ruffle.with_instance_mut(|instance| { + // Only fire player mouse event for left clicks. + if js_event.button() == 0 { + if let Some(target) = js_event.current_target() { + let _ = target + .unchecked_ref::() + .release_pointer_capture(js_event.pointer_id()); + } + let event = PlayerEvent::MouseUp { + x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio, + y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio, + }; + let _ = instance.with_core_mut(|core| { + core.handle_event(event); + }); + } + + if instance.has_focus { + js_event.prevent_default(); + } + }); + }) as Box); + + let canvas_events: &EventTarget = canvas.as_ref(); + canvas_events + .add_event_listener_with_callback( + "pointerup", + mouse_up_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.mouse_up_callback = Some(mouse_up_callback); + + // Create mouse wheel handler. + let mouse_wheel_callback = Closure::wrap(Box::new(move |js_event: WheelEvent| { + let _ = ruffle.with_instance(|instance| { + let delta = match js_event.delta_mode() { + WheelEvent::DOM_DELTA_LINE => MouseWheelDelta::Lines(-js_event.delta_y()), + WheelEvent::DOM_DELTA_PIXEL => MouseWheelDelta::Pixels(-js_event.delta_y()), + _ => return, + }; + let _ = instance.with_core_mut(|core| { + core.handle_event(PlayerEvent::MouseWheel { delta }); + if core.should_prevent_scrolling() { js_event.prevent_default(); } }); - }) - as Box); + }); + }) as Box); - let canvas_events: &EventTarget = canvas.as_ref(); - canvas_events - .add_event_listener_with_callback( - "pointerdown", - mouse_down_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().mouse_down_callback = Some(mouse_down_callback); - } - - // Create player mouse down handler. - { - let window = window.clone(); - let player_mouse_down_callback = - Closure::wrap(Box::new(move |_js_event: PointerEvent| { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - if let Some(instance) = instances.get(index) { - instance.borrow_mut().has_focus = true; - // Ensure the parent window gets focus. This is necessary for events - // to be received when the player is inside a frame. - let _ = window.focus(); - } - }); - }) as Box); - - let js_player_events: &EventTarget = js_player.as_ref(); - js_player_events - .add_event_listener_with_callback( - "pointerdown", - player_mouse_down_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().player_mouse_down_callback = Some(player_mouse_down_callback); - } - - // Create window mouse down handler. - { - let window_mouse_down_callback = - Closure::wrap(Box::new(move |_js_event: PointerEvent| { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - if let Some(instance) = instances.get(index) { - // If we actually clicked on the player, this will be reset to true - // after the event bubbles down to the player. - instance.borrow_mut().has_focus = false; - } - }); - }) as Box); - - window - .add_event_listener_with_callback_and_bool( - "pointerdown", - window_mouse_down_callback.as_ref().unchecked_ref(), - true, // Use capture so this first *before* the player mouse down handler. - ) - .unwrap(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().window_mouse_down_callback = Some(window_mouse_down_callback); - } - - // Create mouse up handler. - { - let mouse_up_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| { - INSTANCES.with(move |instances| { - let instances = instances.borrow(); - if let Some(instance) = instances.get(index) { - let instance = instance.borrow(); - - // Only fire player mouse event for left clicks. - if js_event.button() == 0 { - if let Some(target) = js_event.current_target() { - let _ = target - .unchecked_ref::() - .release_pointer_capture(js_event.pointer_id()); - } - let event = PlayerEvent::MouseUp { - x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio, - y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio, - }; - instance.core.lock().unwrap().handle_event(event); - } - - if instance.has_focus { - js_event.prevent_default(); - } - } - }); - }) - as Box); - - let canvas_events: &EventTarget = canvas.as_ref(); - canvas_events - .add_event_listener_with_callback( - "pointerup", - mouse_up_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().mouse_up_callback = Some(mouse_up_callback); - } - - // Create mouse wheel handler. - { - let mouse_wheel_callback = Closure::wrap(Box::new(move |js_event: WheelEvent| { - INSTANCES.with(move |instances| { - let instances = instances.borrow(); - if let Some(instance) = instances.get(index) { - let delta = match js_event.delta_mode() { - WheelEvent::DOM_DELTA_LINE => { - MouseWheelDelta::Lines(-js_event.delta_y()) - } - WheelEvent::DOM_DELTA_PIXEL => { - MouseWheelDelta::Pixels(-js_event.delta_y()) - } - _ => return, - }; - let core = &instance.borrow().core; - let mut core_lock = core.lock().unwrap(); - core_lock.handle_event(PlayerEvent::MouseWheel { delta }); - if core_lock.should_prevent_scrolling() { - js_event.prevent_default(); - } - } - }); - }) - as Box); - - let canvas_events: &EventTarget = canvas.as_ref(); - let mut options = AddEventListenerOptions::new(); - options.passive(false); - canvas_events - .add_event_listener_with_callback_and_add_event_listener_options( - "wheel", - mouse_wheel_callback.as_ref().unchecked_ref(), - &options, - ) - .unwrap(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().mouse_wheel_callback = Some(mouse_wheel_callback); - } + let canvas_events: &EventTarget = canvas.as_ref(); + let mut options = AddEventListenerOptions::new(); + options.passive(false); + canvas_events + .add_event_listener_with_callback_and_add_event_listener_options( + "wheel", + mouse_wheel_callback.as_ref().unchecked_ref(), + &options, + ) + .warn_on_error(); + instance.mouse_wheel_callback = Some(mouse_wheel_callback); // Create keydown event handler. - { - let key_down_callback = Closure::wrap(Box::new(move |js_event: KeyboardEvent| { - INSTANCES.with(|instances| { - if let Some(instance) = instances.borrow().get(index) { - let instance = instance.borrow(); - if instance.has_focus { - let mut core = instance.core.lock().unwrap(); - let ui = core.ui_mut().downcast_mut::().unwrap(); - ui.keydown(&js_event); + let key_down_callback = Closure::wrap(Box::new(move |js_event: KeyboardEvent| { + let _ = ruffle.with_instance(|instance| { + if instance.has_focus { + let _ = instance.with_core_mut(|core| { + let ui = if let Some(ui) = + core.ui_mut().downcast_mut::() + { + ui + } else { + return; + }; + ui.keydown(&js_event); - let key_code = ui.last_key_code(); - let key_char = ui.last_key_char(); + let key_code = ui.last_key_code(); + let key_char = ui.last_key_char(); - if key_code != KeyCode::Unknown { - core.handle_event(PlayerEvent::KeyDown { key_code }); - } - - if let Some(codepoint) = key_char { - core.handle_event(PlayerEvent::TextInput { codepoint }); - } - - js_event.prevent_default(); + if key_code != KeyCode::Unknown { + core.handle_event(PlayerEvent::KeyDown { key_code }); } - } - }); - }) - as Box); - window - .add_event_listener_with_callback( - "keydown", - key_down_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - let instance = instances.get(index).unwrap(); - instance.borrow_mut().key_down_callback = Some(key_down_callback); - } + if let Some(codepoint) = key_char { + core.handle_event(PlayerEvent::TextInput { codepoint }); + } + }); + + js_event.prevent_default(); + } + }); + }) as Box); + + window + .add_event_listener_with_callback( + "keydown", + key_down_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.key_down_callback = Some(key_down_callback); // Create keyup event handler. - { - let key_up_callback = Closure::wrap(Box::new(move |js_event: KeyboardEvent| { - js_event.prevent_default(); - INSTANCES.with(|instances| { - if let Some(instance) = instances.borrow().get(index) { - let instance = instance.borrow(); - if instance.has_focus { - let mut core = instance.core.lock().unwrap(); - let ui = core.ui_mut().downcast_mut::().unwrap(); - ui.keyup(&js_event); + let key_up_callback = Closure::wrap(Box::new(move |js_event: KeyboardEvent| { + let _ = ruffle.with_instance(|instance| { + if instance.has_focus { + let _ = instance.with_core_mut(|core| { + let ui = if let Some(ui) = + core.ui_mut().downcast_mut::() + { + ui + } else { + return; + }; + ui.keyup(&js_event); - let key_code = ui.last_key_code(); - if key_code != KeyCode::Unknown { - core.handle_event(PlayerEvent::KeyUp { key_code }); - } - - js_event.prevent_default(); + let key_code = ui.last_key_code(); + if key_code != KeyCode::Unknown { + core.handle_event(PlayerEvent::KeyUp { key_code }); } - } - }); - }) - as Box); + }); + js_event.prevent_default(); + } + }); + }) as Box); - window - .add_event_listener_with_callback( - "keyup", - key_up_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - let mut instance = instances.get(index).unwrap().borrow_mut(); - instance.key_up_callback = Some(key_up_callback); - } + window + .add_event_listener_with_callback("keyup", key_up_callback.as_ref().unchecked_ref()) + .warn_on_error(); + instance.key_up_callback = Some(key_up_callback); - { - let unload_callback = Closure::wrap(Box::new(move |_| { - INSTANCES.with(|instances| { - if let Some(instance) = instances.borrow().get(index) { - let instance = instance.borrow(); - let mut player = instance.core.lock().unwrap(); - player.flush_shared_objects(); - } - }); - }) as Box); + let unload_callback = Closure::wrap(Box::new(move |_| { + let _ = ruffle.with_core_mut(|core| { + core.flush_shared_objects(); + }); + }) as Box); - window - .add_event_listener_with_callback( - "unload", - unload_callback.as_ref().unchecked_ref(), - ) - .unwrap(); - let mut instance = instances.get(index).unwrap().borrow_mut(); - instance.unload_callback = Some(unload_callback); - } - - ruffle - }); + window + .add_event_listener_with_callback( + "unload", + unload_callback.as_ref().unchecked_ref(), + ) + .warn_on_error(); + instance.unload_callback = Some(unload_callback); + })?; // Set initial timestamp and do initial tick to start animation loop. ruffle.tick(0.0); @@ -904,93 +775,211 @@ impl Ruffle { Ok(ruffle) } + /// Registers a new Ruffle instance and returns the handle to the instance. + fn add_instance(instance: RuffleInstance) -> Result { + INSTANCES.try_with(|instances| { + let mut instances = instances.try_borrow_mut()?; + let ruffle = Ruffle(instances.insert(RefCell::new(instance))); + Ok(ruffle) + })? + } + + /// Unregisters a Ruffle instance, and returns the removed instance. + fn remove_instance(&self) -> Result { + INSTANCES.try_with(|instances| { + let mut instances = instances.try_borrow_mut()?; + if let Some(instance) = instances.remove(self.0) { + Ok(instance.into_inner()) + } else { + Err(RuffleInstanceError::InstanceNotFound) + } + })? + } + + /// Runs the given function on this Ruffle instance. + fn with_instance(&self, f: F) -> Result + where + F: FnOnce(&RuffleInstance) -> O, + { + let ret = INSTANCES + .try_with(|instances| { + let instances = instances.try_borrow()?; + if let Some(instance) = instances.get(self.0) { + let instance = instance.try_borrow()?; + Ok(f(&*instance)) + } else { + Err(RuffleInstanceError::InstanceNotFound) + } + }) + .map_err(RuffleInstanceError::from) + .and_then(std::convert::identity); + if let Err(e) = &ret { + log::error!("{}", e); + } + ret + } + + /// Runs the given function on this Ruffle instance. + fn with_instance_mut(&self, f: F) -> Result + where + F: FnOnce(&mut RuffleInstance) -> O, + { + let ret = INSTANCES + .try_with(|instances| { + let instances = instances.try_borrow()?; + if let Some(instance) = instances.get(self.0) { + let mut instance = instance.try_borrow_mut()?; + Ok(f(&mut *instance)) + } else { + Err(RuffleInstanceError::InstanceNotFound) + } + }) + .map_err(RuffleInstanceError::from) + .and_then(std::convert::identity); + if let Err(e) = &ret { + log::error!("{}", e); + } + ret + } + + /// Runs the given function on this instance's `Player`. + fn with_core(&self, f: F) -> Result + where + F: FnOnce(&ruffle_core::Player) -> O, + { + let ret = INSTANCES + .try_with(|instances| { + let instances = instances.try_borrow()?; + if let Some(instance) = instances.get(self.0) { + let instance = instance.try_borrow()?; + // This clone lets us drop the instance borrow to avoid potential double-borrows. + let core = instance.core.clone(); + drop(instance); + let core = core + .try_lock() + .map_err(|_| RuffleInstanceError::TryLockError)?; + Ok(f(&*core)) + } else { + Err(RuffleInstanceError::InstanceNotFound) + } + }) + .map_err(RuffleInstanceError::from) + .and_then(std::convert::identity); + if let Err(e) = &ret { + log::error!("{}", e); + } + ret + } + + /// Runs the given function on this instance's `Player`. + fn with_core_mut(&self, f: F) -> Result + where + F: FnOnce(&mut ruffle_core::Player) -> O, + { + let ret = INSTANCES + .try_with(|instances| { + let instances = instances.try_borrow()?; + if let Some(instance) = instances.get(self.0) { + let instance = instance.try_borrow()?; + // This clone lets us drop the instance to avoid potential double-borrows. + let core = instance.core.clone(); + drop(instance); + let mut core = core + .try_lock() + .map_err(|_| RuffleInstanceError::TryLockError)?; + Ok(f(&mut *core)) + } else { + Err(RuffleInstanceError::InstanceNotFound) + } + }) + .map_err(RuffleInstanceError::from) + .and_then(std::convert::identity); + if let Err(e) = &ret { + log::error!("{}", e); + } + ret + } + fn tick(&mut self, timestamp: f64) { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - if let Some(instance) = instances.get(self.0) { - let window = web_sys::window().unwrap(); + let mut dt = 0.0; + let mut new_dimensions = None; + let _ = self.with_instance_mut(|instance| { + // Check for canvas resize. + let canvas_width = instance.canvas.client_width(); + let canvas_height = instance.canvas.client_height(); + let device_pixel_ratio = instance.window.device_pixel_ratio(); // Changes via user zooming. + if instance.canvas_width != canvas_width + || instance.canvas_height != canvas_height + || (instance.device_pixel_ratio - device_pixel_ratio).abs() >= f64::EPSILON + { + // If a canvas resizes, its drawing context will get scaled. You must reset + // the width and height attributes of the canvas element to recreate the context. + // (NOT the CSS width/height!) + instance.canvas_width = canvas_width; + instance.canvas_height = canvas_height; + instance.device_pixel_ratio = device_pixel_ratio; - let mut mut_instance = instance.borrow_mut(); - // Calculate the dt from last tick. - let dt = if let Some(prev_timestamp) = mut_instance.timestamp { - mut_instance.timestamp = Some(timestamp); - timestamp - prev_timestamp - } else { - // Store the timestamp from the initial tick. - // (I tried to use Performance.now() to get the initial timestamp, - // but this didn't seem to be accurate and caused negative dts on - // Chrome.) - mut_instance.timestamp = Some(timestamp); - 0.0 - }; - drop(mut_instance); + // The actual viewport is scaled by DPI, bigger than CSS pixels. + let viewport_width = (f64::from(canvas_width) * device_pixel_ratio) as u32; + let viewport_height = (f64::from(canvas_height) * device_pixel_ratio) as u32; - let core = instance.borrow().core.clone(); - let mut core_lock = core.lock().unwrap(); - core_lock.tick(dt); - let mut needs_render = core_lock.needs_render(); + new_dimensions = Some(( + instance.canvas.clone(), + viewport_width, + viewport_height, + device_pixel_ratio, + )); + } - // Check for canvas resize. - let canvas = instance.borrow().canvas.to_owned(); - let canvas_width = canvas.client_width(); - let canvas_height = canvas.client_height(); - let device_pixel_ratio = window.device_pixel_ratio(); // Changes via user zooming. - if instance.borrow().canvas_width != canvas_width - || instance.borrow().canvas_height != canvas_height - || (instance.borrow().device_pixel_ratio - device_pixel_ratio).abs() - >= f64::EPSILON - { - let mut mut_instance = instance.borrow_mut(); - // If a canvas resizes, its drawing context will get scaled. You must reset - // the width and height attributes of the canvas element to recreate the context. - // (NOT the CSS width/height!) - mut_instance.canvas_width = canvas_width; - mut_instance.canvas_height = canvas_height; - mut_instance.device_pixel_ratio = device_pixel_ratio; - drop(mut_instance); + // Request next animation frame. + if let Some(handler) = &instance.animation_handler { + let id = instance + .window + .request_animation_frame(handler.as_ref().unchecked_ref()) + .unwrap_or_default(); + instance.animation_handler_id = NonZeroI32::new(id); + } else { + instance.animation_handler_id = None; + } - // The actual viewport is scaled by DPI, bigger than CSS pixels. - let viewport_width = (f64::from(canvas_width) * device_pixel_ratio) as u32; - let viewport_height = (f64::from(canvas_height) * device_pixel_ratio) as u32; - canvas.set_width(viewport_width); - canvas.set_height(viewport_height); + // Calculate the dt from last tick. + dt = if let Some(prev_timestamp) = instance.timestamp { + instance.timestamp = Some(timestamp); + timestamp - prev_timestamp + } else { + // Store the timestamp from the initial tick. + // (I tried to use Performance.now() to get the initial timestamp, + // but this didn't seem to be accurate and caused negative dts on + // Chrome.) + instance.timestamp = Some(timestamp); + 0.0 + }; + }); - core_lock.set_viewport_dimensions( - viewport_width, - viewport_height, - device_pixel_ratio, - ); - core_lock - .renderer_mut() - .set_viewport_dimensions(viewport_width, viewport_height); + // Tick the Ruffle core. + let _ = self.with_core_mut(|core| { + if let Some((ref canvas, viewport_width, viewport_height, device_pixel_ratio)) = + new_dimensions + { + canvas.set_width(viewport_width); + canvas.set_height(viewport_height); - // Force a re-render if we resize. - needs_render = true; - } + core.set_viewport_dimensions(viewport_width, viewport_height, device_pixel_ratio); + core.renderer_mut() + .set_viewport_dimensions(viewport_width, viewport_height); + } - if needs_render { - core_lock.render(); - } + core.tick(dt); - // Request next animation frame. - let mut instance = instance.borrow_mut(); - if let Some(handler) = &instance.animation_handler { - let window = web_sys::window().unwrap(); - let id = window - .request_animation_frame(handler.as_ref().unchecked_ref()) - .unwrap(); - instance.animation_handler_id = NonZeroI32::new(id); - } else { - instance.animation_handler_id = None; - } + // Render if the core signals a new frame, or if we resized. + if core.needs_render() || new_dimensions.is_some() { + core.render(); } }); } fn on_metadata(&self, swf_header: &ruffle_core::swf::Header) { - INSTANCES.with(|instances| { - let instances = instances.borrow(); - let instance = instances.get(self.0).unwrap().borrow(); + let _ = self.with_instance(|instance| { let width = swf_header.stage_size.x_max - swf_header.stage_size.x_min; let height = swf_header.stage_size.y_max - swf_header.stage_size.y_min; let metadata = MovieMetadata { @@ -1001,13 +990,60 @@ impl Ruffle { swf_version: swf_header.version, }; - instance - .js_player - .set_metadata(JsValue::from_serde(&metadata).unwrap()); - }) + if let Ok(value) = JsValue::from_serde(&metadata) { + instance.js_player.set_metadata(value); + } + }); } } +impl RuffleInstance { + #[allow(dead_code)] + fn with_core(&self, f: F) -> Result + where + F: FnOnce(&ruffle_core::Player) -> O, + { + let ret = self + .core + .try_lock() + .map(|core| f(&*core)) + .map_err(|_| RuffleInstanceError::TryLockError); + if let Err(e) = &ret { + log::error!("{}", e); + } + ret + } + + fn with_core_mut(&self, f: F) -> Result + where + F: FnOnce(&mut ruffle_core::Player) -> O, + { + let ret = self + .core + .try_lock() + .map(|mut core| f(&mut *core)) + .map_err(|_| RuffleInstanceError::TryLockError); + if let Err(e) = &ret { + log::error!("{}", e); + } + ret + } +} + +#[derive(thiserror::Error, Debug)] +pub enum RuffleInstanceError { + #[error("Unable to access INSTANCES threadlocal")] + ThreadLocalAccessError(#[from] std::thread::AccessError), + #[error("Unable to mutably borrow Ruffle instance")] + CannotBorrow(#[from] std::cell::BorrowError), + #[error("Unable to borrow Ruffle instance")] + CannotBorrowMut(#[from] std::cell::BorrowMutError), + #[error("Unable to lock Ruffle core")] + TryLockError, + #[error("Ruffle Instance ID does not exist")] + InstanceNotFound, +} + struct JavascriptMethod { this: JsValue, function: JsValue,