web: Use browser focus events for focus management

This patch changes how the player focus is managed.

Before this patch, `instance.has_focus` was unrelated
to the current focus managed by the browser.
When the user clicked anywhere on the window, it was set to `false`,
and when the user clicked on the player, it was set to `true`.
This had two major issues.
1. When the user clicked on the player, `has_focus` was set to `false`,
   and then again to `true`. That was problematic when listening
   to focus change events.
2. Not using browser's focus makes it harder to integrate with Ruffle,
   i.e. tab out of Ruffle.

This patch uses browser's focus management to detect focus
and listen to focus events:
1. on `focusin` – FocusGained event is fired,
2. on `focusout` – FocusLost event is fired, and
3. on `pointerdown` – focus is manually set to the canvas.
The canvas has `tabindex` set to -1 in order to be focusable.
This commit is contained in:
Kamil Jarosz 2024-07-08 20:31:56 +02:00 committed by TÖRÖK Attila
parent 599352093c
commit 5441749e3e
3 changed files with 53 additions and 36 deletions

View File

@ -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]

View File

@ -574,6 +574,8 @@ impl RuffleInstanceBuilder {
let (renderer, canvas) = self.create_renderer().await?;
canvas.set_tab_index(-1);
let mut builder = PlayerBuilder::new()
.with_boxed_renderer(renderer)
.with_boxed_audio(self.create_audio_backend(log_subscriber.clone()))

View File

@ -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, PointerEvent, WheelEvent, Window,
};
static RUFFLE_GLOBAL_PANIC: Once = Once::new();
@ -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>>,
@ -473,14 +474,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,
@ -587,35 +589,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,
@ -767,6 +740,48 @@ impl RuffleHandle {
core.flush_shared_objects();
});
}));
instance.focusin_callback = Some(JsCallback::register(
&player.canvas,
"focusin",
false,
move |_js_event: FocusEvent| {
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);
});
}
});
},
));
instance.focusout_callback = Some(JsCallback::register(
&player.canvas,
"focusout",
false,
move |_js_event: FocusEvent| {
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 canvas = player.canvas.clone();
instance.focus_on_press_callback = Some(JsCallback::register(
&player.canvas,
"pointerdown",
false,
move |_js_event| {
canvas.focus().warn_on_error();
},
));
})?;
// Set initial timestamp and do initial tick to start animation loop.