Skip to content

Commit 78aba86

Browse files
authored
Listen to Hotkeys in Child Windows on the Web (#868)
This allows you to add a child window to a hotkey hook on the web, allowing it to receive hotkey events for the window as well. Additionally, there's some small changes to the Web Renderer: - The child canvas elements are relative, but the parent was not marked as absolute. - The parent element is always an HTML element, which was not reflected in the type. It's technically also a `div` element, but we don't want to guarantee that, since it could be changed in the future. This may be the case if we switch to only a single canvas element in the future.
1 parent 5d2bd50 commit 78aba86

6 files changed

Lines changed: 100 additions & 50 deletions

File tree

capi/src/hotkey_system.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,18 @@ pub unsafe extern "C" fn HotkeySystem_resolve(
8585
_ => output_str(name),
8686
}
8787
}
88+
89+
#[cfg(all(target_family = "wasm", feature = "wasm-web"))]
90+
use wasm_bindgen::prelude::*;
91+
92+
/// On the web you can use this to listen to keyboard events on an additional
93+
/// child window as well.
94+
#[cfg(all(target_family = "wasm", feature = "wasm-web"))]
95+
#[wasm_bindgen]
96+
pub unsafe fn HotkeySystem_add_window(
97+
hotkey_system: *const HotkeySystem,
98+
window: web_sys::Window,
99+
) -> bool {
100+
// SAFETY: The caller guarantees that `hotkey_system` is valid.
101+
unsafe { (*hotkey_system).add_window(window).is_ok() }
102+
}

capi/src/web_rendering.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
use livesplit_core::{layout::LayoutState, rendering::web, settings::ImageCache};
55
use wasm_bindgen::prelude::*;
6-
use web_sys::Element;
6+
use web_sys::HtmlElement;
77

