2020-08-13 06:51:12 +00:00
|
|
|
#![allow(clippy::same_item_push, clippy::unknown_clippy_lints)]
|
2020-08-13 02:04:14 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
//! Ruffle web frontend.
|
2019-05-03 02:11:47 +00:00
|
|
|
mod audio;
|
2019-12-19 01:20:49 +00:00
|
|
|
mod input;
|
2020-09-02 20:05:29 +00:00
|
|
|
mod locale;
|
2020-09-05 19:43:14 +00:00
|
|
|
mod log_adapter;
|
2019-09-01 19:24:04 +00:00
|
|
|
mod navigator;
|
2020-06-15 20:55:29 +00:00
|
|
|
mod storage;
|
2019-05-03 02:11:47 +00:00
|
|
|
|
2020-09-05 19:43:14 +00:00
|
|
|
use crate::log_adapter::WebLogBackend;
|
2020-06-15 20:55:29 +00:00
|
|
|
use crate::storage::LocalStorageBackend;
|
2020-09-02 20:05:29 +00:00
|
|
|
use crate::{
|
|
|
|
audio::WebAudioBackend, input::WebInputBackend, locale::WebLocaleBackend,
|
|
|
|
navigator::WebNavigatorBackend,
|
|
|
|
};
|
2019-05-10 16:06:47 +00:00
|
|
|
use generational_arena::{Arena, Index};
|
2020-09-02 19:02:32 +00:00
|
|
|
use js_sys::{Array, Function, Object, Uint8Array};
|
2020-11-21 05:56:15 +00:00
|
|
|
use ruffle_core::backend::input::InputBackend;
|
2020-05-02 09:59:13 +00:00
|
|
|
use ruffle_core::backend::render::RenderBackend;
|
2020-06-15 16:53:19 +00:00
|
|
|
use ruffle_core::backend::storage::MemoryStorageBackend;
|
2020-06-15 23:59:06 +00:00
|
|
|
use ruffle_core::backend::storage::StorageBackend;
|
2020-09-02 19:02:32 +00:00
|
|
|
use ruffle_core::context::UpdateContext;
|
2020-11-21 05:56:15 +00:00
|
|
|
use ruffle_core::events::{KeyCode, MouseWheelDelta};
|
2020-09-02 19:02:32 +00:00
|
|
|
use ruffle_core::external::{
|
|
|
|
ExternalInterfaceMethod, ExternalInterfaceProvider, Value as ExternalValue, Value,
|
|
|
|
};
|
2020-10-11 18:35:28 +00:00
|
|
|
use ruffle_core::property_map::PropertyMap;
|
2020-05-04 19:37:06 +00:00
|
|
|
use ruffle_core::tag_utils::SwfMovie;
|
2019-11-08 20:09:57 +00:00
|
|
|
use ruffle_core::PlayerEvent;
|
2020-05-14 08:12:24 +00:00
|
|
|
use ruffle_web_common::JsResult;
|
2020-09-02 19:02:32 +00:00
|
|
|
use std::collections::BTreeMap;
|
2020-09-09 22:15:02 +00:00
|
|
|
use std::sync::Once;
|
2019-11-08 20:09:57 +00:00
|
|
|
use std::sync::{Arc, Mutex};
|
2019-05-17 02:14:23 +00:00
|
|
|
use std::{cell::RefCell, error::Error, num::NonZeroI32};
|
2019-08-09 21:50:20 +00:00
|
|
|
use wasm_bindgen::{prelude::*, JsCast, JsValue};
|
2020-08-22 00:04:02 +00:00
|
|
|
use web_sys::{
|
|
|
|
AddEventListenerOptions, Element, EventTarget, HtmlCanvasElement, HtmlElement, KeyboardEvent,
|
|
|
|
PointerEvent, WheelEvent,
|
|
|
|
};
|
2019-04-28 01:15:43 +00:00
|
|
|
|
2020-09-09 22:15:02 +00:00
|
|
|
static RUFFLE_GLOBAL_PANIC: Once = Once::new();
|
|
|
|
|
2019-05-03 00:17:02 +00:00
|
|
|
thread_local! {
|
2019-05-24 17:25:03 +00:00
|
|
|
/// We store the actual instances of the ruffle core in a static pool.
|
|
|
|
/// This gives us a clear boundary between the JS side and Rust side, avoiding
|
2020-09-19 14:27:24 +00:00
|
|
|
/// issues with lifetimes and type parameters (which cannot be exported with wasm-bindgen).
|
2020-09-11 20:45:15 +00:00
|
|
|
static INSTANCES: RefCell<Arena<RefCell<RuffleInstance>>> = RefCell::new(Arena::new());
|
2020-09-02 19:51:39 +00:00
|
|
|
|
|
|
|
static CURRENT_CONTEXT: RefCell<Option<*mut UpdateContext<'static, 'static, 'static>>> = RefCell::new(None);
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
|
2019-08-15 20:48:51 +00:00
|
|
|
type AnimationHandler = Closure<dyn FnMut(f64)>;
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
struct RuffleInstance {
|
2019-11-08 20:09:57 +00:00
|
|
|
core: Arc<Mutex<ruffle_core::Player>>,
|
2020-09-02 19:02:32 +00:00
|
|
|
js_player: JavascriptPlayer,
|
2019-08-20 01:40:55 +00:00
|
|
|
canvas: HtmlCanvasElement,
|
|
|
|
canvas_width: i32,
|
|
|
|
canvas_height: i32,
|
2019-08-20 05:23:02 +00:00
|
|
|
device_pixel_ratio: f64,
|
2019-08-22 20:28:06 +00:00
|
|
|
timestamp: Option<f64>,
|
2019-05-17 02:14:23 +00:00
|
|
|
animation_handler: Option<AnimationHandler>, // requestAnimationFrame callback
|
|
|
|
animation_handler_id: Option<NonZeroI32>, // requestAnimationFrame id
|
2019-08-09 21:50:20 +00:00
|
|
|
#[allow(dead_code)]
|
2019-12-23 20:15:06 +00:00
|
|
|
mouse_move_callback: Option<Closure<dyn FnMut(PointerEvent)>>,
|
|
|
|
mouse_down_callback: Option<Closure<dyn FnMut(PointerEvent)>>,
|
|
|
|
mouse_up_callback: Option<Closure<dyn FnMut(PointerEvent)>>,
|
2020-11-14 10:15:59 +00:00
|
|
|
player_mouse_down_callback: Option<Closure<dyn FnMut(PointerEvent)>>,
|
2019-12-23 20:15:06 +00:00
|
|
|
window_mouse_down_callback: Option<Closure<dyn FnMut(PointerEvent)>>,
|
2020-08-22 00:04:02 +00:00
|
|
|
mouse_wheel_callback: Option<Closure<dyn FnMut(WheelEvent)>>,
|
2019-12-22 02:42:55 +00:00
|
|
|
key_down_callback: Option<Closure<dyn FnMut(KeyboardEvent)>>,
|
|
|
|
key_up_callback: Option<Closure<dyn FnMut(KeyboardEvent)>>,
|
|
|
|
has_focus: bool,
|
2020-09-05 19:43:14 +00:00
|
|
|
trace_observer: Arc<RefCell<JsValue>>,
|
2019-05-03 00:17:02 +00:00
|
|
|
}
|
|
|
|
|
2020-09-12 22:03:19 +00:00
|
|
|
#[wasm_bindgen]
|
|
|
|
extern "C" {
|
|
|
|
#[wasm_bindgen(js_name = Error)]
|
|
|
|
type JsError;
|
|
|
|
|
|
|
|
#[wasm_bindgen(constructor, js_class = "Error")]
|
|
|
|
fn new(message: &str) -> JsError;
|
|
|
|
}
|
|
|
|
|
2020-11-12 22:32:53 +00:00
|
|
|
#[wasm_bindgen(module = "/packages/core/src/ruffle-player.ts")]
|
2020-09-02 19:02:32 +00:00
|
|
|
extern "C" {
|
2020-11-14 10:15:59 +00:00
|
|
|
#[wasm_bindgen(extends = EventTarget)]
|
2020-09-02 19:02:32 +00:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub type JavascriptPlayer;
|
|
|
|
|
2020-11-16 21:50:06 +00:00
|
|
|
#[wasm_bindgen(method, js_name = "onCallbackAvailable")]
|
2020-09-02 19:02:32 +00:00
|
|
|
fn on_callback_available(this: &JavascriptPlayer, name: &str);
|
2020-09-09 22:15:02 +00:00
|
|
|
|
|
|
|
#[wasm_bindgen(method)]
|
2020-09-12 22:03:19 +00:00
|
|
|
fn panic(this: &JavascriptPlayer, error: &JsError);
|
2020-09-02 19:02:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct JavascriptInterface {
|
|
|
|
js_player: JavascriptPlayer,
|
|
|
|
}
|
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
/// An opaque handle to a `RuffleInstance` inside the pool.
|
|
|
|
///
|
|
|
|
/// This type is exported to JS, and is used to interact with the library.
|
2019-04-28 01:15:43 +00:00
|
|
|
#[wasm_bindgen]
|
2019-05-17 02:14:23 +00:00
|
|
|
#[derive(Clone)]
|
2019-05-24 17:25:03 +00:00
|
|
|
pub struct Ruffle(Index);
|
2019-04-28 01:15:43 +00:00
|
|
|
|
|
|
|
#[wasm_bindgen]
|
2019-05-24 17:25:03 +00:00
|
|
|
impl Ruffle {
|
2020-11-12 22:32:53 +00:00
|
|
|
#[wasm_bindgen(constructor)]
|
2020-09-02 21:34:04 +00:00
|
|
|
pub fn new(
|
|
|
|
parent: HtmlElement,
|
|
|
|
js_player: JavascriptPlayer,
|
|
|
|
allow_script_access: bool,
|
2020-12-11 11:41:44 +00:00
|
|
|
upgrade_to_https: bool,
|
2020-09-02 21:34:04 +00:00
|
|
|
) -> Result<Ruffle, JsValue> {
|
2020-09-09 22:15:02 +00:00
|
|
|
if RUFFLE_GLOBAL_PANIC.is_completed() {
|
|
|
|
// If an actual panic happened, then we can't trust the state it left us in.
|
|
|
|
// Prevent future players from loading so that they can inform the user about the error.
|
|
|
|
return Err("Ruffle is panicking!".into());
|
|
|
|
}
|
|
|
|
set_panic_handler();
|
2020-12-11 11:41:44 +00:00
|
|
|
Ruffle::new_internal(parent, js_player, allow_script_access, upgrade_to_https)
|
2020-09-02 21:34:04 +00:00
|
|
|
.map_err(|_| "Error creating player".into())
|
2020-07-23 03:18:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Stream an arbitrary movie file from (presumably) the Internet.
|
|
|
|
///
|
|
|
|
/// This method should only be called once per player.
|
2020-10-11 18:35:28 +00:00
|
|
|
pub fn stream_from(&mut self, movie_url: &str, parameters: &JsValue) -> Result<(), JsValue> {
|
2020-07-23 03:18:30 +00:00
|
|
|
INSTANCES.with(|instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
let instances = instances.borrow();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(self.0).unwrap().borrow();
|
2020-10-11 18:35:28 +00:00
|
|
|
let mut parameters_to_load = PropertyMap::new();
|
2020-10-12 21:27:53 +00:00
|
|
|
populate_movie_parameters(¶meters, &mut parameters_to_load);
|
2020-10-11 18:35:28 +00:00
|
|
|
instance
|
|
|
|
.core
|
|
|
|
.lock()
|
|
|
|
.unwrap()
|
|
|
|
.fetch_root_movie(movie_url, parameters_to_load);
|
|
|
|
Ok(())
|
|
|
|
})
|
2020-07-23 03:18:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Play an arbitrary movie on this instance.
|
|
|
|
///
|
|
|
|
/// This method should only be called once per player.
|
2020-10-11 18:35:28 +00:00
|
|
|
pub fn load_data(&mut self, swf_data: Uint8Array, parameters: &JsValue) -> Result<(), JsValue> {
|
2020-07-23 03:18:30 +00:00
|
|
|
let movie = Arc::new({
|
|
|
|
let mut data = vec![0; swf_data.length() as usize];
|
|
|
|
swf_data.copy_to(&mut data[..]);
|
2020-10-11 18:35:28 +00:00
|
|
|
let mut movie = SwfMovie::from_data(&data, None)
|
|
|
|
.map_err(|e| format!("Error loading movie: {}", e))?;
|
2020-10-12 21:27:53 +00:00
|
|
|
populate_movie_parameters(¶meters, movie.parameters_mut());
|
2020-10-11 18:35:28 +00:00
|
|
|
movie
|
2020-07-23 03:18:30 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
INSTANCES.with(|instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
let instance = instances.get(self.0).unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
instance.borrow().core.lock().unwrap().set_root_movie(movie);
|
2020-07-23 03:18:30 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
Ok(())
|
2019-04-28 01:15:43 +00:00
|
|
|
}
|
|
|
|
|
2020-04-27 10:34:47 +00:00
|
|
|
pub fn play(&mut self) {
|
|
|
|
// Remove instance from the active list.
|
|
|
|
INSTANCES.with(|instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
let instance = instances.get(self.0).unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
instance.borrow().core.lock().unwrap().set_is_playing(true);
|
2020-04-27 10:34:47 +00:00
|
|
|
log::info!("PLAY!");
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-18 22:52:35 +00:00
|
|
|
pub fn pause(&mut self) {
|
|
|
|
// Remove instance from the active list.
|
|
|
|
INSTANCES.with(|instances| {
|
|
|
|
let instances = instances.borrow();
|
|
|
|
let instance = instances.get(self.0).unwrap();
|
|
|
|
instance.borrow().core.lock().unwrap().set_is_playing(false);
|
|
|
|
log::info!("PAUSE!");
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-12 19:10:46 +00:00
|
|
|
pub fn destroy(&mut self) {
|
2019-05-17 02:14:23 +00:00
|
|
|
// Remove instance from the active list.
|
2020-09-11 20:45:15 +00:00
|
|
|
if let Some(instance) = INSTANCES.with(|instances| {
|
2020-09-12 22:03:19 +00:00
|
|
|
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
|
|
|
|
}
|
2019-05-17 02:14:23 +00:00
|
|
|
}) {
|
2020-09-11 20:45:15 +00:00
|
|
|
let mut instance = instance.borrow_mut();
|
2020-05-14 08:12:24 +00:00
|
|
|
instance.canvas.remove();
|
|
|
|
|
2020-04-13 11:57:10 +00:00
|
|
|
// Stop all audio playing from the instance
|
2020-09-11 20:45:15 +00:00
|
|
|
instance.core.lock().unwrap().audio_mut().stop_all_sounds();
|
2020-04-13 11:57:10 +00:00
|
|
|
|
2020-05-14 08:12:24 +00:00
|
|
|
// Clean up all event listeners.
|
|
|
|
instance.key_down_callback = None;
|
|
|
|
instance.key_up_callback = None;
|
|
|
|
instance.mouse_down_callback = None;
|
|
|
|
instance.mouse_move_callback = None;
|
|
|
|
instance.mouse_up_callback = None;
|
2020-11-14 10:15:59 +00:00
|
|
|
instance.player_mouse_down_callback = None;
|
2020-05-14 08:12:24 +00:00
|
|
|
instance.window_mouse_down_callback = None;
|
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Cancel the animation handler, if it's still active.
|
2019-05-24 17:25:03 +00:00
|
|
|
if let Some(id) = instance.animation_handler_id {
|
2019-05-17 02:14:23 +00:00
|
|
|
if let Some(window) = web_sys::window() {
|
2020-09-12 19:10:46 +00:00
|
|
|
let _ = window.cancel_animation_frame(id.into());
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
2020-06-15 23:59:06 +00:00
|
|
|
}
|
|
|
|
}
|
2019-05-06 10:51:09 +00:00
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Player is dropped at this point.
|
2019-04-28 01:15:43 +00:00
|
|
|
}
|
2020-09-02 19:02:32 +00:00
|
|
|
|
|
|
|
#[allow(clippy::boxed_local)] // for js_bind
|
|
|
|
pub fn call_exposed_callback(&self, name: &str, args: Box<[JsValue]>) -> JsValue {
|
|
|
|
let args: Vec<ExternalValue> = args.iter().map(js_to_external_value).collect();
|
2020-09-02 19:51:39 +00:00
|
|
|
|
|
|
|
// Re-entrant callbacks need to return through the hole that was punched through for them
|
|
|
|
// We record the context of external functions, and then if we get an internal callback
|
|
|
|
// during the same call we'll reuse that.
|
|
|
|
// This is unsafe by nature. I don't know any safe way to do this.
|
|
|
|
if let Some(context) = CURRENT_CONTEXT.with(|v| *v.borrow()) {
|
|
|
|
unsafe {
|
|
|
|
if let Some(callback) = (*context).external_interface.get_callback(name) {
|
|
|
|
return external_to_js_value(callback.call(&mut *context, name, args));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-02 19:02:32 +00:00
|
|
|
INSTANCES.with(move |instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
if let Ok(instances) = instances.try_borrow() {
|
|
|
|
if let Some(instance) = instances.get(self.0) {
|
2020-09-11 20:45:15 +00:00
|
|
|
if let Ok(mut player) = instance.borrow().core.try_lock() {
|
2020-09-02 19:02:32 +00:00
|
|
|
return external_to_js_value(player.call_internal_interface(name, args));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
JsValue::NULL
|
|
|
|
})
|
|
|
|
}
|
2020-09-05 19:43:14 +00:00
|
|
|
|
|
|
|
pub fn set_trace_observer(&self, observer: JsValue) {
|
|
|
|
INSTANCES.with(move |instances| {
|
2020-09-11 20:45:15 +00:00
|
|
|
if let Ok(instances) = instances.try_borrow() {
|
|
|
|
if let Some(instance) = instances.get(self.0) {
|
|
|
|
*instance.borrow_mut().trace_observer.borrow_mut() = observer;
|
2020-09-05 19:43:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2020-10-19 00:11:25 +00:00
|
|
|
|
|
|
|
/// 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<web_sys::AudioContext> {
|
|
|
|
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::<WebAudioBackend>()
|
|
|
|
.map(|audio| audio.audio_context().clone());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
})
|
|
|
|
}
|
2019-04-28 01:15:43 +00:00
|
|
|
}
|
2020-06-15 23:54:00 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
impl Ruffle {
|
2020-09-02 19:02:32 +00:00
|
|
|
fn new_internal(
|
|
|
|
parent: HtmlElement,
|
|
|
|
js_player: JavascriptPlayer,
|
2020-09-02 21:34:04 +00:00
|
|
|
allow_script_access: bool,
|
2020-12-11 11:41:44 +00:00
|
|
|
upgrade_to_https: bool,
|
2020-09-02 19:02:32 +00:00
|
|
|
) -> Result<Ruffle, Box<dyn Error>> {
|
2019-05-06 10:51:09 +00:00
|
|
|
let _ = console_log::init_with_level(log::Level::Trace);
|
2019-04-30 08:53:21 +00:00
|
|
|
|
2020-09-07 19:18:25 +00:00
|
|
|
let window = web_sys::window().ok_or("Expected window")?;
|
2020-05-14 08:12:24 +00:00
|
|
|
let document = window.document().ok_or("Expected document")?;
|
|
|
|
|
|
|
|
let (canvas, renderer) = create_renderer(&document)?;
|
|
|
|
parent
|
|
|
|
.append_child(&canvas.clone().into())
|
|
|
|
.into_js_result()?;
|
|
|
|
|
2019-11-08 20:09:57 +00:00
|
|
|
let audio = Box::new(WebAudioBackend::new()?);
|
2020-12-11 11:41:44 +00:00
|
|
|
let navigator = Box::new(WebNavigatorBackend::new(upgrade_to_https));
|
2019-11-08 20:09:57 +00:00
|
|
|
let input = Box::new(WebInputBackend::new(&canvas));
|
2020-09-02 20:05:29 +00:00
|
|
|
let locale = Box::new(WebLocaleBackend::new());
|
2019-11-08 20:09:57 +00:00
|
|
|
|
2020-06-15 23:54:00 +00:00
|
|
|
let current_domain = window.location().href().unwrap();
|
|
|
|
|
2020-11-20 06:28:45 +00:00
|
|
|
let local_storage = match window.local_storage() {
|
|
|
|
Ok(Some(s)) => {
|
2020-06-15 23:59:06 +00:00
|
|
|
Box::new(LocalStorageBackend::new(s, current_domain)) as Box<dyn StorageBackend>
|
2020-11-20 06:28:45 +00:00
|
|
|
}
|
|
|
|
err => {
|
|
|
|
log::warn!("Unable to use localStorage: {:?}\nData will not save.", err);
|
|
|
|
Box::new(MemoryStorageBackend::default())
|
|
|
|
}
|
|
|
|
};
|
2020-06-15 20:55:29 +00:00
|
|
|
|
2020-09-05 19:43:14 +00:00
|
|
|
let trace_observer = Arc::new(RefCell::new(JsValue::UNDEFINED));
|
|
|
|
let log = Box::new(WebLogBackend::new(trace_observer.clone()));
|
2020-09-05 16:19:03 +00:00
|
|
|
|
|
|
|
let core = ruffle_core::Player::new(
|
|
|
|
renderer,
|
|
|
|
audio,
|
|
|
|
navigator,
|
|
|
|
input,
|
|
|
|
local_storage,
|
|
|
|
locale,
|
|
|
|
log,
|
|
|
|
)?;
|
2019-11-08 20:09:57 +00:00
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Create instance.
|
2019-05-24 17:25:03 +00:00
|
|
|
let instance = RuffleInstance {
|
2019-05-17 02:14:23 +00:00
|
|
|
core,
|
2020-11-14 10:15:59 +00:00
|
|
|
js_player: js_player.clone(),
|
2019-08-20 01:40:55 +00:00
|
|
|
canvas: canvas.clone(),
|
2020-09-19 14:27:24 +00:00
|
|
|
canvas_width: 0, // Initialize canvas width and height to 0 to force an initial canvas resize.
|
2019-08-20 01:40:55 +00:00
|
|
|
canvas_height: 0,
|
2019-08-20 05:23:02 +00:00
|
|
|
device_pixel_ratio: window.device_pixel_ratio(),
|
2019-05-17 02:14:23 +00:00
|
|
|
animation_handler: None,
|
|
|
|
animation_handler_id: None,
|
2019-08-20 01:17:29 +00:00
|
|
|
mouse_move_callback: None,
|
|
|
|
mouse_down_callback: None,
|
2020-11-14 10:15:59 +00:00
|
|
|
player_mouse_down_callback: None,
|
2019-12-22 02:42:55 +00:00
|
|
|
window_mouse_down_callback: None,
|
2019-08-20 01:17:29 +00:00
|
|
|
mouse_up_callback: None,
|
2020-08-22 00:04:02 +00:00
|
|
|
mouse_wheel_callback: None,
|
2019-12-22 02:42:55 +00:00
|
|
|
key_down_callback: None,
|
|
|
|
key_up_callback: None,
|
2019-08-22 20:28:06 +00:00
|
|
|
timestamp: None,
|
2019-12-22 02:42:55 +00:00
|
|
|
has_focus: false,
|
2020-09-05 19:43:14 +00:00
|
|
|
trace_observer,
|
2019-05-17 02:14:23 +00:00
|
|
|
};
|
|
|
|
|
2019-12-23 20:15:06 +00:00
|
|
|
// Prevent touch-scrolling on canvas.
|
|
|
|
canvas.style().set_property("touch-action", "none").unwrap();
|
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Register the instance and create the animation frame closure.
|
2019-05-24 17:25:03 +00:00
|
|
|
let mut ruffle = INSTANCES.with(move |instances| {
|
2020-09-11 20:45:15 +00:00
|
|
|
let index = instances.borrow_mut().insert(RefCell::new(instance));
|
|
|
|
let instances = instances.borrow();
|
2019-05-24 17:25:03 +00:00
|
|
|
let ruffle = Ruffle(index);
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2020-09-02 19:02:32 +00:00
|
|
|
// Create the external interface
|
2020-09-02 21:34:04 +00:00
|
|
|
if allow_script_access {
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
let player = instance.borrow().js_player.clone();
|
2020-09-02 19:02:32 +00:00
|
|
|
instance
|
2020-09-11 20:45:15 +00:00
|
|
|
.borrow()
|
2020-09-02 19:02:32 +00:00
|
|
|
.core
|
|
|
|
.lock()
|
|
|
|
.unwrap()
|
2020-09-11 20:45:15 +00:00
|
|
|
.add_external_interface(Box::new(JavascriptInterface::new(player)));
|
2020-09-02 19:02:32 +00:00
|
|
|
}
|
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Create the animation frame closure.
|
|
|
|
{
|
2019-05-24 17:25:03 +00:00
|
|
|
let mut ruffle = ruffle.clone();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
instance.borrow_mut().animation_handler =
|
|
|
|
Some(Closure::wrap(Box::new(move |timestamp: f64| {
|
|
|
|
ruffle.tick(timestamp);
|
|
|
|
}) as Box<dyn FnMut(f64)>));
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
// Create mouse move handler.
|
|
|
|
{
|
2019-12-23 20:15:06 +00:00
|
|
|
let mouse_move_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| {
|
2019-08-20 01:17:29 +00:00
|
|
|
INSTANCES.with(move |instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
if let Some(instance) = instances.get(index) {
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instance.borrow();
|
2019-08-20 01:17:29 +00:00
|
|
|
let event = PlayerEvent::MouseMove {
|
2019-08-20 05:23:02 +00:00
|
|
|
x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio,
|
|
|
|
y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio,
|
2019-08-20 01:17:29 +00:00
|
|
|
};
|
2019-11-08 20:09:57 +00:00
|
|
|
instance.core.lock().unwrap().handle_event(event);
|
2019-12-22 02:42:55 +00:00
|
|
|
if instance.has_focus {
|
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
2019-08-20 01:17:29 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
2019-12-23 20:15:06 +00:00
|
|
|
as Box<dyn FnMut(PointerEvent)>);
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
let canvas_events: &EventTarget = canvas.as_ref();
|
|
|
|
canvas_events
|
|
|
|
.add_event_listener_with_callback(
|
2019-12-23 20:15:06 +00:00
|
|
|
"pointermove",
|
2019-08-20 01:17:29 +00:00
|
|
|
mouse_move_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
instance.borrow_mut().mouse_move_callback = Some(mouse_move_callback);
|
2019-08-20 01:17:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create mouse down handler.
|
|
|
|
{
|
2019-12-23 20:15:06 +00:00
|
|
|
let mouse_down_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| {
|
2019-08-20 01:17:29 +00:00
|
|
|
INSTANCES.with(move |instances| {
|
2020-09-11 20:45:15 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
if let Some(instance) = instances.get(index) {
|
2020-10-23 01:59:43 +00:00
|
|
|
// 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::<Element>()
|
|
|
|
.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);
|
2019-12-23 20:15:06 +00:00
|
|
|
}
|
2020-10-23 01:59:43 +00:00
|
|
|
|
2019-12-22 02:42:55 +00:00
|
|
|
js_event.prevent_default();
|
2019-08-20 01:17:29 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
2019-12-23 20:15:06 +00:00
|
|
|
as Box<dyn FnMut(PointerEvent)>);
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
let canvas_events: &EventTarget = canvas.as_ref();
|
|
|
|
canvas_events
|
|
|
|
.add_event_listener_with_callback(
|
2019-12-23 20:15:06 +00:00
|
|
|
"pointerdown",
|
2019-08-20 01:17:29 +00:00
|
|
|
mouse_down_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
instance.borrow_mut().mouse_down_callback = Some(mouse_down_callback);
|
2019-08-20 01:17:29 +00:00
|
|
|
}
|
|
|
|
|
2020-11-14 10:15:59 +00:00
|
|
|
// Create player mouse down handler.
|
|
|
|
{
|
2020-12-11 01:36:01 +00:00
|
|
|
let window = window.clone();
|
2020-11-14 10:15:59 +00:00
|
|
|
let player_mouse_down_callback =
|
|
|
|
Closure::wrap(Box::new(move |_js_event: PointerEvent| {
|
2020-12-11 01:36:01 +00:00
|
|
|
INSTANCES.with(|instances| {
|
2020-11-14 10:15:59 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
if let Some(instance) = instances.get(index) {
|
|
|
|
instance.borrow_mut().has_focus = true;
|
2020-12-11 01:36:01 +00:00
|
|
|
// Ensure the parent window gets focus. This is necessary for events
|
|
|
|
// to be received when the player is inside a frame.
|
|
|
|
let _ = window.focus();
|
2020-11-14 10:15:59 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}) as Box<dyn FnMut(PointerEvent)>);
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-12-22 02:42:55 +00:00
|
|
|
// Create window mouse down handler.
|
|
|
|
{
|
|
|
|
let window_mouse_down_callback =
|
2019-12-23 20:15:06 +00:00
|
|
|
Closure::wrap(Box::new(move |_js_event: PointerEvent| {
|
2019-12-22 02:42:55 +00:00
|
|
|
INSTANCES.with(|instances| {
|
2020-09-11 20:45:15 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
if let Some(instance) = instances.get(index) {
|
2020-11-14 10:15:59 +00:00
|
|
|
// If we actually clicked on the player, this will be reset to true
|
|
|
|
// after the event bubbles down to the player.
|
2020-09-11 20:45:15 +00:00
|
|
|
instance.borrow_mut().has_focus = false;
|
2019-12-22 02:42:55 +00:00
|
|
|
}
|
|
|
|
});
|
2019-12-23 20:15:06 +00:00
|
|
|
}) as Box<dyn FnMut(PointerEvent)>);
|
2019-12-22 02:42:55 +00:00
|
|
|
|
|
|
|
window
|
|
|
|
.add_event_listener_with_callback_and_bool(
|
2019-12-23 20:15:06 +00:00
|
|
|
"pointerdown",
|
2019-12-22 02:42:55 +00:00
|
|
|
window_mouse_down_callback.as_ref().unchecked_ref(),
|
2020-11-14 10:15:59 +00:00
|
|
|
true, // Use capture so this first *before* the player mouse down handler.
|
2019-12-22 02:42:55 +00:00
|
|
|
)
|
|
|
|
.unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
instance.borrow_mut().window_mouse_down_callback = Some(window_mouse_down_callback);
|
2019-12-22 02:42:55 +00:00
|
|
|
}
|
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
// Create mouse up handler.
|
|
|
|
{
|
2019-12-23 20:15:06 +00:00
|
|
|
let mouse_up_callback = Closure::wrap(Box::new(move |js_event: PointerEvent| {
|
2019-08-20 01:17:29 +00:00
|
|
|
INSTANCES.with(move |instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
if let Some(instance) = instances.get(index) {
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instance.borrow();
|
2020-10-23 01:59:43 +00:00
|
|
|
|
|
|
|
// 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::<Element>()
|
|
|
|
.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);
|
2019-12-23 20:15:06 +00:00
|
|
|
}
|
2020-10-23 01:59:43 +00:00
|
|
|
|
2019-12-22 02:42:55 +00:00
|
|
|
if instance.has_focus {
|
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
2019-08-20 01:17:29 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
2019-12-23 20:15:06 +00:00
|
|
|
as Box<dyn FnMut(PointerEvent)>);
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
let canvas_events: &EventTarget = canvas.as_ref();
|
|
|
|
canvas_events
|
|
|
|
.add_event_listener_with_callback(
|
2019-12-23 20:15:06 +00:00
|
|
|
"pointerup",
|
2019-08-20 01:17:29 +00:00
|
|
|
mouse_up_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
instance.borrow_mut().mouse_up_callback = Some(mouse_up_callback);
|
2019-08-20 01:17:29 +00:00
|
|
|
}
|
|
|
|
|
2020-08-22 00:04:02 +00:00
|
|
|
// Create mouse wheel handler.
|
|
|
|
{
|
|
|
|
let mouse_wheel_callback = Closure::wrap(Box::new(move |js_event: WheelEvent| {
|
|
|
|
INSTANCES.with(move |instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
if let Some(instance) = instances.get(index) {
|
2020-08-22 00:04:02 +00:00
|
|
|
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,
|
|
|
|
};
|
2020-09-11 20:45:15 +00:00
|
|
|
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() {
|
2020-08-22 00:04:02 +00:00
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
|
|
|
as Box<dyn FnMut(WheelEvent)>);
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2020-08-22 00:04:02 +00:00
|
|
|
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();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
instance.borrow_mut().mouse_wheel_callback = Some(mouse_wheel_callback);
|
2020-08-22 00:04:02 +00:00
|
|
|
}
|
2019-08-09 21:50:20 +00:00
|
|
|
|
2019-12-19 01:20:49 +00:00
|
|
|
// Create keydown event handler.
|
|
|
|
{
|
2019-12-22 02:42:55 +00:00
|
|
|
let key_down_callback = Closure::wrap(Box::new(move |js_event: KeyboardEvent| {
|
|
|
|
INSTANCES.with(|instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
if let Some(instance) = instances.borrow().get(index) {
|
2020-11-21 05:56:15 +00:00
|
|
|
let instance = instance.borrow();
|
|
|
|
if instance.has_focus {
|
|
|
|
let mut core = instance.core.lock().unwrap();
|
|
|
|
let input =
|
|
|
|
core.input_mut().downcast_mut::<WebInputBackend>().unwrap();
|
|
|
|
input.keydown(&js_event);
|
|
|
|
|
|
|
|
let key_code = input.last_key_code();
|
|
|
|
let key_char = input.last_key_char();
|
|
|
|
|
|
|
|
if key_code != KeyCode::Unknown {
|
|
|
|
core.handle_event(PlayerEvent::KeyDown { key_code });
|
2019-12-24 10:41:35 +00:00
|
|
|
}
|
|
|
|
|
2020-11-21 05:56:15 +00:00
|
|
|
if let Some(codepoint) = key_char {
|
|
|
|
core.handle_event(PlayerEvent::TextInput { codepoint });
|
2019-12-24 10:41:35 +00:00
|
|
|
}
|
|
|
|
|
2019-12-22 02:42:55 +00:00
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
2019-12-19 01:20:49 +00:00
|
|
|
}
|
2019-12-22 02:42:55 +00:00
|
|
|
});
|
2019-12-19 01:20:49 +00:00
|
|
|
})
|
|
|
|
as Box<dyn FnMut(KeyboardEvent)>);
|
|
|
|
|
|
|
|
window
|
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"keydown",
|
2019-12-22 02:42:55 +00:00
|
|
|
key_down_callback.as_ref().unchecked_ref(),
|
2019-12-19 01:20:49 +00:00
|
|
|
)
|
|
|
|
.unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
let instance = instances.get(index).unwrap();
|
|
|
|
instance.borrow_mut().key_down_callback = Some(key_down_callback);
|
2019-12-22 02:42:55 +00:00
|
|
|
}
|
2019-12-19 01:20:49 +00:00
|
|
|
|
2019-12-22 02:42:55 +00:00
|
|
|
{
|
|
|
|
let key_up_callback = Closure::wrap(Box::new(move |js_event: KeyboardEvent| {
|
|
|
|
js_event.prevent_default();
|
|
|
|
INSTANCES.with(|instances| {
|
2020-09-11 18:42:35 +00:00
|
|
|
if let Some(instance) = instances.borrow().get(index) {
|
2020-11-21 05:56:15 +00:00
|
|
|
let instance = instance.borrow();
|
|
|
|
if instance.has_focus {
|
|
|
|
let mut core = instance.core.lock().unwrap();
|
|
|
|
let input =
|
|
|
|
core.input_mut().downcast_mut::<WebInputBackend>().unwrap();
|
|
|
|
input.keyup(&js_event);
|
|
|
|
|
|
|
|
let key_code = input.last_key_code();
|
|
|
|
if key_code != KeyCode::Unknown {
|
|
|
|
core.handle_event(PlayerEvent::KeyUp { key_code });
|
2020-02-26 11:03:36 +00:00
|
|
|
}
|
|
|
|
|
2019-12-22 02:42:55 +00:00
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
2019-12-19 01:20:49 +00:00
|
|
|
}
|
2019-12-22 02:42:55 +00:00
|
|
|
});
|
2019-12-19 01:20:49 +00:00
|
|
|
})
|
|
|
|
as Box<dyn FnMut(KeyboardEvent)>);
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2019-12-19 01:20:49 +00:00
|
|
|
window
|
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"keyup",
|
2019-12-22 02:42:55 +00:00
|
|
|
key_up_callback.as_ref().unchecked_ref(),
|
2019-12-19 01:20:49 +00:00
|
|
|
)
|
|
|
|
.unwrap();
|
2020-09-11 20:45:15 +00:00
|
|
|
let mut instance = instances.get(index).unwrap().borrow_mut();
|
2019-12-22 02:42:55 +00:00
|
|
|
instance.key_up_callback = Some(key_up_callback);
|
2019-12-19 01:20:49 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
ruffle
|
2019-05-03 00:17:02 +00:00
|
|
|
});
|
|
|
|
|
2019-08-22 20:28:06 +00:00
|
|
|
// Set initial timestamp and do initial tick to start animation loop.
|
|
|
|
ruffle.tick(0.0);
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
Ok(ruffle)
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn tick(&mut self, timestamp: f64) {
|
2019-05-24 17:25:03 +00:00
|
|
|
INSTANCES.with(|instances| {
|
2020-09-11 20:45:15 +00:00
|
|
|
let instances = instances.borrow();
|
|
|
|
if let Some(instance) = instances.get(self.0) {
|
2020-01-16 05:34:22 +00:00
|
|
|
let window = web_sys::window().unwrap();
|
|
|
|
|
2020-09-11 20:45:15 +00:00
|
|
|
let mut mut_instance = instance.borrow_mut();
|
2019-08-22 20:28:06 +00:00
|
|
|
// Calculate the dt from last tick.
|
2020-09-11 20:45:15 +00:00
|
|
|
let dt = if let Some(prev_timestamp) = mut_instance.timestamp {
|
|
|
|
mut_instance.timestamp = Some(timestamp);
|
2019-08-22 20:28:06 +00:00
|
|
|
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.)
|
2020-09-11 20:45:15 +00:00
|
|
|
mut_instance.timestamp = Some(timestamp);
|
2019-08-22 20:28:06 +00:00
|
|
|
0.0
|
|
|
|
};
|
2020-09-11 20:45:15 +00:00
|
|
|
drop(mut_instance);
|
2019-08-09 21:50:20 +00:00
|
|
|
|
2020-09-11 20:45:15 +00:00
|
|
|
let core = instance.borrow().core.clone();
|
|
|
|
let mut core_lock = core.lock().unwrap();
|
2020-05-02 11:25:21 +00:00
|
|
|
core_lock.tick(dt);
|
|
|
|
let mut needs_render = core_lock.needs_render();
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2019-08-20 01:40:55 +00:00
|
|
|
// Check for canvas resize.
|
2020-09-11 20:45:15 +00:00
|
|
|
let canvas = instance.borrow().canvas.to_owned();
|
|
|
|
let canvas_width = canvas.client_width();
|
|
|
|
let canvas_height = canvas.client_height();
|
2020-01-16 05:34:22 +00:00
|
|
|
let device_pixel_ratio = window.device_pixel_ratio(); // Changes via user zooming.
|
2020-09-11 20:45:15 +00:00
|
|
|
if instance.borrow().canvas_width != canvas_width
|
|
|
|
|| instance.borrow().canvas_height != canvas_height
|
|
|
|
|| (instance.borrow().device_pixel_ratio - device_pixel_ratio).abs()
|
|
|
|
>= std::f64::EPSILON
|
2019-08-20 01:40:55 +00:00
|
|
|
{
|
2020-09-11 20:45:15 +00:00
|
|
|
let mut mut_instance = instance.borrow_mut();
|
2020-11-11 09:55:46 +00:00
|
|
|
// If a canvas resizes, its drawing context will get scaled. You must reset
|
2019-08-20 01:40:55 +00:00
|
|
|
// the width and height attributes of the canvas element to recreate the context.
|
|
|
|
// (NOT the CSS width/height!)
|
2020-09-11 20:45:15 +00:00
|
|
|
mut_instance.canvas_width = canvas_width;
|
|
|
|
mut_instance.canvas_height = canvas_height;
|
|
|
|
mut_instance.device_pixel_ratio = device_pixel_ratio;
|
|
|
|
drop(mut_instance);
|
2019-08-20 05:23:02 +00:00
|
|
|
|
2019-08-20 01:40:55 +00:00
|
|
|
// The actual viewport is scaled by DPI, bigger than CSS pixels.
|
2020-09-11 20:45:15 +00:00
|
|
|
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);
|
2019-11-08 20:09:57 +00:00
|
|
|
|
|
|
|
core_lock.set_viewport_dimensions(viewport_width, viewport_height);
|
|
|
|
core_lock
|
2019-08-20 01:40:55 +00:00
|
|
|
.renderer_mut()
|
|
|
|
.set_viewport_dimensions(viewport_width, viewport_height);
|
|
|
|
|
|
|
|
// Force a re-render if we resize.
|
2020-05-02 11:25:21 +00:00
|
|
|
needs_render = true;
|
|
|
|
}
|
2019-11-08 20:09:57 +00:00
|
|
|
|
2020-05-02 11:25:21 +00:00
|
|
|
if needs_render {
|
|
|
|
core_lock.render();
|
2019-08-20 01:40:55 +00:00
|
|
|
}
|
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Request next animation frame.
|
2020-09-11 20:45:15 +00:00
|
|
|
let mut instance = instance.borrow_mut();
|
2019-05-24 17:25:03 +00:00
|
|
|
if let Some(handler) = &instance.animation_handler {
|
2019-05-17 02:14:23 +00:00
|
|
|
let window = web_sys::window().unwrap();
|
|
|
|
let id = window
|
|
|
|
.request_animation_frame(handler.as_ref().unchecked_ref())
|
|
|
|
.unwrap();
|
2019-05-24 17:25:03 +00:00
|
|
|
instance.animation_handler_id = NonZeroI32::new(id);
|
2019-05-17 02:14:23 +00:00
|
|
|
} else {
|
2019-05-24 17:25:03 +00:00
|
|
|
instance.animation_handler_id = None;
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2019-04-28 06:08:59 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-02 09:59:13 +00:00
|
|
|
|
2020-09-02 19:02:32 +00:00
|
|
|
struct JavascriptMethod {
|
|
|
|
this: JsValue,
|
|
|
|
function: JsValue,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ExternalInterfaceMethod for JavascriptMethod {
|
|
|
|
fn call(
|
|
|
|
&self,
|
2020-09-02 19:51:39 +00:00
|
|
|
context: &mut UpdateContext<'_, '_, '_>,
|
2020-09-02 19:02:32 +00:00
|
|
|
args: &[ExternalValue],
|
|
|
|
) -> ExternalValue {
|
2020-09-02 19:51:39 +00:00
|
|
|
let old_context = CURRENT_CONTEXT.with(|v| {
|
|
|
|
v.replace(Some(unsafe {
|
|
|
|
std::mem::transmute::<
|
|
|
|
&mut UpdateContext,
|
|
|
|
&mut UpdateContext<'static, 'static, 'static>,
|
|
|
|
>(context)
|
|
|
|
} as *mut UpdateContext))
|
|
|
|
});
|
|
|
|
let result = if let Some(function) = self.function.dyn_ref::<Function>() {
|
2020-09-02 19:02:32 +00:00
|
|
|
let args_array = Array::new();
|
|
|
|
for arg in args {
|
|
|
|
args_array.push(&external_to_js_value(arg.to_owned()));
|
|
|
|
}
|
|
|
|
if let Ok(result) = function.apply(&self.this, &args_array) {
|
|
|
|
js_to_external_value(&result)
|
|
|
|
} else {
|
|
|
|
ExternalValue::Null
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ExternalValue::Null
|
2020-09-02 19:51:39 +00:00
|
|
|
};
|
|
|
|
CURRENT_CONTEXT.with(|v| v.replace(old_context));
|
|
|
|
result
|
2020-09-02 19:02:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl JavascriptInterface {
|
2020-09-02 21:36:36 +00:00
|
|
|
fn new(js_player: JavascriptPlayer) -> Self {
|
|
|
|
Self { js_player }
|
2020-09-02 19:02:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn find_method(&self, root: JsValue, name: &str) -> Option<JavascriptMethod> {
|
|
|
|
let mut parent = JsValue::UNDEFINED;
|
|
|
|
let mut value = root;
|
|
|
|
for key in name.split('.') {
|
|
|
|
parent = value;
|
|
|
|
value = js_sys::Reflect::get(&parent, &JsValue::from_str(key)).ok()?;
|
|
|
|
}
|
|
|
|
if value.is_function() {
|
|
|
|
Some(JavascriptMethod {
|
|
|
|
this: parent,
|
|
|
|
function: value,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ExternalInterfaceProvider for JavascriptInterface {
|
|
|
|
fn get_method(&self, name: &str) -> Option<Box<dyn ExternalInterfaceMethod>> {
|
2020-09-02 21:36:36 +00:00
|
|
|
if let Some(method) = self.find_method(self.js_player.clone().into(), name) {
|
2020-09-02 19:02:32 +00:00
|
|
|
return Some(Box::new(method));
|
|
|
|
}
|
|
|
|
if let Some(window) = web_sys::window() {
|
|
|
|
if let Some(method) = self.find_method(window.into(), name) {
|
|
|
|
return Some(Box::new(method));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_callback_available(&self, name: &str) {
|
|
|
|
self.js_player.on_callback_available(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn js_to_external_value(js: &JsValue) -> ExternalValue {
|
|
|
|
if let Some(value) = js.as_f64() {
|
|
|
|
ExternalValue::Number(value)
|
|
|
|
} else if let Some(value) = js.as_string() {
|
|
|
|
ExternalValue::String(value)
|
|
|
|
} else if let Some(value) = js.as_bool() {
|
|
|
|
ExternalValue::Bool(value)
|
|
|
|
} else if let Some(array) = js.dyn_ref::<Array>() {
|
|
|
|
let mut values = Vec::new();
|
|
|
|
for value in array.values() {
|
|
|
|
if let Ok(value) = value {
|
|
|
|
values.push(js_to_external_value(&value));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ExternalValue::List(values)
|
|
|
|
} else if let Some(object) = js.dyn_ref::<Object>() {
|
|
|
|
let mut values = BTreeMap::new();
|
|
|
|
for entry in Object::entries(&object).values() {
|
|
|
|
if let Ok(entry) = entry.and_then(|v| v.dyn_into::<Array>()) {
|
|
|
|
if let Some(key) = entry.get(0).as_string() {
|
|
|
|
values.insert(key, js_to_external_value(&entry.get(1)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ExternalValue::Object(values)
|
|
|
|
} else {
|
|
|
|
ExternalValue::Null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn external_to_js_value(external: ExternalValue) -> JsValue {
|
|
|
|
match external {
|
|
|
|
Value::Null => JsValue::NULL,
|
|
|
|
Value::Bool(value) => JsValue::from_bool(value),
|
|
|
|
Value::Number(value) => JsValue::from_f64(value),
|
|
|
|
Value::String(value) => JsValue::from_str(&value),
|
|
|
|
Value::Object(object) => {
|
|
|
|
let entries = Array::new();
|
|
|
|
for (key, value) in object {
|
|
|
|
entries.push(&Array::of2(
|
|
|
|
&JsValue::from_str(&key),
|
|
|
|
&external_to_js_value(value),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
if let Ok(result) = Object::from_entries(&entries) {
|
|
|
|
result.into()
|
|
|
|
} else {
|
|
|
|
JsValue::NULL
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Value::List(values) => {
|
|
|
|
let array = Array::new();
|
|
|
|
for value in values {
|
|
|
|
array.push(&external_to_js_value(value));
|
|
|
|
}
|
|
|
|
array.into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-14 08:12:24 +00:00
|
|
|
fn create_renderer(
|
|
|
|
document: &web_sys::Document,
|
|
|
|
) -> Result<(HtmlCanvasElement, Box<dyn RenderBackend>), Box<dyn Error>> {
|
2020-05-02 09:59:13 +00:00
|
|
|
#[cfg(not(any(feature = "canvas", feature = "webgl")))]
|
|
|
|
std::compile_error!("You must enable one of the render backend features (e.g., webgl).");
|
|
|
|
|
2020-05-14 08:12:24 +00:00
|
|
|
// Try to create a backend, falling through to the next backend on failure.
|
|
|
|
// We must recreate the canvas each attempt, as only a single context may be created per canvas
|
|
|
|
// with `getContext`.
|
2020-05-02 09:59:13 +00:00
|
|
|
#[cfg(feature = "webgl")]
|
|
|
|
{
|
2020-05-14 08:12:24 +00:00
|
|
|
log::info!("Creating WebGL renderer...");
|
|
|
|
let canvas: HtmlCanvasElement = document
|
|
|
|
.create_element("canvas")
|
|
|
|
.into_js_result()?
|
|
|
|
.dyn_into()
|
|
|
|
.map_err(|_| "Expected HtmlCanvasElement")?;
|
2020-09-23 21:41:27 +00:00
|
|
|
match ruffle_render_webgl::WebGlRenderBackend::new(&canvas) {
|
|
|
|
Ok(renderer) => return Ok((canvas, Box::new(renderer))),
|
|
|
|
Err(error) => log::error!("Error creating WebGL renderer: {}", error),
|
2020-05-02 09:59:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "canvas")]
|
|
|
|
{
|
2020-05-14 08:12:24 +00:00
|
|
|
log::info!("Falling back to Canvas renderer...");
|
|
|
|
let canvas: HtmlCanvasElement = document
|
|
|
|
.create_element("canvas")
|
|
|
|
.into_js_result()?
|
|
|
|
.dyn_into()
|
|
|
|
.map_err(|_| "Expected HtmlCanvasElement")?;
|
2020-09-23 21:41:27 +00:00
|
|
|
match ruffle_render_canvas::WebCanvasRenderBackend::new(&canvas) {
|
|
|
|
Ok(renderer) => return Ok((canvas, Box::new(renderer))),
|
|
|
|
Err(error) => log::error!("Error creating canvas renderer: {}", error),
|
2020-05-02 09:59:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Err("Unable to create renderer".into())
|
|
|
|
}
|
2020-09-09 22:15:02 +00:00
|
|
|
|
|
|
|
pub fn set_panic_handler() {
|
|
|
|
static HOOK_HAS_BEEN_SET: Once = Once::new();
|
|
|
|
HOOK_HAS_BEEN_SET.call_once(|| {
|
|
|
|
std::panic::set_hook(Box::new(|info| {
|
|
|
|
RUFFLE_GLOBAL_PANIC.call_once(|| {
|
2020-09-11 20:45:15 +00:00
|
|
|
console_error_panic_hook::hook(info);
|
2020-09-12 22:03:19 +00:00
|
|
|
|
2020-10-11 07:48:08 +00:00
|
|
|
let error = JsError::new(&info.to_string());
|
2020-09-09 22:15:02 +00:00
|
|
|
let _ = INSTANCES.try_with(|instances| {
|
2020-09-12 22:03:19 +00:00
|
|
|
let mut players = Vec::new();
|
|
|
|
|
|
|
|
// We have to be super cautious to not panic here, and not hold any borrows for
|
|
|
|
// longer than we need to.
|
|
|
|
// We grab all of the JsPlayers out from the list and release our hold, as they
|
|
|
|
// may call back to destroy() - which will mutably borrow instances.
|
|
|
|
|
2020-09-09 22:15:02 +00:00
|
|
|
if let Ok(instances) = instances.try_borrow() {
|
|
|
|
for (_, instance) in instances.iter() {
|
2020-09-12 22:03:19 +00:00
|
|
|
if let Ok(player) = instance.try_borrow().map(|i| i.js_player.clone()) {
|
|
|
|
players.push(player);
|
2020-09-11 20:45:15 +00:00
|
|
|
}
|
2020-09-09 22:15:02 +00:00
|
|
|
}
|
|
|
|
}
|
2020-09-12 22:03:19 +00:00
|
|
|
for player in players {
|
|
|
|
player.panic(&error);
|
|
|
|
}
|
2020-09-09 22:15:02 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
}
|
2020-10-11 18:35:28 +00:00
|
|
|
|
2020-10-12 21:27:53 +00:00
|
|
|
fn populate_movie_parameters(input: &JsValue, output: &mut PropertyMap<String>) {
|
|
|
|
if let Ok(keys) = js_sys::Reflect::own_keys(input) {
|
|
|
|
for key in keys.values() {
|
|
|
|
if let Ok(key) = key {
|
|
|
|
if let Ok(value) = js_sys::Reflect::get(input, &key) {
|
|
|
|
if let (Some(key), Some(value)) = (key.as_string(), value.as_string()) {
|
|
|
|
output.insert(&key, value, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-11 18:35:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|