2022-12-30 23:13:52 +00:00
|
|
|
#![deny(clippy::unwrap_used)]
|
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
//! Ruffle web frontend.
|
2019-05-03 02:11:47 +00:00
|
|
|
mod audio;
|
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;
|
2020-12-15 16:39:11 +00:00
|
|
|
mod ui;
|
2019-05-03 02:11:47 +00:00
|
|
|
|
2019-05-10 16:06:47 +00:00
|
|
|
use generational_arena::{Arena, Index};
|
2023-02-27 13:16:29 +00:00
|
|
|
use js_sys::{Array, Error as JsError, Function, Object, Promise, Uint8Array};
|
2023-02-28 19:09:33 +00:00
|
|
|
use ruffle_core::compatibility_rules::CompatibilityRules;
|
2021-01-06 09:08:47 +00:00
|
|
|
use ruffle_core::config::Letterbox;
|
2020-09-02 19:02:32 +00:00
|
|
|
use ruffle_core::context::UpdateContext;
|
2021-12-17 10:14:39 +00:00
|
|
|
use ruffle_core::events::{KeyCode, MouseButton, MouseWheelDelta};
|
2020-09-02 19:02:32 +00:00
|
|
|
use ruffle_core::external::{
|
|
|
|
ExternalInterfaceMethod, ExternalInterfaceProvider, Value as ExternalValue, Value,
|
|
|
|
};
|
2020-05-04 19:37:06 +00:00
|
|
|
use ruffle_core::tag_utils::SwfMovie;
|
2023-02-09 19:06:17 +00:00
|
|
|
use ruffle_core::{
|
2023-02-04 11:55:09 +00:00
|
|
|
Color, Player, PlayerBuilder, PlayerEvent, SandboxType, StageScaleMode, StaticCallstack,
|
|
|
|
ViewportDimensions,
|
2023-02-09 19:06:17 +00:00
|
|
|
};
|
2023-02-03 14:09:08 +00:00
|
|
|
use ruffle_render::quality::StageQuality;
|
2022-08-25 22:50:52 +00:00
|
|
|
use ruffle_video_software::backend::SoftwareVideoBackend;
|
2020-05-14 08:12:24 +00:00
|
|
|
use ruffle_web_common::JsResult;
|
2021-01-06 07:32:07 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2020-09-02 19:02:32 +00:00
|
|
|
use std::collections::BTreeMap;
|
2023-01-04 12:54:21 +00:00
|
|
|
use std::str::FromStr;
|
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};
|
2021-02-12 08:42:31 +00:00
|
|
|
use std::time::Duration;
|
2019-05-17 02:14:23 +00:00
|
|
|
use std::{cell::RefCell, error::Error, num::NonZeroI32};
|
2023-01-04 12:54:21 +00:00
|
|
|
use tracing_subscriber::layer::{Layered, SubscriberExt};
|
|
|
|
use tracing_subscriber::registry::Registry;
|
|
|
|
use tracing_wasm::{WASMLayer, WASMLayerConfigBuilder};
|
2023-01-22 09:48:14 +00:00
|
|
|
use url::Url;
|
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::{
|
2020-12-22 04:26:15 +00:00
|
|
|
AddEventListenerOptions, Element, Event, EventTarget, HtmlCanvasElement, HtmlElement,
|
2021-04-27 05:17:47 +00:00
|
|
|
KeyboardEvent, PointerEvent, WheelEvent, Window,
|
2020-08-22 00:04:02 +00:00
|
|
|
};
|
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
|
|
|
|
2023-01-06 23:33:31 +00:00
|
|
|
static CURRENT_CONTEXT: RefCell<Option<*mut UpdateContext<'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 {
|
2021-04-27 05:17:47 +00:00
|
|
|
core: Arc<Mutex<Player>>,
|
2022-08-28 16:30:20 +00:00
|
|
|
callstack: Option<StaticCallstack>,
|
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,
|
2021-04-27 05:17:47 +00:00
|
|
|
window: Window,
|
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)>>,
|
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)>>,
|
2021-01-14 12:06:36 +00:00
|
|
|
mouse_up_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)>>,
|
2020-12-27 22:01:42 +00:00
|
|
|
unload_callback: Option<Closure<dyn FnMut(Event)>>,
|
2019-12-22 02:42:55 +00:00
|
|
|
has_focus: bool,
|
2020-09-05 19:43:14 +00:00
|
|
|
trace_observer: Arc<RefCell<JsValue>>,
|
2023-01-04 12:54:21 +00:00
|
|
|
log_subscriber: Arc<Layered<WASMLayer, Registry>>,
|
2019-05-03 00:17:02 +00:00
|
|
|
}
|
|
|
|
|
2023-02-27 13:17:37 +00:00
|
|
|
#[wasm_bindgen(raw_module = "./ruffle-player")]
|
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
|
|
|
|
2021-01-28 10:55:26 +00:00
|
|
|
#[wasm_bindgen(method, catch, js_name = "onFSCommand")]
|
|
|
|
fn on_fs_command(this: &JavascriptPlayer, command: &str, args: &str) -> Result<bool, JsValue>;
|
|
|
|
|
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-12-15 16:39:11 +00:00
|
|
|
|
2021-01-31 01:57:44 +00:00
|
|
|
#[wasm_bindgen(method, js_name = "displayUnsupportedMessage")]
|
|
|
|
fn display_unsupported_message(this: &JavascriptPlayer);
|
|
|
|
|
2021-06-24 20:29:34 +00:00
|
|
|
#[wasm_bindgen(method, js_name = "displayRootMovieDownloadFailedMessage")]
|
|
|
|
fn display_root_movie_download_failed_message(this: &JavascriptPlayer);
|
|
|
|
|
2020-12-15 16:39:11 +00:00
|
|
|
#[wasm_bindgen(method, js_name = "displayMessage")]
|
|
|
|
fn display_message(this: &JavascriptPlayer, message: &str);
|
2021-01-06 00:41:47 +00:00
|
|
|
|
|
|
|
#[wasm_bindgen(method, getter, js_name = "isFullscreen")]
|
|
|
|
fn is_fullscreen(this: &JavascriptPlayer) -> bool;
|
2021-04-21 21:26:06 +00:00
|
|
|
|
2021-08-25 08:02:53 +00:00
|
|
|
#[wasm_bindgen(catch, method, js_name = "setFullscreen")]
|
|
|
|
fn set_fullscreen(this: &JavascriptPlayer, is_full: bool) -> Result<(), JsValue>;
|
|
|
|
|
2021-04-21 21:26:06 +00:00
|
|
|
#[wasm_bindgen(method, js_name = "setMetadata")]
|
|
|
|
fn set_metadata(this: &JavascriptPlayer, metadata: JsValue);
|
2023-02-28 14:15:17 +00:00
|
|
|
|
|
|
|
#[wasm_bindgen(method, js_name = "openVirtualKeyboard")]
|
|
|
|
fn open_virtual_keyboard(this: &JavascriptPlayer);
|
2020-09-02 19:02:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct JavascriptInterface {
|
|
|
|
js_player: JavascriptPlayer,
|
|
|
|
}
|
|
|
|
|
2023-01-04 12:54:21 +00:00
|
|
|
fn deserialize_log_level<'de, D>(deserializer: D) -> Result<tracing::Level, D::Error>
|
|
|
|
where
|
|
|
|
D: serde::Deserializer<'de>,
|
|
|
|
{
|
|
|
|
use serde::de::Error;
|
|
|
|
let value: String = serde::Deserialize::deserialize(deserializer)?;
|
|
|
|
tracing::Level::from_str(&value).map_err(D::Error::custom)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
2022-09-25 22:57:43 +00:00
|
|
|
struct Config {
|
2021-01-31 00:59:58 +00:00
|
|
|
#[serde(rename = "allowScriptAccess")]
|
|
|
|
allow_script_access: bool,
|
|
|
|
|
2021-01-13 08:54:23 +00:00
|
|
|
#[serde(rename = "backgroundColor")]
|
|
|
|
background_color: Option<String>,
|
|
|
|
|
2021-01-06 07:32:07 +00:00
|
|
|
letterbox: Letterbox,
|
2021-01-06 09:08:47 +00:00
|
|
|
|
|
|
|
#[serde(rename = "upgradeToHttps")]
|
|
|
|
upgrade_to_https: bool,
|
2021-01-09 19:36:56 +00:00
|
|
|
|
2023-02-28 19:09:33 +00:00
|
|
|
#[serde(rename = "compatibilityRules")]
|
|
|
|
compatibility_rules: bool,
|
|
|
|
|
2021-08-08 20:38:55 +00:00
|
|
|
#[serde(rename = "base")]
|
|
|
|
base_url: Option<String>,
|
|
|
|
|
2021-08-24 18:52:02 +00:00
|
|
|
#[serde(rename = "menu")]
|
|
|
|
show_menu: bool,
|
|
|
|
|
2021-09-02 13:14:11 +00:00
|
|
|
salign: Option<String>,
|
|
|
|
|
|
|
|
quality: Option<String>,
|
|
|
|
|
|
|
|
scale: Option<String>,
|
|
|
|
|
2023-02-04 11:55:09 +00:00
|
|
|
#[serde(rename = "forceScale")]
|
|
|
|
force_scale: bool,
|
|
|
|
|
2022-04-13 18:55:15 +00:00
|
|
|
wmode: Option<String>,
|
|
|
|
|
2021-01-09 19:36:56 +00:00
|
|
|
#[serde(rename = "warnOnUnsupportedContent")]
|
|
|
|
warn_on_unsupported_content: bool,
|
2021-01-20 02:10:35 +00:00
|
|
|
|
2023-01-04 12:54:21 +00:00
|
|
|
#[serde(rename = "logLevel", deserialize_with = "deserialize_log_level")]
|
|
|
|
log_level: tracing::Level,
|
2021-02-12 08:42:31 +00:00
|
|
|
|
|
|
|
#[serde(rename = "maxExecutionDuration")]
|
|
|
|
max_execution_duration: Duration,
|
2022-12-29 22:12:49 +00:00
|
|
|
|
|
|
|
#[serde(rename = "playerVersion")]
|
2023-01-14 23:27:27 +00:00
|
|
|
player_version: Option<u8>,
|
2021-01-06 09:08:47 +00:00
|
|
|
}
|
|
|
|
|
2021-04-21 21:26:06 +00:00
|
|
|
/// Metadata about the playing SWF file to be passed back to JavaScript.
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct MovieMetadata {
|
|
|
|
width: f64,
|
|
|
|
height: f64,
|
|
|
|
#[serde(rename = "frameRate")]
|
|
|
|
frame_rate: f32,
|
|
|
|
#[serde(rename = "numFrames")]
|
|
|
|
num_frames: u16,
|
|
|
|
#[serde(rename = "swfVersion")]
|
|
|
|
swf_version: u8,
|
2021-05-23 01:42:26 +00:00
|
|
|
#[serde(rename = "backgroundColor")]
|
|
|
|
background_color: Option<String>,
|
|
|
|
#[serde(rename = "isActionScript3")]
|
|
|
|
is_action_script_3: bool,
|
2022-08-01 19:44:02 +00:00
|
|
|
#[serde(rename = "uncompressedLength")]
|
|
|
|
uncompressed_len: u32,
|
2021-04-21 21:26:06 +00:00
|
|
|
}
|
|
|
|
|
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]
|
2021-04-27 05:17:47 +00:00
|
|
|
#[derive(Clone, Copy)]
|
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 {
|
2021-09-09 00:54:13 +00:00
|
|
|
#[allow(clippy::new_ret_no_self)]
|
2020-11-12 22:32:53 +00:00
|
|
|
#[wasm_bindgen(constructor)]
|
2022-09-24 22:44:52 +00:00
|
|
|
pub fn new(parent: HtmlElement, js_player: JavascriptPlayer, config: JsValue) -> Promise {
|
2021-09-09 00:54:13 +00:00
|
|
|
wasm_bindgen_futures::future_to_promise(async move {
|
2022-09-25 22:57:43 +00:00
|
|
|
let config: Config = serde_wasm_bindgen::from_value(config)
|
2022-10-26 23:46:09 +00:00
|
|
|
.map_err(|e| format!("Error parsing config: {e}"))?;
|
2022-09-25 22:57:43 +00:00
|
|
|
|
2021-09-09 00:54:13 +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();
|
2021-01-06 07:32:07 +00:00
|
|
|
|
2021-09-09 00:54:13 +00:00
|
|
|
let ruffle = Ruffle::new_internal(parent, js_player, config)
|
|
|
|
.await
|
|
|
|
.map_err(|_| JsValue::from("Error creating player"))?;
|
|
|
|
Ok(JsValue::from(ruffle))
|
|
|
|
})
|
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.
|
2022-09-13 18:06:53 +00:00
|
|
|
pub fn stream_from(&mut self, movie_url: String, parameters: JsValue) -> Result<(), JsValue> {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_core_mut(|core| {
|
2022-09-13 18:06:53 +00:00
|
|
|
let parameters_to_load = parse_movie_parameters(¶meters);
|
2021-04-21 21:26:06 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
let ruffle = *self;
|
2021-05-23 00:19:45 +00:00
|
|
|
let on_metadata = move |swf_header: &ruffle_core::swf::HeaderExt| {
|
2021-04-21 21:26:06 +00:00
|
|
|
ruffle.on_metadata(swf_header);
|
|
|
|
};
|
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
core.fetch_root_movie(movie_url, parameters_to_load, Box::new(on_metadata));
|
|
|
|
});
|
|
|
|
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.
|
2023-01-22 09:48:14 +00:00
|
|
|
pub fn load_data(
|
|
|
|
&mut self,
|
|
|
|
swf_data: Uint8Array,
|
|
|
|
parameters: JsValue,
|
|
|
|
swf_name: String,
|
|
|
|
) -> Result<(), JsValue> {
|
|
|
|
let window = web_sys::window().ok_or("Expected window")?;
|
|
|
|
let mut url = Url::from_str(&window.location().href()?)
|
|
|
|
.map_err(|e| format!("Error creating url: {e}"))?;
|
|
|
|
url.set_query(None);
|
|
|
|
url.set_fragment(None);
|
|
|
|
if let Ok(mut segments) = url.path_segments_mut() {
|
|
|
|
segments.pop();
|
|
|
|
segments.push(&swf_name);
|
|
|
|
}
|
|
|
|
|
2023-02-26 10:25:41 +00:00
|
|
|
let mut movie = SwfMovie::from_data(&swf_data.to_vec(), url.to_string(), None)
|
2022-10-26 23:46:09 +00:00
|
|
|
.map_err(|e| format!("Error loading movie: {e}"))?;
|
2022-09-13 18:06:53 +00:00
|
|
|
movie.append_parameters(parse_movie_parameters(¶meters));
|
2020-07-23 03:18:30 +00:00
|
|
|
|
2021-04-21 21:26:06 +00:00
|
|
|
self.on_metadata(movie.header());
|
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_core_mut(move |core| {
|
2022-04-08 21:02:07 +00:00
|
|
|
core.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) {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_core_mut(|core| {
|
|
|
|
core.set_is_playing(true);
|
2020-04-27 10:34:47 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-18 22:52:35 +00:00
|
|
|
pub fn pause(&mut self) {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_core_mut(|core| {
|
|
|
|
core.set_is_playing(false);
|
2020-09-18 22:52:35 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-24 19:58:33 +00:00
|
|
|
pub fn is_playing(&mut self) -> bool {
|
2021-04-27 05:17:47 +00:00
|
|
|
self.with_core(|core| core.is_playing()).unwrap_or_default()
|
2021-01-24 19:58:33 +00:00
|
|
|
}
|
|
|
|
|
2022-08-02 15:35:20 +00:00
|
|
|
pub fn volume(&self) -> f32 {
|
|
|
|
self.with_core(|core| core.volume()).unwrap_or_default()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_volume(&mut self, value: f32) {
|
|
|
|
let _ = self.with_core_mut(|core| core.set_volume(value));
|
|
|
|
}
|
|
|
|
|
2023-01-03 14:53:22 +00:00
|
|
|
pub fn renderer_debug_info(&self) -> JsValue {
|
|
|
|
self.with_core(|core| JsValue::from_str(&core.renderer().debug_info()))
|
|
|
|
.unwrap_or(JsValue::NULL)
|
2022-08-29 05:27:49 +00:00
|
|
|
}
|
|
|
|
|
2021-05-02 22:28:00 +00:00
|
|
|
// after the context menu is closed, remember to call `clear_custom_menu_items`!
|
|
|
|
pub fn prepare_context_menu(&mut self) -> JsValue {
|
2021-04-27 05:17:47 +00:00
|
|
|
self.with_core_mut(|core| {
|
|
|
|
let info = core.prepare_context_menu();
|
2022-09-24 22:44:52 +00:00
|
|
|
serde_wasm_bindgen::to_value(&info).unwrap_or(JsValue::UNDEFINED)
|
2021-04-26 09:02:09 +00:00
|
|
|
})
|
2021-04-27 05:17:47 +00:00
|
|
|
.unwrap_or(JsValue::UNDEFINED)
|
2021-04-26 09:02:09 +00:00
|
|
|
}
|
|
|
|
|
2021-05-02 22:28:00 +00:00
|
|
|
pub fn run_context_menu_callback(&mut self, index: usize) {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_core_mut(|core| core.run_context_menu_callback(index));
|
2021-02-20 13:40:55 +00:00
|
|
|
}
|
|
|
|
|
2021-08-25 08:02:53 +00:00
|
|
|
pub fn set_fullscreen(&mut self, is_fullscreen: bool) {
|
|
|
|
let _ = self.with_core_mut(|core| core.set_fullscreen(is_fullscreen));
|
|
|
|
}
|
|
|
|
|
2021-05-02 22:28:00 +00:00
|
|
|
pub fn clear_custom_menu_items(&mut self) {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_core_mut(Player::clear_custom_menu_items);
|
2021-02-20 13:40:55 +00:00
|
|
|
}
|
|
|
|
|
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.
|
2021-04-27 05:17:47 +00:00
|
|
|
if let Ok(mut instance) = self.remove_instance() {
|
2020-05-14 08:12:24 +00:00
|
|
|
instance.canvas.remove();
|
|
|
|
|
2021-01-14 12:06:36 +00:00
|
|
|
// Stop all audio playing from the instance.
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = instance.with_core_mut(|core| {
|
|
|
|
core.audio_mut().stop_all_sounds();
|
|
|
|
core.flush_shared_objects();
|
|
|
|
});
|
2020-04-13 11:57:10 +00:00
|
|
|
|
2020-05-14 08:12:24 +00:00
|
|
|
// Clean up all event listeners.
|
2021-04-27 05:17:47 +00:00
|
|
|
if let Some(mouse_move_callback) = &instance.mouse_move_callback {
|
2023-02-24 23:25:12 +00:00
|
|
|
instance
|
|
|
|
.canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.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 {
|
2023-02-24 23:25:12 +00:00
|
|
|
instance
|
|
|
|
.canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.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 {
|
2023-02-24 23:25:12 +00:00
|
|
|
instance
|
|
|
|
.js_player
|
2021-04-27 05:17:47 +00:00
|
|
|
.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 {
|
2023-02-24 23:25:12 +00:00
|
|
|
instance
|
|
|
|
.canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.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 {
|
2023-02-24 23:25:12 +00:00
|
|
|
instance
|
|
|
|
.canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.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;
|
2021-01-14 12:06:36 +00:00
|
|
|
}
|
2020-05-14 08:12:24 +00:00
|
|
|
|
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 {
|
2021-04-27 05:17:47 +00:00
|
|
|
instance
|
|
|
|
.window
|
|
|
|
.cancel_animation_frame(id.into())
|
|
|
|
.warn_on_error();
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
self.with_core_mut(|core| external_to_js_value(core.call_internal_interface(name, args)))
|
|
|
|
.unwrap_or(JsValue::NULL)
|
2020-09-02 19:02:32 +00:00
|
|
|
}
|
2020-09-05 19:43:14 +00:00
|
|
|
|
|
|
|
pub fn set_trace_observer(&self, observer: JsValue) {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_instance(|instance| {
|
|
|
|
*instance.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> {
|
2021-04-27 05:17:47 +00:00
|
|
|
self.with_core_mut(|core| {
|
|
|
|
core.audio()
|
|
|
|
.downcast_ref::<audio::WebAudioBackend>()
|
|
|
|
.map(|audio| audio.audio_context().clone())
|
2020-10-19 00:11:25 +00:00
|
|
|
})
|
2021-04-27 05:17:47 +00:00
|
|
|
.unwrap_or_default()
|
2020-10-19 00:11:25 +00:00
|
|
|
}
|
2022-01-08 03:49:18 +00:00
|
|
|
|
|
|
|
/// Returns whether the `simd128` target feature was enabled at build time.
|
|
|
|
/// This is intended to discriminate between the two WebAssembly module
|
|
|
|
/// versions, one of which uses WebAssembly extensions, and the other one
|
|
|
|
/// being "vanilla". `simd128` is used as proxy for most extensions, since
|
|
|
|
/// no other WebAssembly target feature is exposed to `cfg!`.
|
|
|
|
pub fn is_wasm_simd_used() -> bool {
|
|
|
|
cfg!(target_feature = "simd128")
|
|
|
|
}
|
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 {
|
2021-09-09 00:54:13 +00:00
|
|
|
async fn new_internal(
|
2020-09-02 19:02:32 +00:00
|
|
|
parent: HtmlElement,
|
|
|
|
js_player: JavascriptPlayer,
|
2021-01-06 07:32:07 +00:00
|
|
|
config: Config,
|
2020-09-02 19:02:32 +00:00
|
|
|
) -> Result<Ruffle, Box<dyn Error>> {
|
2023-03-03 03:24:10 +00:00
|
|
|
// Redirect Log to Tracing if it isn't already
|
|
|
|
let _ = tracing_log::LogTracer::builder()
|
|
|
|
// wgpu crates are extremely verbose
|
|
|
|
.ignore_crate("wgpu_hal")
|
|
|
|
.ignore_crate("wgpu_core")
|
|
|
|
.init();
|
2023-01-04 12:54:21 +00:00
|
|
|
let log_subscriber = Arc::new(
|
|
|
|
Registry::default().with(WASMLayer::new(
|
|
|
|
WASMLayerConfigBuilder::new()
|
|
|
|
.set_max_level(config.log_level)
|
|
|
|
.build(),
|
|
|
|
)),
|
|
|
|
);
|
|
|
|
let _subscriber = tracing::subscriber::set_default(log_subscriber.clone());
|
2021-01-31 00:59:58 +00:00
|
|
|
let allow_script_access = config.allow_script_access;
|
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")?;
|
|
|
|
|
2023-01-03 14:53:22 +00:00
|
|
|
let (mut builder, canvas) =
|
2022-04-26 00:56:57 +00:00
|
|
|
create_renderer(PlayerBuilder::new(), &document, &config).await?;
|
2022-04-13 18:55:15 +00:00
|
|
|
|
2020-05-14 08:12:24 +00:00
|
|
|
parent
|
|
|
|
.append_child(&canvas.clone().into())
|
|
|
|
.into_js_result()?;
|
2022-04-26 00:56:57 +00:00
|
|
|
|
|
|
|
if let Ok(audio) = audio::WebAudioBackend::new() {
|
|
|
|
builder = builder.with_audio(audio);
|
2021-01-28 10:49:24 +00:00
|
|
|
} else {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::error!("Unable to create audio backend. No audio will be played.");
|
2022-04-26 00:56:57 +00:00
|
|
|
}
|
|
|
|
builder = builder.with_navigator(navigator::WebNavigatorBackend::new(
|
2021-01-11 09:35:45 +00:00
|
|
|
allow_script_access,
|
|
|
|
config.upgrade_to_https,
|
2021-08-08 20:38:55 +00:00
|
|
|
config.base_url,
|
2023-03-03 03:10:12 +00:00
|
|
|
log_subscriber.clone(),
|
2021-01-11 09:35:45 +00:00
|
|
|
));
|
2022-04-26 00:56:57 +00:00
|
|
|
|
|
|
|
match window.local_storage() {
|
2021-01-31 00:36:45 +00:00
|
|
|
Ok(Some(s)) => {
|
2022-04-26 00:56:57 +00:00
|
|
|
builder = builder.with_storage(storage::LocalStorageBackend::new(s));
|
2021-01-31 00:36:45 +00:00
|
|
|
}
|
2020-11-20 06:28:45 +00:00
|
|
|
err => {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::warn!("Unable to use localStorage: {:?}\nData will not save.", err);
|
2020-11-20 06:28:45 +00:00
|
|
|
}
|
|
|
|
};
|
2022-04-26 00:56:57 +00:00
|
|
|
|
2023-02-03 15:53:24 +00:00
|
|
|
let default_quality = if ruffle_web_common::is_mobile_or_tablet() {
|
|
|
|
tracing::info!("Running on a mobile device; defaulting to low quality");
|
|
|
|
StageQuality::Low
|
|
|
|
} else {
|
|
|
|
StageQuality::High
|
|
|
|
};
|
|
|
|
|
2020-09-05 19:43:14 +00:00
|
|
|
let trace_observer = Arc::new(RefCell::new(JsValue::UNDEFINED));
|
2022-04-26 00:56:57 +00:00
|
|
|
let core = builder
|
|
|
|
.with_log(log_adapter::WebLogBackend::new(trace_observer.clone()))
|
|
|
|
.with_ui(ui::WebUiBackend::new(js_player.clone(), &canvas))
|
2022-08-25 22:50:52 +00:00
|
|
|
.with_video(SoftwareVideoBackend::new())
|
2022-04-26 03:35:56 +00:00
|
|
|
.with_letterbox(config.letterbox)
|
|
|
|
.with_max_execution_duration(config.max_execution_duration)
|
|
|
|
.with_warn_on_unsupported_content(config.warn_on_unsupported_content)
|
2023-01-14 23:27:27 +00:00
|
|
|
.with_player_version(config.player_version)
|
2023-02-28 19:09:33 +00:00
|
|
|
.with_compatibility_rules(if config.compatibility_rules {
|
|
|
|
CompatibilityRules::default()
|
|
|
|
} else {
|
|
|
|
CompatibilityRules::empty()
|
|
|
|
})
|
2023-02-03 15:53:24 +00:00
|
|
|
.with_quality(
|
|
|
|
config
|
|
|
|
.quality
|
|
|
|
.and_then(|q| StageQuality::from_str(&q).ok())
|
|
|
|
.unwrap_or(default_quality),
|
|
|
|
)
|
2023-02-04 11:55:09 +00:00
|
|
|
.with_scale_mode(
|
|
|
|
config
|
|
|
|
.scale
|
|
|
|
.and_then(|s| StageScaleMode::from_str(&s).ok())
|
|
|
|
.unwrap_or(StageScaleMode::ShowAll),
|
|
|
|
config.force_scale,
|
|
|
|
)
|
2023-02-09 19:06:17 +00:00
|
|
|
// FIXME - should this be configurable?
|
|
|
|
.with_sandbox_type(SandboxType::Remote)
|
2022-04-29 03:43:36 +00:00
|
|
|
.build();
|
2021-04-27 05:17:47 +00:00
|
|
|
|
2022-08-28 16:30:20 +00:00
|
|
|
let mut callstack = None;
|
2021-04-27 05:17:47 +00:00
|
|
|
if let Ok(mut core) = core.try_lock() {
|
|
|
|
// Set config parameters.
|
2021-01-13 08:54:23 +00:00
|
|
|
if let Some(color) = config.background_color.and_then(parse_html_color) {
|
|
|
|
core.set_background_color(Some(color));
|
|
|
|
}
|
2021-08-24 18:52:02 +00:00
|
|
|
core.set_show_menu(config.show_menu);
|
2021-09-02 13:14:11 +00:00
|
|
|
core.set_stage_align(config.salign.as_deref().unwrap_or(""));
|
2022-04-13 18:55:15 +00:00
|
|
|
core.set_window_mode(config.wmode.as_deref().unwrap_or("window"));
|
2021-04-27 05:17:47 +00:00
|
|
|
|
|
|
|
// Create the external interface.
|
|
|
|
if allow_script_access {
|
|
|
|
core.add_external_interface(Box::new(JavascriptInterface::new(js_player.clone())));
|
|
|
|
}
|
2022-08-28 16:30:20 +00:00
|
|
|
callstack = Some(core.callstack());
|
2021-01-13 08:54:23 +00:00
|
|
|
}
|
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,
|
2022-08-28 16:30:20 +00:00
|
|
|
callstack,
|
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(),
|
2021-04-27 05:17:47 +00:00
|
|
|
window: window.clone(),
|
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,
|
2020-12-27 22:01:42 +00:00
|
|
|
unload_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,
|
2023-01-04 12:54:21 +00:00
|
|
|
log_subscriber,
|
2019-05-17 02:14:23 +00:00
|
|
|
};
|
|
|
|
|
2019-12-23 20:15:06 +00:00
|
|
|
// Prevent touch-scrolling on canvas.
|
2021-04-27 05:17:47 +00:00
|
|
|
canvas
|
|
|
|
.style()
|
|
|
|
.set_property("touch-action", "none")
|
|
|
|
.warn_on_error();
|
2019-12-23 20:15:06 +00:00
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Register the instance and create the animation frame closure.
|
2021-04-27 05:17:47 +00:00
|
|
|
let mut ruffle = Ruffle::add_instance(instance)?;
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
// Create the animation frame closure.
|
|
|
|
ruffle.with_instance_mut(|instance| {
|
2023-02-24 23:25:12 +00:00
|
|
|
instance.animation_handler = Some(Closure::new(move |timestamp| {
|
2021-04-27 05:17:47 +00:00
|
|
|
ruffle.tick(timestamp);
|
2023-02-24 23:25:12 +00:00
|
|
|
}));
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
// Create mouse move handler.
|
2023-02-24 23:25:12 +00:00
|
|
|
let mouse_move_callback = Closure::new(move |js_event: PointerEvent| {
|
2021-04-27 05:17:47 +00:00
|
|
|
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);
|
2019-08-20 01:17:29 +00:00
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
if instance.has_focus {
|
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
|
|
|
});
|
2023-02-24 23:25:12 +00:00
|
|
|
});
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2023-02-24 23:25:12 +00:00
|
|
|
canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"pointermove",
|
|
|
|
mouse_move_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.warn_on_error();
|
|
|
|
instance.mouse_move_callback = Some(mouse_move_callback);
|
2020-10-23 01:59:43 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
// Create mouse down handler.
|
2023-02-24 23:25:12 +00:00
|
|
|
let mouse_down_callback = Closure::new(move |js_event: PointerEvent| {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = ruffle.with_instance(move |instance| {
|
2021-12-17 10:14:39 +00:00
|
|
|
if let Some(target) = js_event.current_target() {
|
|
|
|
let _ = target
|
|
|
|
.unchecked_ref::<Element>()
|
|
|
|
.set_pointer_capture(js_event.pointer_id());
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
2021-12-17 10:14:39 +00:00
|
|
|
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,
|
|
|
|
button: match js_event.button() {
|
|
|
|
0 => MouseButton::Left,
|
|
|
|
1 => MouseButton::Middle,
|
|
|
|
2 => MouseButton::Right,
|
|
|
|
_ => MouseButton::Unknown,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
let _ = instance.with_core_mut(|core| {
|
|
|
|
core.handle_event(event);
|
|
|
|
});
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
js_event.prevent_default();
|
|
|
|
});
|
2023-02-24 23:25:12 +00:00
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
|
2023-02-24 23:25:12 +00:00
|
|
|
canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"pointerdown",
|
|
|
|
mouse_down_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.warn_on_error();
|
|
|
|
instance.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.
|
2023-02-24 23:25:12 +00:00
|
|
|
let player_mouse_down_callback = Closure::new(move |_js_event| {
|
|
|
|
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();
|
|
|
|
});
|
|
|
|
});
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2023-02-24 23:25:12 +00:00
|
|
|
js_player
|
2021-04-27 05:17:47 +00:00
|
|
|
.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);
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2019-12-22 02:42:55 +00:00
|
|
|
// Create window mouse down handler.
|
2023-02-24 23:25:12 +00:00
|
|
|
let window_mouse_down_callback = Closure::new(move |_js_event| {
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
});
|
2019-12-22 02:42:55 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
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);
|
2019-12-22 02:42:55 +00:00
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
// Create mouse up handler.
|
2023-02-24 23:25:12 +00:00
|
|
|
let mouse_up_callback = Closure::new(move |js_event: PointerEvent| {
|
2022-12-19 17:21:27 +00:00
|
|
|
let _ = ruffle.with_instance(|instance| {
|
2021-12-17 10:14:39 +00:00
|
|
|
if let Some(target) = js_event.current_target() {
|
|
|
|
let _ = target
|
|
|
|
.unchecked_ref::<Element>()
|
|
|
|
.release_pointer_capture(js_event.pointer_id());
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
2021-12-17 10:14:39 +00:00
|
|
|
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,
|
|
|
|
button: match js_event.button() {
|
|
|
|
0 => MouseButton::Left,
|
|
|
|
1 => MouseButton::Middle,
|
|
|
|
2 => MouseButton::Right,
|
|
|
|
_ => MouseButton::Unknown,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
let _ = instance.with_core_mut(|core| {
|
|
|
|
core.handle_event(event);
|
|
|
|
});
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
if instance.has_focus {
|
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
|
|
|
});
|
2023-02-24 23:25:12 +00:00
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
|
2023-02-24 23:25:12 +00:00
|
|
|
canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"pointerup",
|
|
|
|
mouse_up_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.warn_on_error();
|
|
|
|
instance.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.
|
2023-02-24 23:25:12 +00:00
|
|
|
let mouse_wheel_callback = Closure::new(move |js_event: WheelEvent| {
|
2021-04-27 05:17:47 +00:00
|
|
|
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();
|
2020-08-22 00:04:02 +00:00
|
|
|
}
|
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
});
|
2023-02-24 23:25:12 +00:00
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
|
2023-02-24 23:25:12 +00:00
|
|
|
canvas
|
2021-04-27 05:17:47 +00:00
|
|
|
.add_event_listener_with_callback_and_add_event_listener_options(
|
|
|
|
"wheel",
|
|
|
|
mouse_wheel_callback.as_ref().unchecked_ref(),
|
2023-02-24 23:25:12 +00:00
|
|
|
AddEventListenerOptions::new().passive(false),
|
2021-04-27 05:17:47 +00:00
|
|
|
)
|
|
|
|
.warn_on_error();
|
|
|
|
instance.mouse_wheel_callback = Some(mouse_wheel_callback);
|
2019-08-09 21:50:20 +00:00
|
|
|
|
2019-12-19 01:20:49 +00:00
|
|
|
// Create keydown event handler.
|
2023-02-24 23:25:12 +00:00
|
|
|
let key_down_callback = Closure::new(move |js_event: KeyboardEvent| {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = ruffle.with_instance(|instance| {
|
|
|
|
if instance.has_focus {
|
|
|
|
let _ = instance.with_core_mut(|core| {
|
2021-11-25 22:56:24 +00:00
|
|
|
let key_code = web_to_ruffle_key_code(&js_event.code());
|
|
|
|
let key_char = web_key_to_codepoint(&js_event.key());
|
|
|
|
core.handle_event(PlayerEvent::KeyDown { key_code, key_char });
|
2020-11-14 10:15:59 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
if let Some(codepoint) = key_char {
|
|
|
|
core.handle_event(PlayerEvent::TextInput { codepoint });
|
|
|
|
}
|
|
|
|
});
|
2019-12-19 01:20:49 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
|
|
|
});
|
2023-02-24 23:25:12 +00:00
|
|
|
});
|
2020-12-22 04:26:15 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
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);
|
2020-12-22 04:26:15 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
// Create keyup event handler.
|
2023-02-24 23:25:12 +00:00
|
|
|
let key_up_callback = Closure::new(move |js_event: KeyboardEvent| {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = ruffle.with_instance(|instance| {
|
|
|
|
if instance.has_focus {
|
|
|
|
let _ = instance.with_core_mut(|core| {
|
2021-11-25 22:56:24 +00:00
|
|
|
let key_code = web_to_ruffle_key_code(&js_event.code());
|
|
|
|
let key_char = web_key_to_codepoint(&js_event.key());
|
|
|
|
core.handle_event(PlayerEvent::KeyUp { key_code, key_char });
|
2021-04-27 05:17:47 +00:00
|
|
|
});
|
|
|
|
js_event.prevent_default();
|
|
|
|
}
|
|
|
|
});
|
2023-02-24 23:25:12 +00:00
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2023-02-24 23:25:12 +00:00
|
|
|
let unload_callback = Closure::new(move |_| {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = ruffle.with_core_mut(|core| {
|
|
|
|
core.flush_shared_objects();
|
|
|
|
});
|
2023-02-24 23:25:12 +00:00
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
|
|
|
|
window
|
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"unload",
|
|
|
|
unload_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.warn_on_error();
|
|
|
|
instance.unload_callback = Some(unload_callback);
|
|
|
|
})?;
|
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
|
|
|
}
|
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
/// Registers a new Ruffle instance and returns the handle to the instance.
|
|
|
|
fn add_instance(instance: RuffleInstance) -> Result<Ruffle, RuffleInstanceError> {
|
|
|
|
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<RuffleInstance, RuffleInstanceError> {
|
|
|
|
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<F, O>(&self, f: F) -> Result<O, RuffleInstanceError>
|
|
|
|
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()?;
|
2023-01-04 12:54:21 +00:00
|
|
|
let _subscriber =
|
|
|
|
tracing::subscriber::set_default(instance.log_subscriber.clone());
|
2022-07-02 12:36:15 +00:00
|
|
|
Ok(f(&instance))
|
2021-04-27 05:17:47 +00:00
|
|
|
} else {
|
|
|
|
Err(RuffleInstanceError::InstanceNotFound)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.map_err(RuffleInstanceError::from)
|
|
|
|
.and_then(std::convert::identity);
|
|
|
|
if let Err(e) = &ret {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::error!("{}", e);
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
|
|
|
ret
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the given function on this Ruffle instance.
|
|
|
|
fn with_instance_mut<F, O>(&self, f: F) -> Result<O, RuffleInstanceError>
|
|
|
|
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()?;
|
2023-01-04 12:54:21 +00:00
|
|
|
let _subscriber =
|
|
|
|
tracing::subscriber::set_default(instance.log_subscriber.clone());
|
2022-07-02 12:36:15 +00:00
|
|
|
Ok(f(&mut instance))
|
2019-08-22 20:28:06 +00:00
|
|
|
} else {
|
2021-04-27 05:17:47 +00:00
|
|
|
Err(RuffleInstanceError::InstanceNotFound)
|
2020-05-02 11:25:21 +00:00
|
|
|
}
|
2021-04-27 05:17:47 +00:00
|
|
|
})
|
|
|
|
.map_err(RuffleInstanceError::from)
|
|
|
|
.and_then(std::convert::identity);
|
|
|
|
if let Err(e) = &ret {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::error!("{}", e);
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
|
|
|
ret
|
|
|
|
}
|
2019-11-08 20:09:57 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
/// Runs the given function on this instance's `Player`.
|
|
|
|
fn with_core<F, O>(&self, f: F) -> Result<O, RuffleInstanceError>
|
|
|
|
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()?;
|
2023-01-04 12:54:21 +00:00
|
|
|
let _subscriber =
|
|
|
|
tracing::subscriber::set_default(instance.log_subscriber.clone());
|
2021-04-27 05:17:47 +00:00
|
|
|
// 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)?;
|
2022-07-02 12:36:15 +00:00
|
|
|
Ok(f(&core))
|
2021-04-27 05:17:47 +00:00
|
|
|
} else {
|
|
|
|
Err(RuffleInstanceError::InstanceNotFound)
|
2019-08-20 01:40:55 +00:00
|
|
|
}
|
2021-04-27 05:17:47 +00:00
|
|
|
})
|
|
|
|
.map_err(RuffleInstanceError::from)
|
|
|
|
.and_then(std::convert::identity);
|
|
|
|
if let Err(e) = &ret {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::error!("{}", e);
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
|
|
|
ret
|
|
|
|
}
|
2019-08-20 01:40:55 +00:00
|
|
|
|
2021-04-27 05:17:47 +00:00
|
|
|
/// Runs the given function on this instance's `Player`.
|
|
|
|
fn with_core_mut<F, O>(&self, f: F) -> Result<O, RuffleInstanceError>
|
|
|
|
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()?;
|
2023-01-04 12:54:21 +00:00
|
|
|
let _subscriber =
|
|
|
|
tracing::subscriber::set_default(instance.log_subscriber.clone());
|
2021-04-27 05:17:47 +00:00
|
|
|
// 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)?;
|
2022-07-02 12:36:15 +00:00
|
|
|
Ok(f(&mut core))
|
2019-05-17 02:14:23 +00:00
|
|
|
} else {
|
2021-04-27 05:17:47 +00:00
|
|
|
Err(RuffleInstanceError::InstanceNotFound)
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
2021-04-27 05:17:47 +00:00
|
|
|
})
|
|
|
|
.map_err(RuffleInstanceError::from)
|
|
|
|
.and_then(std::convert::identity);
|
|
|
|
if let Err(e) = &ret {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::error!("{}", e);
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
|
|
|
ret
|
|
|
|
}
|
|
|
|
|
|
|
|
fn tick(&mut self, timestamp: f64) {
|
|
|
|
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;
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
new_dimensions = Some((
|
|
|
|
instance.canvas.clone(),
|
|
|
|
viewport_width,
|
|
|
|
viewport_height,
|
|
|
|
device_pixel_ratio,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2021-08-17 18:40:16 +00:00
|
|
|
// Calculate the elapsed time since the last tick.
|
|
|
|
dt = instance
|
|
|
|
.timestamp
|
|
|
|
.map_or(0.0, |prev_timestamp| timestamp - prev_timestamp);
|
|
|
|
|
|
|
|
// Store the timestamp of the last tick.
|
|
|
|
instance.timestamp = Some(timestamp);
|
2021-04-27 05:17:47 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2022-08-04 05:50:18 +00:00
|
|
|
core.set_viewport_dimensions(ViewportDimensions {
|
|
|
|
width: viewport_width,
|
|
|
|
height: viewport_height,
|
|
|
|
scale_factor: device_pixel_ratio,
|
|
|
|
});
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
core.tick(dt);
|
|
|
|
|
|
|
|
// Render if the core signals a new frame, or if we resized.
|
|
|
|
if core.needs_render() || new_dimensions.is_some() {
|
|
|
|
core.render();
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
});
|
2019-04-28 06:08:59 +00:00
|
|
|
}
|
2021-04-21 21:26:06 +00:00
|
|
|
|
2021-05-23 00:19:45 +00:00
|
|
|
fn on_metadata(&self, swf_header: &ruffle_core::swf::HeaderExt) {
|
2021-04-27 05:17:47 +00:00
|
|
|
let _ = self.with_instance(|instance| {
|
2021-05-23 01:42:26 +00:00
|
|
|
// Convert the background color to an HTML hex color ("#FFFFFF").
|
|
|
|
let background_color = swf_header
|
|
|
|
.background_color()
|
|
|
|
.map(|color| format!("#{:06X}", color.to_rgb()));
|
2021-04-21 21:26:06 +00:00
|
|
|
let metadata = MovieMetadata {
|
2022-10-29 20:01:01 +00:00
|
|
|
width: swf_header.stage_size().width().to_pixels(),
|
|
|
|
height: swf_header.stage_size().height().to_pixels(),
|
2021-05-30 20:30:06 +00:00
|
|
|
frame_rate: swf_header.frame_rate().to_f32(),
|
2021-05-23 00:19:45 +00:00
|
|
|
num_frames: swf_header.num_frames(),
|
2022-08-01 19:44:02 +00:00
|
|
|
uncompressed_len: swf_header.uncompressed_len(),
|
2021-05-23 00:19:45 +00:00
|
|
|
swf_version: swf_header.version(),
|
2021-05-23 01:42:26 +00:00
|
|
|
background_color,
|
|
|
|
is_action_script_3: swf_header.is_action_script_3(),
|
2021-04-21 21:26:06 +00:00
|
|
|
};
|
|
|
|
|
2022-09-24 22:44:52 +00:00
|
|
|
if let Ok(value) = serde_wasm_bindgen::to_value(&metadata) {
|
2021-04-27 05:17:47 +00:00
|
|
|
instance.js_player.set_metadata(value);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RuffleInstance {
|
|
|
|
#[allow(dead_code)]
|
|
|
|
fn with_core<F, O>(&self, f: F) -> Result<O, RuffleInstanceError>
|
|
|
|
where
|
|
|
|
F: FnOnce(&ruffle_core::Player) -> O,
|
|
|
|
{
|
|
|
|
let ret = self
|
|
|
|
.core
|
|
|
|
.try_lock()
|
2022-07-02 12:36:15 +00:00
|
|
|
.map(|core| f(&core))
|
2021-04-27 05:17:47 +00:00
|
|
|
.map_err(|_| RuffleInstanceError::TryLockError);
|
|
|
|
if let Err(e) = &ret {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::error!("{}", e);
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
|
|
|
ret
|
2021-04-21 21:26:06 +00:00
|
|
|
}
|
2021-04-27 05:17:47 +00:00
|
|
|
|
|
|
|
fn with_core_mut<F, O>(&self, f: F) -> Result<O, RuffleInstanceError>
|
|
|
|
where
|
|
|
|
F: FnOnce(&mut ruffle_core::Player) -> O,
|
|
|
|
{
|
|
|
|
let ret = self
|
|
|
|
.core
|
|
|
|
.try_lock()
|
2022-07-02 12:36:15 +00:00
|
|
|
.map(|mut core| f(&mut core))
|
2021-04-27 05:17:47 +00:00
|
|
|
.map_err(|_| RuffleInstanceError::TryLockError);
|
|
|
|
if let Err(e) = &ret {
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::error!("{}", e);
|
2021-04-27 05:17:47 +00:00
|
|
|
}
|
|
|
|
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,
|
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 {
|
2023-01-06 23:33:31 +00:00
|
|
|
fn call(&self, context: &mut UpdateContext<'_, '_>, args: &[ExternalValue]) -> ExternalValue {
|
2020-09-02 19:51:39 +00:00
|
|
|
let old_context = CURRENT_CONTEXT.with(|v| {
|
|
|
|
v.replace(Some(unsafe {
|
2023-01-06 23:33:31 +00:00
|
|
|
std::mem::transmute::<&mut UpdateContext, &mut UpdateContext<'static, 'static>>(
|
|
|
|
context,
|
|
|
|
)
|
2020-09-02 19:51:39 +00:00
|
|
|
} 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);
|
|
|
|
}
|
2021-01-28 10:55:26 +00:00
|
|
|
|
|
|
|
fn on_fs_command(&self, command: &str, args: &str) -> bool {
|
|
|
|
self.js_player
|
|
|
|
.on_fs_command(command, args)
|
|
|
|
.unwrap_or_default()
|
|
|
|
}
|
2020-09-02 19:02:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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>() {
|
2021-02-19 09:43:01 +00:00
|
|
|
let values: Vec<_> = array
|
|
|
|
.values()
|
|
|
|
.into_iter()
|
|
|
|
.flatten()
|
|
|
|
.map(|v| js_to_external_value(&v))
|
|
|
|
.collect();
|
2020-09-02 19:02:32 +00:00
|
|
|
ExternalValue::List(values)
|
|
|
|
} else if let Some(object) = js.dyn_ref::<Object>() {
|
|
|
|
let mut values = BTreeMap::new();
|
2021-06-05 10:53:23 +00:00
|
|
|
for entry in Object::entries(object).values() {
|
2020-09-02 19:02:32 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-09 00:54:13 +00:00
|
|
|
async fn create_renderer(
|
2022-04-26 00:56:57 +00:00
|
|
|
builder: PlayerBuilder,
|
2020-05-14 08:12:24 +00:00
|
|
|
document: &web_sys::Document,
|
2022-04-13 18:55:15 +00:00
|
|
|
config: &Config,
|
2023-01-03 14:53:22 +00:00
|
|
|
) -> Result<(PlayerBuilder, HtmlCanvasElement), Box<dyn Error>> {
|
2022-08-29 04:57:18 +00:00
|
|
|
#[cfg(not(any(feature = "canvas", feature = "webgpu", feature = "wgpu-webgl")))]
|
2020-05-02 09:59:13 +00:00
|
|
|
std::compile_error!("You must enable one of the render backend features (e.g., webgl).");
|
|
|
|
|
2022-04-13 18:55:15 +00:00
|
|
|
let _is_transparent = config.wmode.as_deref() == Some("transparent");
|
|
|
|
|
2021-09-09 00:54:13 +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`.
|
2022-08-29 04:57:18 +00:00
|
|
|
#[cfg(all(feature = "webgpu", target_family = "wasm"))]
|
2021-09-09 00:54:13 +00:00
|
|
|
{
|
|
|
|
// Check that we have access to WebGPU (navigator.gpu should exist).
|
|
|
|
if web_sys::window()
|
|
|
|
.ok_or(JsValue::FALSE)
|
|
|
|
.and_then(|window| js_sys::Reflect::has(&window.navigator(), &JsValue::from_str("gpu")))
|
|
|
|
.unwrap_or_default()
|
|
|
|
{
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::info!("Creating wgpu webgpu renderer...");
|
2021-09-09 00:54:13 +00:00
|
|
|
let canvas: HtmlCanvasElement = document
|
|
|
|
.create_element("canvas")
|
|
|
|
.into_js_result()?
|
|
|
|
.dyn_into()
|
|
|
|
.map_err(|_| "Expected HtmlCanvasElement")?;
|
|
|
|
|
2023-02-03 16:13:21 +00:00
|
|
|
match ruffle_render_wgpu::backend::WgpuRenderBackend::for_canvas(&canvas).await {
|
2022-04-26 00:56:57 +00:00
|
|
|
Ok(renderer) => {
|
2023-01-03 14:53:22 +00:00
|
|
|
return Ok((builder.with_renderer(renderer), canvas));
|
2022-04-26 00:56:57 +00:00
|
|
|
}
|
2023-01-04 11:33:10 +00:00
|
|
|
Err(error) => tracing::error!("Error creating wgpu webgpu renderer: {}", error),
|
2021-09-09 00:54:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-29 04:57:18 +00:00
|
|
|
#[cfg(all(feature = "wgpu-webgl", target_family = "wasm"))]
|
|
|
|
{
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::info!("Creating wgpu webgl renderer...");
|
2022-08-29 04:57:18 +00:00
|
|
|
let canvas: HtmlCanvasElement = document
|
|
|
|
.create_element("canvas")
|
|
|
|
.into_js_result()?
|
|
|
|
.dyn_into()
|
|
|
|
.map_err(|_| "Expected HtmlCanvasElement")?;
|
|
|
|
|
2023-02-03 16:13:21 +00:00
|
|
|
match ruffle_render_wgpu::backend::WgpuRenderBackend::for_canvas(&canvas).await {
|
2022-08-29 04:57:18 +00:00
|
|
|
Ok(renderer) => {
|
2023-01-03 14:53:22 +00:00
|
|
|
return Ok((builder.with_renderer(renderer), canvas));
|
2022-08-29 04:57:18 +00:00
|
|
|
}
|
2023-01-04 11:33:10 +00:00
|
|
|
Err(error) => tracing::error!("Error creating wgpu webgl renderer: {}", error),
|
2022-08-29 04:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-09 00:54:13 +00:00
|
|
|
|
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")]
|
|
|
|
{
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::info!("Creating WebGL renderer...");
|
2020-05-14 08:12:24 +00:00
|
|
|
let canvas: HtmlCanvasElement = document
|
|
|
|
.create_element("canvas")
|
|
|
|
.into_js_result()?
|
|
|
|
.dyn_into()
|
|
|
|
.map_err(|_| "Expected HtmlCanvasElement")?;
|
2022-04-13 18:55:15 +00:00
|
|
|
match ruffle_render_webgl::WebGlRenderBackend::new(&canvas, _is_transparent) {
|
2022-04-26 00:56:57 +00:00
|
|
|
Ok(renderer) => {
|
2023-01-03 14:53:22 +00:00
|
|
|
return Ok((builder.with_renderer(renderer), canvas));
|
2022-04-26 00:56:57 +00:00
|
|
|
}
|
2023-01-04 11:33:10 +00:00
|
|
|
Err(error) => tracing::error!("Error creating WebGL renderer: {}", error),
|
2020-05-02 09:59:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "canvas")]
|
|
|
|
{
|
2023-01-04 11:33:10 +00:00
|
|
|
tracing::info!("Falling back to Canvas renderer...");
|
2020-05-14 08:12:24 +00:00
|
|
|
let canvas: HtmlCanvasElement = document
|
|
|
|
.create_element("canvas")
|
|
|
|
.into_js_result()?
|
|
|
|
.dyn_into()
|
|
|
|
.map_err(|_| "Expected HtmlCanvasElement")?;
|
2022-04-13 18:55:15 +00:00
|
|
|
match ruffle_render_canvas::WebCanvasRenderBackend::new(&canvas, _is_transparent) {
|
2022-04-26 00:56:57 +00:00
|
|
|
Ok(renderer) => {
|
2023-01-03 14:53:22 +00:00
|
|
|
return Ok((builder.with_renderer(renderer), canvas));
|
2022-04-26 00:56:57 +00:00
|
|
|
}
|
2023-01-04 11:33:10 +00:00
|
|
|
Err(error) => tracing::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-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() {
|
2022-08-28 16:30:20 +00:00
|
|
|
if let Ok((player, Some(callstack))) = instance
|
|
|
|
.try_borrow()
|
|
|
|
.map(|i| (i.js_player.clone(), i.callstack.clone()))
|
|
|
|
{
|
|
|
|
players.push((player, callstack));
|
2020-09-11 20:45:15 +00:00
|
|
|
}
|
2020-09-09 22:15:02 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-28 16:30:20 +00:00
|
|
|
for (player, callstack) in players {
|
|
|
|
let error = JsError::new(&info.to_string());
|
|
|
|
callstack.avm2(|callstack| {
|
|
|
|
let _ = js_sys::Reflect::set(
|
|
|
|
&error,
|
|
|
|
&"avmStack".into(),
|
|
|
|
&callstack.to_string().into(),
|
|
|
|
);
|
|
|
|
});
|
2020-09-12 22:03:19 +00:00
|
|
|
player.panic(&error);
|
|
|
|
}
|
2020-09-09 22:15:02 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
}
|
2020-10-11 18:35:28 +00:00
|
|
|
|
2021-05-03 18:11:38 +00:00
|
|
|
fn parse_movie_parameters(input: &JsValue) -> Vec<(String, String)> {
|
|
|
|
let mut params = Vec::new();
|
2020-10-12 21:27:53 +00:00
|
|
|
if let Ok(keys) = js_sys::Reflect::own_keys(input) {
|
2021-02-19 09:43:01 +00:00
|
|
|
for key in keys.values().into_iter().flatten() {
|
|
|
|
if let Ok(value) = js_sys::Reflect::get(input, &key) {
|
|
|
|
if let (Some(key), Some(value)) = (key.as_string(), value.as_string()) {
|
2021-05-03 18:11:38 +00:00
|
|
|
params.push((key, value))
|
2020-10-12 21:27:53 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-11 18:35:28 +00:00
|
|
|
}
|
|
|
|
}
|
2021-05-03 18:11:38 +00:00
|
|
|
params
|
2020-10-11 18:35:28 +00:00
|
|
|
}
|
2021-01-13 08:54:23 +00:00
|
|
|
|
|
|
|
fn parse_html_color(color: impl AsRef<str>) -> Option<Color> {
|
|
|
|
// Parse classic HTML hex color (XXXXXX or #XXXXXX), attempting to match browser behavior.
|
|
|
|
// Optional leading #.
|
|
|
|
let mut color = color.as_ref();
|
|
|
|
color = color.strip_prefix('#').unwrap_or(color);
|
|
|
|
|
|
|
|
// Fail if less than 6 digits.
|
|
|
|
if color.len() < 6 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Each char represents 4-bits. Invalid hex digit is allowed (converts to 0).
|
|
|
|
let mut ret: u32 = 0;
|
|
|
|
for c in color[..6].bytes() {
|
|
|
|
let digit = match c {
|
|
|
|
b'0'..=b'9' => c - b'0',
|
|
|
|
b'a'..=b'f' => c - b'a' + 10,
|
|
|
|
b'A'..=b'F' => c - b'A' + 10,
|
|
|
|
_ => 0,
|
|
|
|
};
|
|
|
|
ret <<= 4;
|
|
|
|
ret |= u32::from(digit);
|
|
|
|
}
|
|
|
|
Some(Color::from_rgb(ret, 255))
|
|
|
|
}
|
2021-11-25 22:56:24 +00:00
|
|
|
|
|
|
|
/// Convert a web `KeyboardEvent.code` value into a Ruffle `KeyCode`.
|
|
|
|
/// Return `KeyCode::Unknown` if there is no matching Flash key code.
|
|
|
|
fn web_to_ruffle_key_code(key_code: &str) -> KeyCode {
|
|
|
|
match key_code {
|
|
|
|
"Backspace" => KeyCode::Backspace,
|
|
|
|
"Tab" => KeyCode::Tab,
|
|
|
|
"Enter" => KeyCode::Return,
|
|
|
|
"ShiftLeft" | "ShiftRight" => KeyCode::Shift,
|
|
|
|
"ControlLeft" | "ControlRight" => KeyCode::Control,
|
|
|
|
"AltLeft" | "AltRight" => KeyCode::Alt,
|
|
|
|
"CapsLock" => KeyCode::CapsLock,
|
|
|
|
"Escape" => KeyCode::Escape,
|
|
|
|
"Space" => KeyCode::Space,
|
|
|
|
"Digit0" => KeyCode::Key0,
|
|
|
|
"Digit1" => KeyCode::Key1,
|
|
|
|
"Digit2" => KeyCode::Key2,
|
|
|
|
"Digit3" => KeyCode::Key3,
|
|
|
|
"Digit4" => KeyCode::Key4,
|
|
|
|
"Digit5" => KeyCode::Key5,
|
|
|
|
"Digit6" => KeyCode::Key6,
|
|
|
|
"Digit7" => KeyCode::Key7,
|
|
|
|
"Digit8" => KeyCode::Key8,
|
|
|
|
"Digit9" => KeyCode::Key9,
|
|
|
|
"KeyA" => KeyCode::A,
|
|
|
|
"KeyB" => KeyCode::B,
|
|
|
|
"KeyC" => KeyCode::C,
|
|
|
|
"KeyD" => KeyCode::D,
|
|
|
|
"KeyE" => KeyCode::E,
|
|
|
|
"KeyF" => KeyCode::F,
|
|
|
|
"KeyG" => KeyCode::G,
|
|
|
|
"KeyH" => KeyCode::H,
|
|
|
|
"KeyI" => KeyCode::I,
|
|
|
|
"KeyJ" => KeyCode::J,
|
|
|
|
"KeyK" => KeyCode::K,
|
|
|
|
"KeyL" => KeyCode::L,
|
|
|
|
"KeyM" => KeyCode::M,
|
|
|
|
"KeyN" => KeyCode::N,
|
|
|
|
"KeyO" => KeyCode::O,
|
|
|
|
"KeyP" => KeyCode::P,
|
|
|
|
"KeyQ" => KeyCode::Q,
|
|
|
|
"KeyR" => KeyCode::R,
|
|
|
|
"KeyS" => KeyCode::S,
|
|
|
|
"KeyT" => KeyCode::T,
|
|
|
|
"KeyU" => KeyCode::U,
|
|
|
|
"KeyV" => KeyCode::V,
|
|
|
|
"KeyW" => KeyCode::W,
|
|
|
|
"KeyX" => KeyCode::X,
|
|
|
|
"KeyY" => KeyCode::Y,
|
|
|
|
"KeyZ" => KeyCode::Z,
|
|
|
|
"Semicolon" => KeyCode::Semicolon,
|
|
|
|
"Equal" => KeyCode::Equals,
|
|
|
|
"Comma" => KeyCode::Comma,
|
|
|
|
"Minus" => KeyCode::Minus,
|
|
|
|
"Period" => KeyCode::Period,
|
|
|
|
"Slash" => KeyCode::Slash,
|
|
|
|
"Backquote" => KeyCode::Grave,
|
|
|
|
"BracketLeft" => KeyCode::LBracket,
|
|
|
|
"Backslash" => KeyCode::Backslash,
|
|
|
|
"BracketRight" => KeyCode::RBracket,
|
|
|
|
"Quote" => KeyCode::Apostrophe,
|
|
|
|
"Numpad0" => KeyCode::Numpad0,
|
|
|
|
"Numpad1" => KeyCode::Numpad1,
|
|
|
|
"Numpad2" => KeyCode::Numpad2,
|
|
|
|
"Numpad3" => KeyCode::Numpad3,
|
|
|
|
"Numpad4" => KeyCode::Numpad4,
|
|
|
|
"Numpad5" => KeyCode::Numpad5,
|
|
|
|
"Numpad6" => KeyCode::Numpad6,
|
|
|
|
"Numpad7" => KeyCode::Numpad7,
|
|
|
|
"Numpad8" => KeyCode::Numpad8,
|
|
|
|
"Numpad9" => KeyCode::Numpad9,
|
|
|
|
"NumpadMultiply" => KeyCode::Multiply,
|
|
|
|
"NumpadAdd" => KeyCode::Plus,
|
|
|
|
"NumpadSubtract" => KeyCode::NumpadMinus,
|
|
|
|
"NumpadDecimal" => KeyCode::NumpadPeriod,
|
|
|
|
"NumpadDivide" => KeyCode::NumpadSlash,
|
2023-01-18 18:28:26 +00:00
|
|
|
"NumpadEnter" => KeyCode::Return,
|
2021-11-25 22:56:24 +00:00
|
|
|
"PageUp" => KeyCode::PgUp,
|
|
|
|
"PageDown" => KeyCode::PgDown,
|
|
|
|
"End" => KeyCode::End,
|
|
|
|
"Home" => KeyCode::Home,
|
|
|
|
"ArrowLeft" => KeyCode::Left,
|
|
|
|
"ArrowUp" => KeyCode::Up,
|
|
|
|
"ArrowRight" => KeyCode::Right,
|
|
|
|
"ArrowDown" => KeyCode::Down,
|
|
|
|
"Insert" => KeyCode::Insert,
|
|
|
|
"Delete" => KeyCode::Delete,
|
|
|
|
"Pause" => KeyCode::Pause,
|
|
|
|
"ScrollLock" => KeyCode::ScrollLock,
|
|
|
|
"F1" => KeyCode::F1,
|
|
|
|
"F2" => KeyCode::F2,
|
|
|
|
"F3" => KeyCode::F3,
|
|
|
|
"F4" => KeyCode::F4,
|
|
|
|
"F5" => KeyCode::F5,
|
|
|
|
"F6" => KeyCode::F6,
|
|
|
|
"F7" => KeyCode::F7,
|
|
|
|
"F8" => KeyCode::F8,
|
|
|
|
"F9" => KeyCode::F9,
|
|
|
|
"F10" => KeyCode::F10,
|
|
|
|
"F11" => KeyCode::F11,
|
|
|
|
"F12" => KeyCode::F12,
|
|
|
|
_ => KeyCode::Unknown,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Convert a web `KeyboardEvent.key` value into a character codepoint.
|
|
|
|
/// Return `None` if they input was not a printable character.
|
|
|
|
fn web_key_to_codepoint(key: &str) -> Option<char> {
|
|
|
|
// TODO: This is a very cheesy way to tell if a `KeyboardEvent.key` is a printable character.
|
|
|
|
// Single character strings will be an actual printable char that we can use as text input.
|
|
|
|
// All the other special values are multiple characters (e.g. "ArrowLeft").
|
|
|
|
// It's probably better to explicitly match on all the variants.
|
|
|
|
let mut chars = key.chars();
|
|
|
|
let (c1, c2) = (chars.next(), chars.next());
|
|
|
|
if c2.is_none() {
|
|
|
|
// Single character.
|
|
|
|
c1
|
|
|
|
} else {
|
|
|
|
// Check for special characters.
|
|
|
|
match key {
|
|
|
|
"Backspace" => Some(8 as char),
|
|
|
|
"Delete" => Some(127 as char),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|