From 96173b05010648adbd5e15336a377aa11cb8b2ea Mon Sep 17 00:00:00 2001 From: Kamil Jarosz Date: Wed, 17 Jul 2024 16:33:01 +0200 Subject: [PATCH] web: Improve virtual keyboard support This patch integrates the virtual keyboard with the newly added focus management and removes Android-specific code, instead using generic logic which takes advantage of improved focus support in SWFs. --- core/src/focus_tracker.rs | 22 ++++++++---- web/packages/core/src/ruffle-player.tsx | 46 ++++++++++++++++++------- web/src/lib.rs | 8 +++++ web/src/ui.rs | 4 ++- 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/core/src/focus_tracker.rs b/core/src/focus_tracker.rs index 42f3cec89..f8523432a 100644 --- a/core/src/focus_tracker.rs +++ b/core/src/focus_tracker.rs @@ -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; } @@ -194,15 +196,21 @@ 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() { - // 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()); - } context.ui.open_virtual_keyboard(); } else { context.ui.close_virtual_keyboard(); diff --git a/web/packages/core/src/ruffle-player.tsx b/web/packages/core/src/ruffle-player.tsx index de66ca8b7..2d4a58b50 100644 --- a/web/packages/core/src/ruffle-player.tsx +++ b/web/packages/core/src/ruffle-player.tsx @@ -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 ( @@ -1388,6 +1382,7 @@ export class RufflePlayer extends HTMLElement { console.error("SWF download failed"); } } + private virtualKeyboardInput() { const input = this.virtualKeyboard; const string = input.value; @@ -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; } diff --git a/web/src/lib.rs b/web/src/lib.rs index c592886f8..dd3a1c8d0 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -175,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; @@ -289,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() } diff --git a/web/src/ui.rs b/web/src/ui.rs index 1eaa8ff44..27aa45fba 100644 --- a/web/src/ui.rs +++ b/web/src/ui.rs @@ -292,7 +292,9 @@ impl UiBackend for WebUiBackend { self.js_player.open_virtual_keyboard() } - fn close_virtual_keyboard(&self) {} + fn close_virtual_keyboard(&self) { + self.js_player.close_virtual_keyboard() + } fn language(&self) -> LanguageIdentifier { self.language.clone()