88
/// The web renderer renders into a canvas element. The element can then be
99
/// attached anywhere in the DOM with any desired positioning and size.
@@ -30,7 +30,7 @@ impl WebRenderer {
3030

3131
/// Returns the HTML element. This can be attached anywhere in the DOM with
3232
/// any desired positioning and size.
33-
pub fn element(&self) -> Element {
33+
pub fn element(&self) -> HtmlElement {
3434
self.inner.element().clone()
3535
}
3636

@@ -42,8 +42,6 @@ impl WebRenderer {
4242
image_cache: *const ImageCache,
4343
) -> Option<Box<[f32]>> {
4444
// SAFETY: The caller must ensure that the pointers are valid.
45-
unsafe {
46-
self.inner.render(&*state, &*image_cache).map(Box::from)
47-
}
45+
unsafe { self.inner.render(&*state, &*image_cache).map(Box::from) }
4846
}
4947
}

crates/livesplit-hotkey/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ impl Hook {
9393
pub fn unregister(&self, hotkey: Hotkey) -> Result<()> {
9494
self.0.unregister(hotkey)
9595
}
96+
97+
/// On the web you can use this to listen to keyboard events on an
98+
/// additional child window as well.
99+
#[cfg(all(target_family = "wasm", feature = "wasm-web"))]
100+
pub fn add_window(&self, window: web_sys::Window) -> Result<()> {
101+
self.0.add_window(window)
102+
}
96103
}
97104

98105
/// The result type for this crate.

crates/livesplit-hotkey/src/wasm_web/mod.rs

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{ConsumePreference, Hotkey, KeyCode, Modifiers, Result};
22
use js_sys::{Function, Promise, Reflect};
3-
use wasm_bindgen::{prelude::*, JsCast};
4-
use web_sys::{window, Event, Gamepad, GamepadButton, KeyboardEvent};
3+
use wasm_bindgen::{JsCast, prelude::*};
4+
use web_sys::{Event, Gamepad, GamepadButton, KeyboardEvent, window};
55

66
use std::{
77
cell::{Cell, RefCell},
@@ -27,7 +27,7 @@ impl fmt::Display for Error {
2727

2828
pub struct Hook {
2929
hotkeys: Arc<Mutex<HashMap<Hotkey, Box<dyn FnMut() + Send + 'static>>>>,
30-
keyboard_callback: Closure<dyn FnMut(Event)>,
30+
keyboard_callback: Closure<dyn FnMut(MaybeKeyboardEvent)>,
3131
gamepad_callback: Closure<dyn FnMut()>,
3232
interval_id: Cell<Option<i32>>,
3333
keyboard_layout_resolver: Rc<RefCell<Option<(JsValue, Function)>>>,
@@ -87,49 +87,53 @@ impl Hook {
8787
let window = window().ok_or(crate::Error::Platform(Error::FailedToCreateHook))?;
8888

8989
let hotkey_map = hotkeys.clone();
90-
let keyboard_callback = Closure::wrap(Box::new(move |event: Event| {
90+
let keyboard_callback = Closure::wrap(Box::new(move |event: MaybeKeyboardEvent| {
9191
// Despite all sorts of documentation claiming that `keydown` events
9292
// pass you a `KeyboardEvent`, this is not actually always the case
9393
// in browsers. At least in Chrome selecting an element of an
94-
// `input` sends a `keydown` event that is not a `KeyboardEvent`.
95-
if let Ok(event) = event.dyn_into::<KeyboardEvent>() {
96-
if !event.repeat() {
97-
if let Ok(code) = event.code().parse::<KeyCode>() {
98-
let mut modifiers = Modifiers::empty();
99-
if event.shift_key()
100-
&& !matches!(code, KeyCode::ShiftLeft | KeyCode::ShiftRight)
101-
{
102-
modifiers.insert(Modifiers::SHIFT);
103-
}
104-
if event.ctrl_key()
105-
&& !matches!(code, KeyCode::ControlLeft | KeyCode::ControlRight)
106-
{
107-
modifiers.insert(Modifiers::CONTROL);
108-
}
109-
if event.alt_key() && !matches!(code, KeyCode::AltLeft | KeyCode::AltRight)
110-
{
111-
modifiers.insert(Modifiers::ALT);
112-
}
113-
if event.meta_key()
114-
&& !matches!(code, KeyCode::MetaLeft | KeyCode::MetaRight)
115-
{
116-
modifiers.insert(Modifiers::META);
117-
}
94+
// `input` sends a `keydown` event that is not a `KeyboardEvent` and
95+
// instead just an `Event`. On top of that, child windows all have
96+
// their own `KeyboardEvent` class that is not the same as the
97+
// `KeyboardEvent` class of the parent window. So we can't use
98+
// `dyn_into` and instead define our own `MaybeKeyboardEvent` type
99+
// that optionally supports the `repeat` method, allowing us to both
100+
// check if the event even looks like a `KeyboardEvent` and then we
101+
// proceed based on that.
102+
if event.repeat() == Some(false) {
103+
let event = event.unchecked_into::<KeyboardEvent>();
118104

119-
if let Some(callback) = hotkey_map
120-
.lock()
121-
.unwrap()
122-
.get_mut(&code.with_modifiers(modifiers))
123-
{
124-
callback();
125-
if prevent_default {
126-
event.prevent_default();
127-
}
105+
if let Ok(code) = event.code().parse::<KeyCode>() {
106+
let mut modifiers = Modifiers::empty();
107+
if event.shift_key()
108+
&& !matches!(code, KeyCode::ShiftLeft | KeyCode::ShiftRight)
109+
{
110+
modifiers.insert(Modifiers::SHIFT);
111+
}
112+
if event.ctrl_key()
113+
&& !matches!(code, KeyCode::ControlLeft | KeyCode::ControlRight)
114+
{
115+
modifiers.insert(Modifiers::CONTROL);
116+
}
117+
if event.alt_key() && !matches!(code, KeyCode::AltLeft | KeyCode::AltRight) {
118+
modifiers.insert(Modifiers::ALT);
119+
}
120+
if event.meta_key() && !matches!(code, KeyCode::MetaLeft | KeyCode::MetaRight) {
121+
modifiers.insert(Modifiers::META);
122+
}
123+
124+
if let Some(callback) = hotkey_map
125+
.lock()
126+
.unwrap()
127+
.get_mut(&code.with_modifiers(modifiers))
128+
{
129+
callback();
130+
if prevent_default {
131+
event.prevent_default();
128132
}
129133
}
130134
}
131135
}
132-
}) as Box<dyn FnMut(Event)>);
136+
}) as Box<dyn FnMut(MaybeKeyboardEvent)>);
133137

134138
window
135139
.add_event_listener_with_callback("keydown", keyboard_callback.as_ref().unchecked_ref())
@@ -250,4 +254,22 @@ impl Hook {
250254
.ok()?
251255
.as_string()
252256
}
257+
258+
pub fn add_window(&self, window: web_sys::Window) -> Result<()> {
259+
window
260+
.add_event_listener_with_callback(
261+
"keydown",
262+
self.keyboard_callback.as_ref().unchecked_ref(),
263+
)
264+
.map_err(|_| crate::Error::Platform(Error::FailedToCreateHook))
265+
}
266+
}
267+
268+
#[wasm_bindgen]
269+
extern "C" {
270+
# [wasm_bindgen (extends = Event , extends = :: js_sys :: Object , js_name = KeyboardEvent , typescript_type = "KeyboardEvent")]
271+
#[derive(Debug, Clone, PartialEq, Eq)]
272+
pub type MaybeKeyboardEvent;
273+
# [wasm_bindgen (structural , method , getter , js_class = "KeyboardEvent" , js_name = repeat)]
274+
pub fn repeat(this: &MaybeKeyboardEvent) -> Option<bool>;
253275
}

src/hotkey_system.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use alloc::borrow::Cow;
22

33
use crate::{
4-
event,
4+
HotkeyConfig, event,
55
hotkey::{ConsumePreference, Hook, Hotkey, KeyCode},
6-
HotkeyConfig,
76
};
87

98
pub use crate::hotkey::Result;
@@ -33,7 +32,7 @@ enum Action {
3332
}
3433

3534
impl Action {
36-
fn set_hotkey(self, config: &mut HotkeyConfig, hotkey: Option<Hotkey>) {
35+
const fn set_hotkey(self, config: &mut HotkeyConfig, hotkey: Option<Hotkey>) {
3736
match self {
3837
Action::Split => config.split = hotkey,
3938
Action::Reset => config.reset = hotkey,
@@ -288,4 +287,11 @@ impl<S: event::CommandSink + Clone + Send + 'static> HotkeySystem<S> {
288287
pub fn resolve(&self, key_code: KeyCode) -> Cow<'static, str> {
289288
key_code.resolve(&self.hook)
290289
}
290+
291+
/// On the web you can use this to listen to keyboard events on an
292+
/// additional child window as well.
293+
#[cfg(all(target_family = "wasm", feature = "wasm-web"))]
294+
pub fn add_window(&self, window: web_sys::Window) -> Result<()> {
295+
self.hook.add_window(window)
296+
}
291297
}

src/rendering/web/mod.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::{
1414
};
1515
use wasm_bindgen::{JsCast, JsValue};
1616
use wasm_bindgen_futures::JsFuture;
17-
use web_sys::{Blob, Element, HtmlCanvasElement, ImageBitmap, Path2d, Window};
17+
use web_sys::{Blob, HtmlCanvasElement, HtmlElement, ImageBitmap, Path2d, Window};
1818

1919
use crate::{
2020
layout::LayoutState,
@@ -478,7 +478,7 @@ impl FontHandling {
478478
/// attached anywhere in the DOM with any desired positioning and size.
479479
pub struct Renderer {
480480
manager: SceneManager<Path, Image, Rc<CanvasFont>, CanvasLabel>,
481-
div: Element,
481+
div: HtmlElement,
482482
allocator: CanvasAllocator,
483483
canvas_bottom: HtmlCanvasElement,
484484
canvas_top: HtmlCanvasElement,
@@ -623,7 +623,9 @@ impl Renderer {
623623
let window = web_sys::window().unwrap();
624624
let document = window.document().unwrap();
625625

626-
let div = document.create_element("div").unwrap();
626+
let div: HtmlElement = document.create_element("div").unwrap().unchecked_into();
627+
628+
div.set_attribute("style", "position: relative;").unwrap();
627629

628630
let canvas_bottom: HtmlCanvasElement =
629631
document.create_element("canvas").unwrap().unchecked_into();
@@ -689,7 +691,7 @@ impl Renderer {
689691

690692
/// Returns the HTML element. This can be attached anywhere in the DOM with
691693
/// any desired positioning and size.
692-
pub const fn element(&self) -> &Element {
694+
pub const fn element(&self) -> &HtmlElement {
693695
&self.div
694696
}
695697

0 commit comments

Comments
 (0)