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, // Mouse focus change events are not dispatched when the object is the same,
// contrary to key focus change events. // contrary to key focus change events.
if InteractiveObject::option_ptr_eq(old, new) { 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; return;
} }
@ -194,15 +196,21 @@ impl<'gc> FocusTracker<'gc> {
} }
// This applies even if the focused element hasn't changed. // 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 let Some(text_field) = self.get_as_edit_text() {
if text_field.is_editable() { 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(); context.ui.open_virtual_keyboard();
} else { } else {
context.ui.close_virtual_keyboard(); context.ui.close_virtual_keyboard();

View File

@ -755,12 +755,6 @@ export class RufflePlayer extends HTMLElement {
} }
this.unmuteAudioContext(); 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`. // Treat invalid values as `AutoPlay.Auto`.
if ( if (
@ -1388,6 +1382,7 @@ export class RufflePlayer extends HTMLElement {
console.error("SWF download failed"); console.error("SWF download failed");
} }
} }
private virtualKeyboardInput() { private virtualKeyboardInput() {
const input = this.virtualKeyboard; const input = this.virtualKeyboard;
const string = input.value; const string = input.value;
@ -1403,17 +1398,42 @@ export class RufflePlayer extends HTMLElement {
} }
input.value = ""; input.value = "";
} }
protected openVirtualKeyboard(): void { protected openVirtualKeyboard(): void {
// On Android, the Rust code that opens the virtual keyboard triggers // Virtual keyboard is opened/closed synchronously from core,
// before the TypeScript code that closes it, so delay opening it // and opening/closing it is basically dispatching
if (navigator.userAgent.toLowerCase().includes("android")) { // focus events (which may also be dispatched to the player).
setTimeout(() => { // In order not to deadlock here (or rather throw an error),
this.virtualKeyboard.focus({ preventScroll: true }); // these actions should be performed asynchronously.
}, 100); // 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 { } 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 { protected isVirtualKeyboardFocused(): boolean {
return this.shadow.activeElement === this.virtualKeyboard; return this.shadow.activeElement === this.virtualKeyboard;
} }

View File

@ -175,6 +175,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = "openVirtualKeyboard")] #[wasm_bindgen(method, js_name = "openVirtualKeyboard")]
fn open_virtual_keyboard(this: &JavascriptPlayer); fn open_virtual_keyboard(this: &JavascriptPlayer);
#[wasm_bindgen(method, js_name = "closeVirtualKeyboard")]
fn close_virtual_keyboard(this: &JavascriptPlayer);
#[wasm_bindgen(method, js_name = "isVirtualKeyboardFocused")] #[wasm_bindgen(method, js_name = "isVirtualKeyboardFocused")]
fn is_virtual_keyboard_focused(this: &JavascriptPlayer) -> bool; fn is_virtual_keyboard_focused(this: &JavascriptPlayer) -> bool;
@ -289,6 +292,11 @@ impl RuffleHandle {
self.with_core(|core| core.is_playing()).unwrap_or_default() 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 { pub fn volume(&self) -> f32 {
self.with_core(|core| core.volume()).unwrap_or_default() 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() 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 { fn language(&self) -> LanguageIdentifier {
self.language.clone() self.language.clone()