Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
[target.'cfg(all())']
# NOTE that the web build overrides this setting in package.json via the RUSTFLAGS environment variable
rustflags = [
# We need to specify this flag for all targets because Clippy checks all of our code against all targets
# and our web code does not compile without this flag
"--cfg=web_sys_unstable_apis",
]

[target.x86_64-pc-windows-msvc]
# Use the LLD linker, it should be faster than the default.
# See: https://github.com/rust-lang/rust/issues/71520
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
# Based on: https://github.com/dtolnay/linkme/pull/88
- name: Disable rust-lld
if: matrix.rust_version == 'nightly'
run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zlinker-features=-lld >> $GITHUB_ENV
run: echo RUSTFLAGS=${RUSTFLAGS}\ --cfg=web_sys_unstable_apis\ -Zlinker-features=-lld >> $GITHUB_ENV

- name: Cache Cargo output
uses: Swatinem/rust-cache@v2
Expand Down
2 changes: 1 addition & 1 deletion web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ features = [
"EventTarget", "GainNode", "Headers", "HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement",
"HtmlInputElement", "HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent",
"Request", "RequestInit", "Response", "Storage", "WheelEvent", "Window", "ReadableStream", "RequestCredentials",
"Url",
"Url", "Clipboard",
]

[package.metadata.cargo-machete]
Expand Down
36 changes: 33 additions & 3 deletions web/packages/core/src/ruffle-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ interface ContextMenuItem {
* The function to call when clicked.
*
* @param event The mouse event that triggered the click.
*
* @returns Either `void` in case the callback is synchronous,
* or `Promise<void>` when the callback is asynchronous.
*/
onClick: (event: MouseEvent) => void;
onClick: (event: MouseEvent) => void | Promise<void>;

/**
* Whether this item is clickable.
Expand Down Expand Up @@ -146,6 +149,7 @@ export class RufflePlayer extends HTMLElement {
private readonly volumeControls: HTMLDivElement;
private readonly videoModal: HTMLDivElement;
private readonly hardwareAccelerationModal: HTMLDivElement;
private readonly clipboardModal: HTMLDivElement;

private readonly contextMenuOverlay: HTMLElement;
// Firefox has a read-only "contextMenu" property,
Expand Down Expand Up @@ -271,10 +275,14 @@ export class RufflePlayer extends HTMLElement {
this.volumeControls = <HTMLDivElement>(
this.shadow.getElementById("volume-controls-modal")
);
this.clipboardModal = <HTMLDivElement>(
this.shadow.getElementById("clipboard-modal")
);
this.addModalJavaScript(this.saveManager);
this.addModalJavaScript(this.volumeControls);
this.addModalJavaScript(this.videoModal);
this.addModalJavaScript(this.hardwareAccelerationModal);
this.addModalJavaScript(this.clipboardModal);

this.volumeSettings = new VolumeControls(false, 100);
this.addVolumeControlsJavaScript(this.volumeControls);
Expand Down Expand Up @@ -1543,7 +1551,9 @@ export class RufflePlayer extends HTMLElement {
addSeparator();
items.push({
text: text("context-menu-hide"),
onClick: () => (this.contextMenuForceDisabled = true),
onClick: () => {
this.contextMenuForceDisabled = true;
},
});
}
return items;
Expand Down Expand Up @@ -1663,7 +1673,23 @@ export class RufflePlayer extends HTMLElement {
if (enabled !== false) {
menuItem.addEventListener(
this.contextMenuSupported ? "click" : "pointerup",
onClick,
(event: MouseEvent) => {
// Prevent the menu from being destroyed.
// It's required when we're dealing with async callbacks,
// as the async callback may still use the menu in the future.
event.stopPropagation();

const result = onClick(event);

// Then we have to close the context menu manually after the callback finishes.
if (result instanceof Promise) {
result.then(() => {
this.hideContextMenu();
});
} else {
this.hideContextMenu();
}
},
);
} else {
menuItem.classList.add("disabled");
Expand Down Expand Up @@ -2354,6 +2380,10 @@ export class RufflePlayer extends HTMLElement {
}
}

protected displayClipboardModal(): void {
this.clipboardModal.classList.remove("hidden");
}

protected debugPlayerInfo(): string {
return "";
}
Expand Down
42 changes: 42 additions & 0 deletions web/packages/core/src/shadow-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,33 @@ hardwareModalLink.target = "_blank";
hardwareModalLink.className = "acceleration-link";
hardwareModalLink.textContent = text("enable-hardware-acceleration");

// Clipboard message
const clipboardModal = createElement("div", "clipboard-modal", "modal hidden");
const clipboardModalArea = createElement("div", undefined, "modal-area");
const clipboardModalClose = createElement("span", undefined, "close-modal");
clipboardModalClose.textContent = "\u00D7";
const clipboardModalHeading = createElement("h2", "clipboard-modal-heading");
clipboardModalHeading.textContent = text("clipboard-message-title");
const clipboardModalTextIntro = createElement("p", undefined);
clipboardModalTextIntro.textContent = text("clipboard-message-description");
const shortcutModifier =
navigator.userAgent.indexOf("Mac OS X") === -1 ? "Ctrl" : "Command";
const clipboardModalTextCopy = createElement("p", undefined);
const clipboardModalTextCopyShortcut = createElement("b", undefined);
clipboardModalTextCopyShortcut.textContent = `${shortcutModifier}+C`;
const clipboardModalTextCopyText = createElement("span", undefined);
clipboardModalTextCopyText.textContent = text("clipboard-message-copy");
const clipboardModalTextCut = createElement("p", undefined);
const clipboardModalTextCutShortcut = createElement("b", undefined);
clipboardModalTextCutShortcut.textContent = `${shortcutModifier}+X`;
const clipboardModalTextCutText = createElement("span", undefined);
clipboardModalTextCutText.textContent = text("clipboard-message-cut");
const clipboardModalTextPaste = createElement("p", undefined);
const clipboardModalTextPasteShortcut = createElement("b", undefined);
clipboardModalTextPasteShortcut.textContent = `${shortcutModifier}+V`;
const clipboardModalTextPasteText = createElement("span", undefined);
clipboardModalTextPasteText.textContent = text("clipboard-message-paste");

// Context menu overlay elements
const contextMenuOverlay = createElement(
"div",
Expand Down Expand Up @@ -888,6 +915,21 @@ appendElement(ruffleShadowTemplate.content, hardwareModal);
appendElement(hardwareModal, hardwareModalArea);
appendElement(hardwareModalArea, hardwareModalClose);
appendElement(hardwareModalArea, hardwareModalLink);
// Clipboard modal append
appendElement(ruffleShadowTemplate.content, clipboardModal);
appendElement(clipboardModal, clipboardModalArea);
appendElement(clipboardModalArea, clipboardModalClose);
appendElement(clipboardModalArea, clipboardModalHeading);
appendElement(clipboardModalArea, clipboardModalTextIntro);
appendElement(clipboardModalArea, clipboardModalTextCopy);
appendElement(clipboardModalTextCopy, clipboardModalTextCopyShortcut);
appendElement(clipboardModalTextCopy, clipboardModalTextCopyText);
appendElement(clipboardModalArea, clipboardModalTextCut);
appendElement(clipboardModalTextCut, clipboardModalTextCutShortcut);
appendElement(clipboardModalTextCut, clipboardModalTextCutText);
appendElement(clipboardModalArea, clipboardModalTextPaste);
appendElement(clipboardModalTextPaste, clipboardModalTextPasteShortcut);
appendElement(clipboardModalTextPaste, clipboardModalTextPasteText);
// Context menu overlay append
appendElement(ruffleShadowTemplate.content, contextMenuOverlay);
appendElement(contextMenuOverlay, contextMenu);
5 changes: 5 additions & 0 deletions web/packages/core/texts/en-US/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ enable-hardware-acceleration = It looks like hardware acceleration is not enable
view-error-details = View Error Details
open-in-new-tab = Open in a new tab
click-to-unmute = Click to unmute
clipboard-message-title = Copying and pasting in Ruffle
clipboard-message-description = Your browser does not support full clipboard access, but you can always use these shortcuts instead:
clipboard-message-copy = { " " } for copy
clipboard-message-cut = { " " } for cut
clipboard-message-paste = { " " } for paste
error-file-protocol =
It appears you are running Ruffle on the "file:" protocol.
This doesn't work as browsers block many features from working for security reasons.
Expand Down
2 changes: 1 addition & 1 deletion web/packages/core/tools/build_wasm.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function cargoBuild({ profile, features, rustFlags }) {
});
}
function buildWasm(profile, filename, optimise, extensions, wasmSource) {
const rustFlags = ["-Aunknown_lints"];
const rustFlags = ["--cfg=web_sys_unstable_apis", "-Aunknown_lints"];
const wasmBindgenFlags = [];
const wasmOptFlags = [];
const flavor = extensions ? "extensions" : "vanilla";
Expand Down
84 changes: 72 additions & 12 deletions web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use external_interface::{external_to_js_value, js_to_external_value};
use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control};
use js_sys::{Error as JsError, Uint8Array};
use ruffle_core::context::UpdateContext;
use ruffle_core::context_menu::ContextMenuCallback;
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::{Player, PlayerEvent, StaticCallstack, ViewportDimensions};
Expand Down Expand Up @@ -178,6 +179,9 @@ extern "C" {

#[wasm_bindgen(method, js_name = "displayUnsupportedVideo")]
fn display_unsupported_video(this: &JavascriptPlayer, url: &str);

#[wasm_bindgen(method, js_name = "displayClipboardModal")]
fn display_clipboard_modal(this: &JavascriptPlayer);
}

#[derive(Debug, Deserialize, Clone)]
Expand Down Expand Up @@ -212,7 +216,7 @@ impl RuffleHandle {
///
/// `parameters` are *extra* parameters to set on the LoaderInfo -
/// parameters from `movie_url` query parameters will be automatically added.
pub fn stream_from(&mut self, movie_url: String, parameters: JsValue) -> Result<(), JsValue> {
pub fn stream_from(&self, movie_url: String, parameters: JsValue) -> Result<(), JsValue> {
let _ = self.with_core_mut(|core| {
let parameters_to_load = parse_movie_parameters(&parameters);

Expand All @@ -230,7 +234,7 @@ impl RuffleHandle {
///
/// This method should only be called once per player.
pub fn load_data(
&mut self,
&self,
swf_data: Uint8Array,
parameters: JsValue,
swf_name: String,
Expand Down Expand Up @@ -266,27 +270,27 @@ impl RuffleHandle {
Ok(())
}

pub fn play(&mut self) {
pub fn play(&self) {
let _ = self.with_core_mut(|core| {
core.set_is_playing(true);
});
}

pub fn pause(&mut self) {
pub fn pause(&self) {
let _ = self.with_core_mut(|core| {
core.set_is_playing(false);
});
}

pub fn is_playing(&mut self) -> bool {
pub fn is_playing(&self) -> bool {
self.with_core(|core| core.is_playing()).unwrap_or_default()
}

pub fn volume(&self) -> f32 {
self.with_core(|core| core.volume()).unwrap_or_default()
}

pub fn set_volume(&mut self, value: f32) {
pub fn set_volume(&self, value: f32) {
let _ = self.with_core_mut(|core| core.set_volume(value));
}

Expand All @@ -301,27 +305,83 @@ impl RuffleHandle {
}

// after the context menu is closed, remember to call `clear_custom_menu_items`!
pub fn prepare_context_menu(&mut self) -> JsValue {
pub fn prepare_context_menu(&self) -> JsValue {
self.with_core_mut(|core| {
let info = core.prepare_context_menu();
serde_wasm_bindgen::to_value(&info).unwrap_or(JsValue::UNDEFINED)
})
.unwrap_or(JsValue::UNDEFINED)
}

pub fn run_context_menu_callback(&mut self, index: usize) {
let _ = self.with_core_mut(|core| core.run_context_menu_callback(index));
pub async fn run_context_menu_callback(&self, index: usize) {
let is_paste = self
.with_core_mut(|core| {
let is_paste = core.mutate_with_update_context(|context| {
matches!(
context
.current_context_menu
.as_ref()
.map(|menu| menu.callback(index)),
Some(ContextMenuCallback::TextControl {
code: TextControlCode::Paste,
..
})
)
});
if !is_paste {
core.run_context_menu_callback(index)
}
is_paste
})
.unwrap_or_default();

// When the user selects paste, we need to use the Clipboard API which
// requests the clipboard asynchronously, so that the browser can ask for permission.
if is_paste {
self.run_context_menu_callback_paste(index).await;
}
}

async fn run_context_menu_callback_paste(&self, index: usize) {
let window = web_sys::window().expect("Missing window");
let Some(clipboard) = window.navigator().clipboard() else {
// Clipboard not available, display a message
let _ = self.with_instance(|instance| instance.js_player.display_clipboard_modal());
return;
};

let promise = clipboard.read_text();
tracing::debug!("Requested text from clipboard");
let clipboard = wasm_bindgen_futures::JsFuture::from(promise)
.await
.ok()
.and_then(|v| v.as_string());
let Some(clipboard) = clipboard else {
tracing::warn!("Clipboard permission denied");
return;
};

if !clipboard.is_empty() {
let _ = self.with_core_mut(|core| {
core.mutate_with_update_context(|context| {
context.ui.set_clipboard_content(clipboard);
});
core.run_context_menu_callback(index);
});
} else {
tracing::info!("Clipboard was empty");
}
}

pub fn set_fullscreen(&mut self, is_fullscreen: bool) {
pub fn set_fullscreen(&self, is_fullscreen: bool) {
let _ = self.with_core_mut(|core| core.set_fullscreen(is_fullscreen));
}

pub fn clear_custom_menu_items(&mut self) {
pub fn clear_custom_menu_items(&self) {
let _ = self.with_core_mut(Player::clear_custom_menu_items);
}

pub fn destroy(&mut self) {
pub fn destroy(&self) {
// Remove instance from the active list.
let _ = self.remove_instance();
// Instance is dropped at this point.
Expand Down