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.
This commit is contained in:
Kamil Jarosz 2024-07-17 16:33:01 +02:00
parent 53d2d16162
commit 96173b0501
4 changed files with 59 additions and 21 deletions

View File

@ -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();

View File

@ -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;
}

View File

@ -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()
}

View File

@ -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()