Skip to content

Commit

Permalink
Merge branch 'master' into avm1-debug-object-edit
Browse files Browse the repository at this point in the history
  • Loading branch information
crumblingstatue authored Jul 27, 2024
2 parents 8cd17c0 + 96173b0 commit 187ed85
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 55 deletions.
5 changes: 4 additions & 1 deletion core/src/backend/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ pub trait UiBackend: Downcast {
// Unused, but kept in case we need it later.
fn message(&self, message: &str);

// Only used on web.
fn open_virtual_keyboard(&self);

fn close_virtual_keyboard(&self);

fn language(&self) -> LanguageIdentifier;

fn display_unsupported_video(&self, url: Url);
Expand Down Expand Up @@ -360,6 +361,8 @@ impl UiBackend for NullUiBackend {

fn open_virtual_keyboard(&self) {}

fn close_virtual_keyboard(&self) {}

fn language(&self) -> LanguageIdentifier {
US_ENGLISH.clone()
}
Expand Down
24 changes: 19 additions & 5 deletions core/src/focus_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ impl<'gc> FocusTracker<'gc> {
// Mouse focus change events are not dispatched when the object is the same,
// contrary to key focus change events.
if InteractiveObject::option_ptr_eq(old, new) {
// Re-open the keyboard when the user clicked an already focused text field.
self.update_virtual_keyboard(context);
return;
}

Expand Down Expand Up @@ -194,15 +196,27 @@ impl<'gc> FocusTracker<'gc> {
}

// This applies even if the focused element hasn't changed.
if let Some(text_field) = self.get_as_edit_text() {
if text_field.is_editable() && !text_field.movie().is_action_script_3() {
// TODO This logic is inaccurate and addresses
// only setting the focus programmatically.
let length = text_field.text_length();
text_field.set_selection(Some(TextSelection::for_range(0, length)), context.gc());
}
}

self.update_virtual_keyboard(context);
}

fn update_virtual_keyboard(&self, context: &mut UpdateContext<'_, 'gc>) {
if let Some(text_field) = self.get_as_edit_text() {
if text_field.is_editable() {
if !text_field.movie().is_action_script_3() {
let length = text_field.text_length();
text_field
.set_selection(Some(TextSelection::for_range(0, length)), context.gc());
}
context.ui.open_virtual_keyboard();
} else {
context.ui.close_virtual_keyboard();
}
} else {
context.ui.close_virtual_keyboard();
}
}

Expand Down
2 changes: 2 additions & 0 deletions desktop/src/backends/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ impl UiBackend for DesktopUiBackend {
// Unused on desktop
fn open_virtual_keyboard(&self) {}

fn close_virtual_keyboard(&self) {}

fn language(&self) -> LanguageIdentifier {
self.preferences.language().clone()
}
Expand Down
2 changes: 2 additions & 0 deletions tests/framework/src/backends/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ impl UiBackend for TestUiBackend {

fn open_virtual_keyboard(&self) {}

fn close_virtual_keyboard(&self) {}

fn language(&self) -> LanguageIdentifier {
US_ENGLISH.clone()
}
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", "Clipboard",
"Url", "Clipboard", "FocusEvent"
]

[package.metadata.cargo-machete]
Expand Down
1 change: 1 addition & 0 deletions web/packages/core/src/internal/ui/static-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

#container {
overflow: hidden;
outline: none;

canvas {
width: 100%;
Expand Down
46 changes: 33 additions & 13 deletions web/packages/core/src/ruffle-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -755,12 +755,6 @@ export class RufflePlayer extends HTMLElement {
}

this.unmuteAudioContext();
// On Android, the virtual keyboard needs to be dismissed as otherwise it re-focuses when clicking elsewhere
if (navigator.userAgent.toLowerCase().includes("android")) {
this.container.addEventListener("click", () =>
this.virtualKeyboard.blur(),
);
}

// Treat invalid values as `AutoPlay.Auto`.
if (
Expand Down Expand Up @@ -1388,6 +1382,7 @@ export class RufflePlayer extends HTMLElement {
console.error("SWF download failed");
}
}

private virtualKeyboardInput() {
const input = this.virtualKeyboard;
const string = input.value;
Expand All @@ -1403,17 +1398,42 @@ export class RufflePlayer extends HTMLElement {
}
input.value = "";
}

protected openVirtualKeyboard(): void {
// On Android, the Rust code that opens the virtual keyboard triggers
// before the TypeScript code that closes it, so delay opening it
if (navigator.userAgent.toLowerCase().includes("android")) {
setTimeout(() => {
this.virtualKeyboard.focus({ preventScroll: true });
}, 100);
// Virtual keyboard is opened/closed synchronously from core,
// and opening/closing it is basically dispatching
// focus events (which may also be dispatched to the player).
// In order not to deadlock here (or rather throw an error),
// these actions should be performed asynchronously.
// However, some browsers (i.e. Safari) require user interaction
// in order to open the virtual keyboard.
// That is why we are checking whether Ruffle already has focus:
// 1. if it does, no focus events will be dispatched to
// the player when we focus the virtual keyboard, and
// 2. if it doesn't, the action shouldn't be a result of user
// interaction and focusing synchronously wouldn't work anyway.
if (this.instance?.has_focus()) {
this.virtualKeyboard.focus({preventScroll: true});
} else {
this.virtualKeyboard.focus({ preventScroll: true });
setTimeout(() => {
this.virtualKeyboard.focus({preventScroll: true});
}, 0);
}
}

protected closeVirtualKeyboard(): void {
// Note that closing the keyboard is a little tricky, as we cannot
// just remove the focus here, as the player should still be focused.
// We want to switch the focus to the container instead, but the user may also
// click away from the player, and in that case we do not want to re-focus it.
// We also have to take into account that the keyboard may be
// closed even if the player doesn't have focus at all.
// That's why we have to "transfer" the focus from the keyboard to the container.
if (this.isVirtualKeyboardFocused()) {
this.container.focus({ preventScroll: true });
}
}

protected isVirtualKeyboardFocused(): boolean {
return this.shadow.activeElement === this.virtualKeyboard;
}
Expand Down
131 changes: 96 additions & 35 deletions web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ use url::Url;
use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::prelude::*;
use web_sys::{
AddEventListenerOptions, ClipboardEvent, Element, Event, EventTarget, HtmlCanvasElement,
HtmlElement, KeyboardEvent, PointerEvent, WheelEvent, Window,
AddEventListenerOptions, ClipboardEvent, Element, Event, EventTarget, FocusEvent,
HtmlCanvasElement, HtmlElement, KeyboardEvent, Node, PointerEvent, WheelEvent, Window,
};

static RUFFLE_GLOBAL_PANIC: Once = Once::new();
Expand Down Expand Up @@ -125,14 +125,15 @@ struct RuffleInstance {
mouse_enter_callback: Option<JsCallback<PointerEvent>>,
mouse_leave_callback: Option<JsCallback<PointerEvent>>,
mouse_down_callback: Option<JsCallback<PointerEvent>>,
player_mouse_down_callback: Option<JsCallback<PointerEvent>>,
window_mouse_down_callback: Option<JsCallback<PointerEvent>>,
mouse_up_callback: Option<JsCallback<PointerEvent>>,
mouse_wheel_callback: Option<JsCallback<WheelEvent>>,
key_down_callback: Option<JsCallback<KeyboardEvent>>,
key_up_callback: Option<JsCallback<KeyboardEvent>>,
paste_callback: Option<JsCallback<ClipboardEvent>>,
unload_callback: Option<JsCallback<Event>>,
focusin_callback: Option<JsCallback<FocusEvent>>,
focusout_callback: Option<JsCallback<FocusEvent>>,
focus_on_press_callback: Option<JsCallback<PointerEvent>>,
has_focus: bool,
trace_observer: Rc<RefCell<JsValue>>,
log_subscriber: Arc<Layered<WASMLayer, Registry>>,
Expand Down Expand Up @@ -174,6 +175,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = "openVirtualKeyboard")]
fn open_virtual_keyboard(this: &JavascriptPlayer);

#[wasm_bindgen(method, js_name = "closeVirtualKeyboard")]
fn close_virtual_keyboard(this: &JavascriptPlayer);

#[wasm_bindgen(method, js_name = "isVirtualKeyboardFocused")]
fn is_virtual_keyboard_focused(this: &JavascriptPlayer) -> bool;

Expand Down Expand Up @@ -288,6 +292,11 @@ impl RuffleHandle {
self.with_core(|core| core.is_playing()).unwrap_or_default()
}

pub fn has_focus(&self) -> bool {
self.with_instance(|instance| instance.has_focus)
.unwrap_or_default()
}

pub fn volume(&self) -> f32 {
self.with_core(|core| core.volume()).unwrap_or_default()
}
Expand Down Expand Up @@ -476,14 +485,15 @@ impl RuffleHandle {
mouse_enter_callback: None,
mouse_leave_callback: None,
mouse_down_callback: None,
player_mouse_down_callback: None,
window_mouse_down_callback: None,
mouse_up_callback: None,
mouse_wheel_callback: None,
key_down_callback: None,
key_up_callback: None,
paste_callback: None,
unload_callback: None,
focusin_callback: None,
focusout_callback: None,
focus_on_press_callback: None,
timestamp: None,
has_focus: false,
trace_observer: player.trace_observer,
Expand All @@ -500,6 +510,8 @@ impl RuffleHandle {
// Register the instance and create the animation frame closure.
let mut ruffle = Self::add_instance(instance)?;

Self::set_up_focus_management(ruffle, parent)?;

// Create the animation frame closure.
ruffle.with_instance_mut(|instance| {
instance.animation_handler = Some(Closure::new(move |timestamp| {
Expand Down Expand Up @@ -604,35 +616,6 @@ impl RuffleHandle {
},
));

// Create player mouse down handler.
instance.player_mouse_down_callback = Some(JsCallback::register(
&js_player,
"pointerdown",
false,
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();
});
},
));

// Create window mouse down handler.
instance.window_mouse_down_callback = Some(JsCallback::register(
&window,
"pointerdown",
true,
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;
});
},
));

// Create mouse up handler.
instance.mouse_up_callback = Some(JsCallback::register(
&player.canvas,
Expand Down Expand Up @@ -792,6 +775,84 @@ impl RuffleHandle {
Ok(ruffle)
}

fn set_up_focus_management(
ruffle: RuffleHandle,
focus_target: HtmlElement,
) -> Result<(), RuffleInstanceError> {
focus_target.set_tab_index(-1);
ruffle.with_instance_mut(|instance| {
let focus_target_clone = focus_target.clone();
instance.focusin_callback = Some(JsCallback::register(
&focus_target,
"focusin",
false,
move |js_event: FocusEvent| {
if let Some(related_target) = js_event.related_target() {
let related_target = related_target.dyn_ref::<Node>();
if focus_target_clone.contains(related_target) {
// Focus is changed within parent, we can ignore it.
return;
}
}

let _ = ruffle.with_instance_mut(|instance| {
if !instance.has_focus {
instance.has_focus = true;
let _ = instance.with_core_mut(|core| {
core.handle_event(PlayerEvent::FocusGained);
});
}
});
},
));

let focus_target_clone = focus_target.clone();
instance.focusout_callback = Some(JsCallback::register(
&focus_target,
"focusout",
false,
move |js_event: FocusEvent| {
if let Some(related_target) = js_event.related_target() {
let related_target = related_target.dyn_ref::<Node>();
if focus_target_clone.contains(related_target) {
// Focus is changed within parent, we can ignore it.
return;
}
}

let _ = ruffle.with_instance_mut(|instance| {
if instance.has_focus {
let _ = instance.with_core_mut(|core| {
core.handle_event(PlayerEvent::FocusLost);
});
instance.has_focus = false;
}
});
},
));

let focus_target_clone = focus_target.clone();
instance.focus_on_press_callback = Some(JsCallback::register(
&focus_target,
"pointerdown",
// We want to set the focus as early as we can.
true,
move |_js_event| {
let has_focus = ruffle
.with_instance(|instance| instance.has_focus)
.unwrap_or_default();
if has_focus {
// We already have focus, no need to reset it.
return;
}

focus_target_clone.focus().warn_on_error();
},
));
})?;
Ok(())
}

/// Registers a new Ruffle instance and returns the handle to the instance.
fn add_instance(instance: RuffleInstance) -> Result<Self, RuffleInstanceError> {
INSTANCES.try_with(|instances| {
Expand Down
4 changes: 4 additions & 0 deletions web/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ impl UiBackend for WebUiBackend {
self.js_player.open_virtual_keyboard()
}

fn close_virtual_keyboard(&self) {
self.js_player.close_virtual_keyboard()
}

fn language(&self) -> LanguageIdentifier {
self.language.clone()
}
Expand Down

0 comments on commit 187ed85

Please sign in to comment.