From 5441749e3e785c3a5d136bd17b55f00107f31016 Mon Sep 17 00:00:00 2001 From: Kamil Jarosz Date: Mon, 8 Jul 2024 20:31:56 +0200 Subject: [PATCH] web: Use browser focus events for focus management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- web/Cargo.toml | 2 +- web/src/builder.rs | 2 ++ web/src/lib.rs | 85 +++++++++++++++++++++++++++------------------- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/web/Cargo.toml b/web/Cargo.toml index 0f060f3a9..05e266617 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -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] diff --git a/web/src/builder.rs b/web/src/builder.rs index 662bf0c3a..dbfd3d564 100644 --- a/web/src/builder.rs +++ b/web/src/builder.rs @@ -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())) diff --git a/web/src/lib.rs b/web/src/lib.rs index e00d614c6..3f305049a 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -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>, mouse_leave_callback: Option>, mouse_down_callback: Option>, - player_mouse_down_callback: Option>, - window_mouse_down_callback: Option>, mouse_up_callback: Option>, mouse_wheel_callback: Option>, key_down_callback: Option>, key_up_callback: Option>, paste_callback: Option>, unload_callback: Option>, + focusin_callback: Option>, + focusout_callback: Option>, + focus_on_press_callback: Option>, has_focus: bool, trace_observer: Rc>, log_subscriber: Arc>, @@ -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.