web: Don't parse config in Rust, do it in Typescript with some tests
This commit is contained in:
parent
6e53f98068
commit
3fa8735e97
|
@ -41,7 +41,7 @@ impl FromStr for Letterbox {
|
||||||
|
|
||||||
/// The networking API access mode of the Ruffle player.
|
/// The networking API access mode of the Ruffle player.
|
||||||
/// This setting is only used on web.
|
/// This setting is only used on web.
|
||||||
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum NetworkingAccessMode {
|
pub enum NetworkingAccessMode {
|
||||||
/// All networking APIs are permitted in the SWF file.
|
/// All networking APIs are permitted in the SWF file.
|
||||||
#[serde(rename = "all")]
|
#[serde(rename = "all")]
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
import type { RuffleInstanceBuilder } from "../../dist/ruffle_web";
|
||||||
|
import { BaseLoadOptions, Duration, SecsDuration } from "../load-options";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given value is explicitly `T` (not null, not undefined)
|
||||||
|
*
|
||||||
|
* @param value The value to test
|
||||||
|
* @returns true if the value isn't null or undefined
|
||||||
|
*/
|
||||||
|
function isExplicit<T>(value: T | undefined | null): value is T {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the given RuffleInstanceBuilder for the general options provided.
|
||||||
|
*
|
||||||
|
* This is the translation layer between what we allow users to provide through e.g. `window.RufflePlayer.config`,
|
||||||
|
* which is quite relaxed and may evolve over time,
|
||||||
|
* and the actual values we accept inside Rust (which is quite strict).
|
||||||
|
*
|
||||||
|
* This allows us to change the rust side at will, and without needing to worry about backwards compatibility, parsing, etc.
|
||||||
|
*
|
||||||
|
* @param builder The builder to set the options on
|
||||||
|
* @param config The options to apply
|
||||||
|
*/
|
||||||
|
export function configureBuilder(
|
||||||
|
builder: RuffleInstanceBuilder,
|
||||||
|
config: BaseLoadOptions,
|
||||||
|
) {
|
||||||
|
// Guard things for being explicitly set, so that we don't need to specify defaults in yet another place...
|
||||||
|
|
||||||
|
if (isExplicit(config.allowScriptAccess)) {
|
||||||
|
builder.setAllowScriptAccess(config.allowScriptAccess);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.backgroundColor)) {
|
||||||
|
builder.setBackgroundColor(parseColor(config.backgroundColor));
|
||||||
|
}
|
||||||
|
if (isExplicit(config.upgradeToHttps)) {
|
||||||
|
builder.setUpgradeToHttps(config.upgradeToHttps);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.compatibilityRules)) {
|
||||||
|
builder.setCompatibilityRules(config.compatibilityRules);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.letterbox)) {
|
||||||
|
builder.setLetterbox(config.letterbox.toLowerCase());
|
||||||
|
}
|
||||||
|
if (isExplicit(config.base)) {
|
||||||
|
builder.setBaseUrl(config.base);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.menu)) {
|
||||||
|
builder.setShowMenu(config.menu);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.allowFullscreen)) {
|
||||||
|
builder.setAllowFullscreen(config.allowFullscreen);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.salign)) {
|
||||||
|
builder.setStageAlign(config.salign.toLowerCase());
|
||||||
|
}
|
||||||
|
if (isExplicit(config.forceAlign)) {
|
||||||
|
builder.setForceAlign(config.forceAlign);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.quality)) {
|
||||||
|
builder.setQuality(config.quality.toLowerCase());
|
||||||
|
}
|
||||||
|
if (isExplicit(config.scale)) {
|
||||||
|
builder.setScale(config.scale.toLowerCase());
|
||||||
|
}
|
||||||
|
if (isExplicit(config.forceScale)) {
|
||||||
|
builder.setForceScale(config.forceScale);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.frameRate)) {
|
||||||
|
builder.setFrameRate(config.frameRate);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.wmode)) {
|
||||||
|
builder.setWmode(config.wmode);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.logLevel)) {
|
||||||
|
builder.setLogLevel(config.logLevel);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.maxExecutionDuration)) {
|
||||||
|
builder.setMaxExecutionDuration(
|
||||||
|
parseDuration(config.maxExecutionDuration),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.playerVersion)) {
|
||||||
|
builder.setPlayerVersion(config.playerVersion);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.preferredRenderer)) {
|
||||||
|
builder.setPreferredRenderer(config.preferredRenderer);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.openUrlMode)) {
|
||||||
|
builder.setOpenUrlMode(config.openUrlMode.toLowerCase());
|
||||||
|
}
|
||||||
|
if (isExplicit(config.allowNetworking)) {
|
||||||
|
builder.setAllowNetworking(config.allowNetworking.toLowerCase());
|
||||||
|
}
|
||||||
|
if (isExplicit(config.credentialAllowList)) {
|
||||||
|
builder.setCredentialAllowList(config.credentialAllowList);
|
||||||
|
}
|
||||||
|
if (isExplicit(config.playerRuntime)) {
|
||||||
|
builder.setPlayerRuntime(config.playerRuntime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExplicit(config.socketProxy)) {
|
||||||
|
for (const proxy of config.socketProxy) {
|
||||||
|
builder.addSocketProxy(proxy.host, proxy.port, proxy.proxyUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a color into an RGB value.
|
||||||
|
*
|
||||||
|
* @param color The color string to parse
|
||||||
|
* @returns A valid RGB number, or undefined if invalid
|
||||||
|
*/
|
||||||
|
export function parseColor(color: string): number | undefined {
|
||||||
|
if (color.startsWith("#")) {
|
||||||
|
color = color.substring(1);
|
||||||
|
}
|
||||||
|
if (color.length < 6) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let result = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const digit = parseInt(color[i]!, 16);
|
||||||
|
if (!isNaN(digit)) {
|
||||||
|
result = (result << 4) | digit;
|
||||||
|
} else {
|
||||||
|
result = result << 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a duration into number of seconds.
|
||||||
|
*
|
||||||
|
* @param value The duration to parse
|
||||||
|
* @returns A valid number of seconds
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export function parseDuration(value: Duration): SecsDuration {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.secs;
|
||||||
|
}
|
|
@ -9,11 +9,10 @@ import {
|
||||||
signExtensions,
|
signExtensions,
|
||||||
referenceTypes,
|
referenceTypes,
|
||||||
} from "wasm-feature-detect";
|
} from "wasm-feature-detect";
|
||||||
import type { RuffleHandle } from "../dist/ruffle_web";
|
import type { RuffleInstanceBuilder } from "../dist/ruffle_web";
|
||||||
import { setPolyfillsOnLoad } from "./js-polyfills";
|
import { setPolyfillsOnLoad } from "./js-polyfills";
|
||||||
import { publicPath } from "./public-path";
|
import { publicPath } from "./public-path";
|
||||||
import { BaseLoadOptions } from "./load-options";
|
import { BaseLoadOptions } from "./load-options";
|
||||||
import { RufflePlayer } from "./ruffle-player";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
let __webpack_public_path__: string;
|
let __webpack_public_path__: string;
|
||||||
|
@ -30,13 +29,13 @@ type ProgressCallback = (bytesLoaded: number, bytesTotal: number) => void;
|
||||||
*
|
*
|
||||||
* @param config The `window.RufflePlayer.config` object.
|
* @param config The `window.RufflePlayer.config` object.
|
||||||
* @param progressCallback The callback that will be run with Ruffle's download progress.
|
* @param progressCallback The callback that will be run with Ruffle's download progress.
|
||||||
* @returns A ruffle constructor that may be used to create new Ruffle
|
* @returns A ruffle-builder constructor that may be used to create new RuffleInstanceBuilder
|
||||||
* instances.
|
* instances.
|
||||||
*/
|
*/
|
||||||
async function fetchRuffle(
|
async function fetchRuffle(
|
||||||
config: BaseLoadOptions,
|
config: BaseLoadOptions,
|
||||||
progressCallback?: ProgressCallback,
|
progressCallback?: ProgressCallback,
|
||||||
): Promise<typeof RuffleHandle> {
|
): Promise<typeof RuffleInstanceBuilder> {
|
||||||
// Apply some pure JavaScript polyfills to prevent conflicts with external
|
// Apply some pure JavaScript polyfills to prevent conflicts with external
|
||||||
// libraries, if needed.
|
// libraries, if needed.
|
||||||
setPolyfillsOnLoad();
|
setPolyfillsOnLoad();
|
||||||
|
@ -66,7 +65,7 @@ async function fetchRuffle(
|
||||||
|
|
||||||
// Note: The argument passed to import() has to be a simple string literal,
|
// Note: The argument passed to import() has to be a simple string literal,
|
||||||
// otherwise some bundler will get confused and won't include the module?
|
// otherwise some bundler will get confused and won't include the module?
|
||||||
const { default: init, RuffleHandle } = await (extensionsSupported
|
const { default: init, RuffleInstanceBuilder } = await (extensionsSupported
|
||||||
? import("../dist/ruffle_web-wasm_extensions")
|
? import("../dist/ruffle_web-wasm_extensions")
|
||||||
: import("../dist/ruffle_web"));
|
: import("../dist/ruffle_web"));
|
||||||
let response;
|
let response;
|
||||||
|
@ -114,32 +113,28 @@ async function fetchRuffle(
|
||||||
|
|
||||||
await init(response);
|
await init(response);
|
||||||
|
|
||||||
return RuffleHandle;
|
return RuffleInstanceBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nativeConstructor: Promise<typeof RuffleHandle> | null = null;
|
let nativeConstructor: Promise<typeof RuffleInstanceBuilder> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain an instance of `Ruffle`.
|
* Obtain an instance of `Ruffle`.
|
||||||
*
|
*
|
||||||
* This function returns a promise which yields `Ruffle` asynchronously.
|
* This function returns a promise which yields a new `RuffleInstanceBuilder` asynchronously.
|
||||||
*
|
*
|
||||||
* @param container The container that the resulting canvas will be added to.
|
|
||||||
* @param player The `RufflePlayer` object responsible for this instance of Ruffle.
|
|
||||||
* @param config The `window.RufflePlayer.config` object.
|
* @param config The `window.RufflePlayer.config` object.
|
||||||
* @param progressCallback The callback that will be run with Ruffle's download progress.
|
* @param progressCallback The callback that will be run with Ruffle's download progress.
|
||||||
* @returns A ruffle instance.
|
* @returns A ruffle instance builder.
|
||||||
*/
|
*/
|
||||||
export async function loadRuffle(
|
export async function createRuffleBuilder(
|
||||||
container: HTMLElement,
|
|
||||||
player: RufflePlayer,
|
|
||||||
config: BaseLoadOptions,
|
config: BaseLoadOptions,
|
||||||
progressCallback?: ProgressCallback,
|
progressCallback?: ProgressCallback,
|
||||||
): Promise<RuffleHandle> {
|
): Promise<RuffleInstanceBuilder> {
|
||||||
if (nativeConstructor === null) {
|
if (nativeConstructor === null) {
|
||||||
nativeConstructor = fetchRuffle(config, progressCallback);
|
nativeConstructor = fetchRuffle(config, progressCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
const constructor = await nativeConstructor;
|
const constructor = await nativeConstructor;
|
||||||
return new constructor(container, player, config);
|
return new constructor();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { RuffleHandle } from "../dist/ruffle_web";
|
import type { RuffleHandle } from "../dist/ruffle_web";
|
||||||
import { loadRuffle } from "./load-ruffle";
|
import { createRuffleBuilder } from "./load-ruffle";
|
||||||
import { applyStaticStyles, ruffleShadowTemplate } from "./shadow-template";
|
import { applyStaticStyles, ruffleShadowTemplate } from "./shadow-template";
|
||||||
import { lookupElement } from "./register-element";
|
import { lookupElement } from "./register-element";
|
||||||
import { DEFAULT_CONFIG } from "./config";
|
import { DEFAULT_CONFIG } from "./config";
|
||||||
|
@ -17,6 +17,7 @@ import { buildInfo } from "./build-info";
|
||||||
import { text, textAsParagraphs } from "./i18n";
|
import { text, textAsParagraphs } from "./i18n";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { isExtension } from "./current-script";
|
import { isExtension } from "./current-script";
|
||||||
|
import { configureBuilder } from "./internal/builder";
|
||||||
|
|
||||||
const RUFFLE_ORIGIN = "https://ruffle.rs";
|
const RUFFLE_ORIGIN = "https://ruffle.rs";
|
||||||
const DIMENSION_REGEX = /^\s*(\d+(\.\d+)?(%)?)/;
|
const DIMENSION_REGEX = /^\s*(\d+(\.\d+)?(%)?)/;
|
||||||
|
@ -672,9 +673,7 @@ export class RufflePlayer extends HTMLElement {
|
||||||
'The configuration option contextMenu no longer takes a boolean. Use "on", "off", or "rightClickOnly".',
|
'The configuration option contextMenu no longer takes a boolean. Use "on", "off", or "rightClickOnly".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.instance = await loadRuffle(
|
const builder = await createRuffleBuilder(
|
||||||
this.container,
|
|
||||||
this,
|
|
||||||
this.loadedConfig || {},
|
this.loadedConfig || {},
|
||||||
this.onRuffleDownloadProgress.bind(this),
|
this.onRuffleDownloadProgress.bind(this),
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
|
@ -715,6 +714,12 @@ export class RufflePlayer extends HTMLElement {
|
||||||
this.panic(e);
|
this.panic(e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
configureBuilder(builder, this.loadedConfig || {});
|
||||||
|
this.instance = await builder.build(this.container, this).catch((e) => {
|
||||||
|
console.error(`Serious error loading Ruffle: ${e}`);
|
||||||
|
this.panic(e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
if (this.loadedConfig?.fontSources) {
|
if (this.loadedConfig?.fontSources) {
|
||||||
for (const url of this.loadedConfig.fontSources) {
|
for (const url of this.loadedConfig.fontSources) {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { strict as assert } from "assert";
|
||||||
|
import { parseColor, parseDuration } from "../src/internal/builder";
|
||||||
|
|
||||||
|
describe("Color parsing", function () {
|
||||||
|
it("should parse a valid RRGGBB hex, with hash", function () {
|
||||||
|
assert.strictEqual(parseColor("#A1B2C3"), 0xa1b2c3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse a valid RRGGBB hex, without hash", function () {
|
||||||
|
assert.strictEqual(parseColor("#1A2B3C"), 0x1a2b3c);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with not enough digits", function () {
|
||||||
|
assert.strictEqual(parseColor("123"), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should treat invalid hex as 0", function () {
|
||||||
|
assert.strictEqual(parseColor("#AX2Y3Z"), 0xa02030);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Duration parsing", function () {
|
||||||
|
it("should accept number of seconds as number", function () {
|
||||||
|
assert.strictEqual(parseDuration(12.3), 12.3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept a legacy style duration", function () {
|
||||||
|
assert.strictEqual(parseDuration({ secs: 12.3, nanos: 400000 }), 12.3);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,278 @@
|
||||||
|
use crate::{set_panic_hook, JavascriptPlayer, RuffleHandle, SocketProxy, RUFFLE_GLOBAL_PANIC};
|
||||||
|
use js_sys::Promise;
|
||||||
|
use ruffle_core::backend::navigator::OpenURLMode;
|
||||||
|
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
|
||||||
|
use ruffle_core::{Color, PlayerRuntime, StageAlign, StageScaleMode};
|
||||||
|
use ruffle_render::quality::StageQuality;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::HtmlElement;
|
||||||
|
|
||||||
|
#[wasm_bindgen(inspectable)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RuffleInstanceBuilder {
|
||||||
|
pub(crate) allow_script_access: bool,
|
||||||
|
pub(crate) background_color: Option<Color>,
|
||||||
|
pub(crate) letterbox: Letterbox,
|
||||||
|
pub(crate) upgrade_to_https: bool,
|
||||||
|
pub(crate) compatibility_rules: bool,
|
||||||
|
pub(crate) base_url: Option<String>,
|
||||||
|
pub(crate) show_menu: bool,
|
||||||
|
pub(crate) allow_fullscreen: bool,
|
||||||
|
pub(crate) stage_align: StageAlign,
|
||||||
|
pub(crate) force_align: bool,
|
||||||
|
pub(crate) quality: Option<StageQuality>,
|
||||||
|
pub(crate) scale: Option<StageScaleMode>,
|
||||||
|
pub(crate) force_scale: bool,
|
||||||
|
pub(crate) frame_rate: Option<f64>,
|
||||||
|
pub(crate) wmode: Option<String>, // TODO: Enumify? `Player` is working in strings here too...
|
||||||
|
pub(crate) log_level: tracing::Level,
|
||||||
|
pub(crate) max_execution_duration: Duration,
|
||||||
|
pub(crate) player_version: Option<u8>,
|
||||||
|
pub(crate) preferred_renderer: Option<String>, // TODO: Enumify?
|
||||||
|
pub(crate) open_url_mode: OpenURLMode,
|
||||||
|
pub(crate) allow_networking: NetworkingAccessMode,
|
||||||
|
pub(crate) socket_proxy: Vec<SocketProxy>,
|
||||||
|
pub(crate) credential_allow_list: Vec<String>,
|
||||||
|
pub(crate) player_runtime: PlayerRuntime,
|
||||||
|
// TODO: Add font related options
|
||||||
|
// TODO: Add volume
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RuffleInstanceBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Anything available in `BaseLoadOptions` should match the default we list in the docs there.
|
||||||
|
// Some options may be variable (eg allowScriptAccess based on URL) -
|
||||||
|
// those should be always overriding these values in JS
|
||||||
|
|
||||||
|
Self {
|
||||||
|
allow_script_access: false,
|
||||||
|
background_color: None,
|
||||||
|
letterbox: Letterbox::Fullscreen,
|
||||||
|
upgrade_to_https: true,
|
||||||
|
compatibility_rules: true,
|
||||||
|
base_url: None,
|
||||||
|
show_menu: true,
|
||||||
|
allow_fullscreen: false,
|
||||||
|
stage_align: StageAlign::empty(),
|
||||||
|
force_align: false,
|
||||||
|
quality: None,
|
||||||
|
scale: None,
|
||||||
|
force_scale: false,
|
||||||
|
frame_rate: None,
|
||||||
|
wmode: None,
|
||||||
|
log_level: tracing::Level::ERROR,
|
||||||
|
max_execution_duration: Duration::from_secs_f64(15.0),
|
||||||
|
player_version: None,
|
||||||
|
preferred_renderer: None,
|
||||||
|
open_url_mode: OpenURLMode::Allow,
|
||||||
|
allow_networking: NetworkingAccessMode::All,
|
||||||
|
socket_proxy: vec![],
|
||||||
|
credential_allow_list: vec![],
|
||||||
|
player_runtime: PlayerRuntime::FlashPlayer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl RuffleInstanceBuilder {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setAllowScriptAccess")]
|
||||||
|
pub fn set_allow_script_access(&mut self, value: bool) {
|
||||||
|
self.allow_script_access = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setBackgroundColor")]
|
||||||
|
pub fn set_background_color(&mut self, value: Option<u32>) {
|
||||||
|
self.background_color = value.map(|rgb| Color::from_rgb(rgb, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setUpgradeToHttps")]
|
||||||
|
pub fn set_upgrade_to_https(&mut self, value: bool) {
|
||||||
|
self.upgrade_to_https = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setCompatibilityRules")]
|
||||||
|
pub fn set_compatibility_rules(&mut self, value: bool) {
|
||||||
|
self.compatibility_rules = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setLetterbox")]
|
||||||
|
pub fn set_letterbox(&mut self, value: &str) {
|
||||||
|
self.letterbox = match value {
|
||||||
|
"off" => Letterbox::Off,
|
||||||
|
"fullscreen" => Letterbox::Fullscreen,
|
||||||
|
"on" => Letterbox::On,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setBaseUrl")]
|
||||||
|
pub fn set_base_url(&mut self, value: Option<String>) {
|
||||||
|
self.base_url = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setShowMenu")]
|
||||||
|
pub fn set_show_menu(&mut self, value: bool) {
|
||||||
|
self.show_menu = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setAllowFullscreen")]
|
||||||
|
pub fn set_allow_fullscreen(&mut self, value: bool) {
|
||||||
|
self.allow_fullscreen = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setStageAlign")]
|
||||||
|
pub fn set_stage_align(&mut self, value: &str) {
|
||||||
|
// [NA] This is weird. Do we really need this?
|
||||||
|
|
||||||
|
// Chars get converted into flags.
|
||||||
|
// This means "tbbtlbltblbrllrbltlrtbl" is valid, resulting in "TBLR".
|
||||||
|
let mut align = StageAlign::default();
|
||||||
|
for c in value.bytes().map(|c| c.to_ascii_uppercase()) {
|
||||||
|
match c {
|
||||||
|
b'T' => align.insert(StageAlign::TOP),
|
||||||
|
b'B' => align.insert(StageAlign::BOTTOM),
|
||||||
|
b'L' => align.insert(StageAlign::LEFT),
|
||||||
|
b'R' => align.insert(StageAlign::RIGHT),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.stage_align = align;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setForceAlign")]
|
||||||
|
pub fn set_force_align(&mut self, value: bool) {
|
||||||
|
self.force_align = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setQuality")]
|
||||||
|
pub fn set_quality(&mut self, value: &str) {
|
||||||
|
self.quality = match value {
|
||||||
|
"low" => Some(StageQuality::Low),
|
||||||
|
"medium" => Some(StageQuality::Medium),
|
||||||
|
"high" => Some(StageQuality::High),
|
||||||
|
"best" => Some(StageQuality::Best),
|
||||||
|
"8x8" => Some(StageQuality::High8x8),
|
||||||
|
"8x8linear" => Some(StageQuality::High8x8Linear),
|
||||||
|
"16x16" => Some(StageQuality::High16x16),
|
||||||
|
"16x16linear" => Some(StageQuality::High16x16Linear),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setScale")]
|
||||||
|
pub fn set_scale(&mut self, value: &str) {
|
||||||
|
self.scale = match value {
|
||||||
|
"exactfit" => Some(StageScaleMode::ExactFit),
|
||||||
|
"noborder" => Some(StageScaleMode::NoBorder),
|
||||||
|
"noscale" => Some(StageScaleMode::NoScale),
|
||||||
|
"showall" => Some(StageScaleMode::ShowAll),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setForceScale")]
|
||||||
|
pub fn set_force_scale(&mut self, value: bool) {
|
||||||
|
self.force_scale = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setFrameRate")]
|
||||||
|
pub fn set_frame_rate(&mut self, value: Option<f64>) {
|
||||||
|
self.frame_rate = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setWmode")]
|
||||||
|
pub fn set_wmode(&mut self, value: Option<String>) {
|
||||||
|
self.wmode = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setLogLevel")]
|
||||||
|
pub fn set_log_level(&mut self, value: &str) {
|
||||||
|
if let Ok(level) = tracing::Level::from_str(value) {
|
||||||
|
self.log_level = level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setMaxExecutionDuration")]
|
||||||
|
pub fn set_max_execution_duration(&mut self, value: f64) {
|
||||||
|
self.max_execution_duration = Duration::from_secs_f64(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setPlayerVersion")]
|
||||||
|
pub fn set_player_version(&mut self, value: Option<u8>) {
|
||||||
|
self.player_version = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setPreferredRenderer")]
|
||||||
|
pub fn set_preferred_renderer(&mut self, value: Option<String>) {
|
||||||
|
self.preferred_renderer = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setOpenUrlMode")]
|
||||||
|
pub fn set_open_url_mode(&mut self, value: &str) {
|
||||||
|
self.open_url_mode = match value {
|
||||||
|
"allow" => OpenURLMode::Allow,
|
||||||
|
"confirm" => OpenURLMode::Confirm,
|
||||||
|
"deny" => OpenURLMode::Deny,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setAllowNetworking")]
|
||||||
|
pub fn set_allow_networking(&mut self, value: &str) {
|
||||||
|
self.allow_networking = match value {
|
||||||
|
"all" => NetworkingAccessMode::All,
|
||||||
|
"internal" => NetworkingAccessMode::Internal,
|
||||||
|
"none" => NetworkingAccessMode::None,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "addSocketProxy")]
|
||||||
|
pub fn add_socket_proxy(&mut self, host: String, port: u16, proxy_url: String) {
|
||||||
|
self.socket_proxy.push(SocketProxy {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
proxy_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setCredentialAllowList")]
|
||||||
|
pub fn set_credential_allow_list(&mut self, value: Vec<String>) {
|
||||||
|
self.credential_allow_list = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "setPlayerRuntime")]
|
||||||
|
pub fn set_player_runtime(&mut self, value: &str) {
|
||||||
|
self.player_runtime = match value {
|
||||||
|
"air" => PlayerRuntime::AIR,
|
||||||
|
"flashPlayer" => PlayerRuntime::FlashPlayer,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This should be split into two methods that either load url or load data
|
||||||
|
// Right now, that's done immediately afterwards in TS
|
||||||
|
pub async fn build(&self, parent: HtmlElement, js_player: JavascriptPlayer) -> Promise {
|
||||||
|
let copy = self.clone();
|
||||||
|
wasm_bindgen_futures::future_to_promise(async move {
|
||||||
|
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_hook();
|
||||||
|
|
||||||
|
let ruffle = RuffleHandle::new_internal(parent, js_player, copy)
|
||||||
|
.await
|
||||||
|
.map_err(|err| JsValue::from(format!("Error creating player: {}", err)))?;
|
||||||
|
Ok(JsValue::from(ruffle))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
278
web/src/lib.rs
278
web/src/lib.rs
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
//! Ruffle web frontend.
|
//! Ruffle web frontend.
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod builder;
|
||||||
mod external_interface;
|
mod external_interface;
|
||||||
mod input;
|
mod input;
|
||||||
mod log_adapter;
|
mod log_adapter;
|
||||||
|
@ -10,20 +11,19 @@ mod navigator;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
use crate::builder::RuffleInstanceBuilder;
|
||||||
use external_interface::{external_to_js_value, js_to_external_value, JavascriptInterface};
|
use external_interface::{external_to_js_value, js_to_external_value, JavascriptInterface};
|
||||||
use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control};
|
use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control};
|
||||||
use js_sys::{Error as JsError, Promise, Uint8Array};
|
use js_sys::{Error as JsError, Uint8Array};
|
||||||
use ruffle_core::backend::navigator::OpenURLMode;
|
|
||||||
use ruffle_core::backend::ui::FontDefinition;
|
use ruffle_core::backend::ui::FontDefinition;
|
||||||
use ruffle_core::compatibility_rules::CompatibilityRules;
|
use ruffle_core::compatibility_rules::CompatibilityRules;
|
||||||
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
|
use ruffle_core::config::NetworkingAccessMode;
|
||||||
use ruffle_core::context::UpdateContext;
|
use ruffle_core::context::UpdateContext;
|
||||||
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
|
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
|
||||||
use ruffle_core::tag_utils::SwfMovie;
|
use ruffle_core::tag_utils::SwfMovie;
|
||||||
use ruffle_core::{swf, DefaultFont};
|
use ruffle_core::{swf, DefaultFont};
|
||||||
use ruffle_core::{
|
use ruffle_core::{
|
||||||
Color, Player, PlayerBuilder, PlayerEvent, PlayerRuntime, SandboxType, StageAlign,
|
Player, PlayerBuilder, PlayerEvent, SandboxType, StaticCallstack, ViewportDimensions,
|
||||||
StageScaleMode, StaticCallstack, ViewportDimensions,
|
|
||||||
};
|
};
|
||||||
use ruffle_render::quality::StageQuality;
|
use ruffle_render::quality::StageQuality;
|
||||||
use ruffle_video_software::backend::SoftwareVideoBackend;
|
use ruffle_video_software::backend::SoftwareVideoBackend;
|
||||||
|
@ -34,7 +34,6 @@ use std::rc::Rc;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
|
||||||
use std::{cell::RefCell, error::Error, num::NonZeroI32};
|
use std::{cell::RefCell, error::Error, num::NonZeroI32};
|
||||||
use tracing_subscriber::layer::{Layered, SubscriberExt};
|
use tracing_subscriber::layer::{Layered, SubscriberExt};
|
||||||
use tracing_subscriber::registry::Registry;
|
use tracing_subscriber::registry::Registry;
|
||||||
|
@ -188,129 +187,7 @@ extern "C" {
|
||||||
fn display_unsupported_video(this: &JavascriptPlayer, url: &str);
|
fn display_unsupported_video(this: &JavascriptPlayer, url: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_color<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let color: Option<String> = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
let color = match color {
|
|
||||||
Some(color) => color,
|
|
||||||
None => return Ok(None),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse classic HTML hex color (XXXXXX or #XXXXXX), attempting to match browser behavior.
|
|
||||||
// Optional leading #.
|
|
||||||
let color = color.strip_prefix('#').unwrap_or(&color);
|
|
||||||
|
|
||||||
// Fail if less than 6 digits.
|
|
||||||
let color = match color.get(..6) {
|
|
||||||
Some(color) => color,
|
|
||||||
None => return Ok(None),
|
|
||||||
};
|
|
||||||
|
|
||||||
let rgb = color.chars().fold(0, |acc, c| {
|
|
||||||
// Each char represents 4-bits. Invalid hex digit is allowed (converts to 0).
|
|
||||||
let digit = c.to_digit(16).unwrap_or_default();
|
|
||||||
(acc << 4) | digit
|
|
||||||
});
|
|
||||||
Ok(Some(Color::from_rgb(rgb, 255)))
|
|
||||||
}
|
|
||||||
|
|
||||||
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(Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_player_runtime<'de, D>(deserializer: D) -> Result<PlayerRuntime, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let value: String = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
Ok(match value.as_str() {
|
|
||||||
"air" => PlayerRuntime::AIR,
|
|
||||||
"flashPlayer" => PlayerRuntime::FlashPlayer,
|
|
||||||
_ => Default::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::de::Error;
|
|
||||||
|
|
||||||
struct DurationVisitor;
|
|
||||||
|
|
||||||
impl<'de> serde::de::Visitor<'de> for DurationVisitor {
|
|
||||||
type Value = Duration;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str(
|
|
||||||
"Either a non-negative number (indicating seconds) or a {secs: number, nanos: number}."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
Ok(Duration::from_secs_f64(if value < 1 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
value as f64
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
Ok(Duration::from_secs_f64(if value.is_nan() || value < 1.0 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: serde::de::MapAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut secs = None;
|
|
||||||
let mut nanos = None;
|
|
||||||
while let Some(key) = map.next_key::<String>()? {
|
|
||||||
let key_s = key.as_str();
|
|
||||||
|
|
||||||
match key_s {
|
|
||||||
"secs" => {
|
|
||||||
if secs.is_some() {
|
|
||||||
return Err(Error::duplicate_field("secs"));
|
|
||||||
}
|
|
||||||
secs = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
"nanos" => {
|
|
||||||
if nanos.is_some() {
|
|
||||||
return Err(Error::duplicate_field("nanos"));
|
|
||||||
}
|
|
||||||
nanos = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
_ => return Err(Error::unknown_field(key_s, &["secs", "nanos"])),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let secs = secs.ok_or_else(|| Error::missing_field("secs"))?;
|
|
||||||
let nanos = nanos.ok_or_else(|| Error::missing_field("nanos"))?;
|
|
||||||
Ok(Duration::new(secs, nanos))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(DurationVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SocketProxy {
|
pub struct SocketProxy {
|
||||||
host: String,
|
host: String,
|
||||||
|
@ -319,64 +196,6 @@ pub struct SocketProxy {
|
||||||
proxy_url: String,
|
proxy_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Config {
|
|
||||||
allow_script_access: bool,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "deserialize_color")]
|
|
||||||
background_color: Option<Color>,
|
|
||||||
|
|
||||||
letterbox: Letterbox,
|
|
||||||
|
|
||||||
upgrade_to_https: bool,
|
|
||||||
|
|
||||||
compatibility_rules: bool,
|
|
||||||
|
|
||||||
#[serde(rename = "base")]
|
|
||||||
base_url: Option<String>,
|
|
||||||
|
|
||||||
#[serde(rename = "menu")]
|
|
||||||
show_menu: bool,
|
|
||||||
|
|
||||||
allow_fullscreen: bool,
|
|
||||||
|
|
||||||
salign: Option<String>,
|
|
||||||
|
|
||||||
force_align: bool,
|
|
||||||
|
|
||||||
quality: Option<String>,
|
|
||||||
|
|
||||||
scale: Option<String>,
|
|
||||||
|
|
||||||
force_scale: bool,
|
|
||||||
|
|
||||||
frame_rate: Option<f64>,
|
|
||||||
|
|
||||||
wmode: Option<String>,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "deserialize_log_level")]
|
|
||||||
log_level: tracing::Level,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "deserialize_duration")]
|
|
||||||
max_execution_duration: Duration,
|
|
||||||
|
|
||||||
player_version: Option<u8>,
|
|
||||||
|
|
||||||
preferred_renderer: Option<String>,
|
|
||||||
|
|
||||||
open_url_mode: OpenURLMode,
|
|
||||||
|
|
||||||
allow_networking: NetworkingAccessMode,
|
|
||||||
|
|
||||||
socket_proxy: Vec<SocketProxy>,
|
|
||||||
|
|
||||||
credential_allow_list: Vec<String>,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "deserialize_player_runtime")]
|
|
||||||
player_runtime: PlayerRuntime,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata about the playing SWF file to be passed back to JavaScript.
|
/// Metadata about the playing SWF file to be passed back to JavaScript.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -394,27 +213,6 @@ struct MovieMetadata {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl RuffleHandle {
|
impl RuffleHandle {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
|
||||||
#[wasm_bindgen(constructor)]
|
|
||||||
pub fn new(parent: HtmlElement, js_player: JavascriptPlayer, config: JsValue) -> Promise {
|
|
||||||
wasm_bindgen_futures::future_to_promise(async move {
|
|
||||||
let config: Config = serde_wasm_bindgen::from_value(config)
|
|
||||||
.map_err(|e| format!("Error parsing config: {e}"))?;
|
|
||||||
|
|
||||||
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_hook();
|
|
||||||
|
|
||||||
let ruffle = Self::new_internal(parent, js_player, config)
|
|
||||||
.await
|
|
||||||
.map_err(|err| JsValue::from(format!("Error creating player: {}", err)))?;
|
|
||||||
Ok(JsValue::from(ruffle))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stream an arbitrary movie file from (presumably) the Internet.
|
/// Stream an arbitrary movie file from (presumably) the Internet.
|
||||||
///
|
///
|
||||||
/// This method should only be called once per player.
|
/// This method should only be called once per player.
|
||||||
|
@ -646,7 +444,7 @@ impl RuffleHandle {
|
||||||
async fn new_internal(
|
async fn new_internal(
|
||||||
parent: HtmlElement,
|
parent: HtmlElement,
|
||||||
js_player: JavascriptPlayer,
|
js_player: JavascriptPlayer,
|
||||||
config: Config,
|
config: RuffleInstanceBuilder,
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
// Redirect Log to Tracing if it isn't already
|
// Redirect Log to Tracing if it isn't already
|
||||||
let _ = tracing_log::LogTracer::builder()
|
let _ = tracing_log::LogTracer::builder()
|
||||||
|
@ -730,63 +528,9 @@ impl RuffleHandle {
|
||||||
} else {
|
} else {
|
||||||
CompatibilityRules::empty()
|
CompatibilityRules::empty()
|
||||||
})
|
})
|
||||||
.with_quality(
|
.with_quality(config.quality.unwrap_or(default_quality))
|
||||||
config
|
.with_align(config.stage_align, config.force_align)
|
||||||
.quality
|
.with_scale_mode(config.scale.unwrap_or_default(), config.force_scale)
|
||||||
.and_then(|q| {
|
|
||||||
let quality = match q.to_ascii_lowercase().as_str() {
|
|
||||||
"low" => StageQuality::Low,
|
|
||||||
"medium" => StageQuality::Medium,
|
|
||||||
"high" => StageQuality::High,
|
|
||||||
"best" => StageQuality::Best,
|
|
||||||
"8x8" => StageQuality::High8x8,
|
|
||||||
"8x8linear" => StageQuality::High8x8Linear,
|
|
||||||
"16x16" => StageQuality::High16x16,
|
|
||||||
"16x16linear" => StageQuality::High16x16Linear,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(quality)
|
|
||||||
})
|
|
||||||
.unwrap_or(default_quality),
|
|
||||||
)
|
|
||||||
.with_align(
|
|
||||||
config
|
|
||||||
.salign
|
|
||||||
.map(|s| {
|
|
||||||
// Chars get converted into flags.
|
|
||||||
// This means "tbbtlbltblbrllrbltlrtbl" is valid, resulting in "TBLR".
|
|
||||||
let mut align = StageAlign::default();
|
|
||||||
for c in s.bytes().map(|c| c.to_ascii_uppercase()) {
|
|
||||||
match c {
|
|
||||||
b'T' => align.insert(StageAlign::TOP),
|
|
||||||
b'B' => align.insert(StageAlign::BOTTOM),
|
|
||||||
b'L' => align.insert(StageAlign::LEFT),
|
|
||||||
b'R' => align.insert(StageAlign::RIGHT),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
align
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
config.force_align,
|
|
||||||
)
|
|
||||||
.with_scale_mode(
|
|
||||||
config
|
|
||||||
.scale
|
|
||||||
.and_then(|s| {
|
|
||||||
let scale_mode = match s.to_ascii_lowercase().as_str() {
|
|
||||||
"exactfit" => StageScaleMode::ExactFit,
|
|
||||||
"noborder" => StageScaleMode::NoBorder,
|
|
||||||
"noscale" => StageScaleMode::NoScale,
|
|
||||||
"showall" => StageScaleMode::ShowAll,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
Some(scale_mode)
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
config.force_scale,
|
|
||||||
)
|
|
||||||
.with_frame_rate(config.frame_rate)
|
.with_frame_rate(config.frame_rate)
|
||||||
// FIXME - should this be configurable?
|
// FIXME - should this be configurable?
|
||||||
.with_sandbox_type(SandboxType::Remote)
|
.with_sandbox_type(SandboxType::Remote)
|
||||||
|
@ -1421,7 +1165,7 @@ pub enum RuffleInstanceError {
|
||||||
async fn create_renderer(
|
async fn create_renderer(
|
||||||
builder: PlayerBuilder,
|
builder: PlayerBuilder,
|
||||||
document: &web_sys::Document,
|
document: &web_sys::Document,
|
||||||
config: &Config,
|
config: &RuffleInstanceBuilder,
|
||||||
) -> Result<(PlayerBuilder, HtmlCanvasElement), Box<dyn Error>> {
|
) -> Result<(PlayerBuilder, HtmlCanvasElement), Box<dyn Error>> {
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
feature = "canvas",
|
feature = "canvas",
|
||||||
|
|
Loading…
Reference in New Issue