Skip to content
This repository has been archived by the owner on Dec 28, 2021. It is now read-only.

Issue #9, Implement Keyboard mgmt engine #10

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[*.rs]
indent_size = 4
indent_style = space
trim_trailing_whitespace = true
max_line_length = 80
gurobokum marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ wasm-pack.log

# A thirdparty downloaded js
/lib/core/msdf-sys/msdfgen_wasm.js

# VIM
*.sw?
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,7 @@ this repository:
#### Linting
Please be sure to fix all errors reported by `scripts/lint.sh` before creating a
pull request to this repository.

#### Testing

`wasm-pack test --chrome --headless`
88 changes: 88 additions & 0 deletions lib/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ mod example_02 {
use crate::display::shape::text::TextComponentBuilder;
use crate::display::shape::text::TextComponentProperties;


#[wasm_bindgen]
#[allow(dead_code)]
pub fn run_example_text() {
Expand Down Expand Up @@ -357,6 +358,93 @@ mod example_02 {
}
}

// ==================
// === Example 04 ===
// ==================
mod example_04 {
use wasm_bindgen::prelude::*;
use basegl_system_web::keyboard_engine::KeyboardEngine;
use basegl_system_web::{window};
use web_sys::KeyEvent;

#[wasm_bindgen(start)]
pub fn run_example_keyboard_management() {
init_keyboard_engine();
}

/// Init keyboard handlers on Window with the specific combinations
///
/// Combinations:
/// - a
/// - Shift-a
/// - Control-Shift-a
/// - Control-Space
/// - Alt-Space
fn init_keyboard_engine() {
let keyboard_engine = KeyboardEngine::new(&window().unwrap());
let handle1 = keyboard_engine.capture(
vec![
KeyEvent::DOM_VK_A
],
Box::new(|| {
web_sys::console::log_1(&"Key pressed: a".into());
})
);
std::mem::forget(handle1);
let handle2 = keyboard_engine.capture(
vec![
KeyEvent::DOM_VK_A,
KeyEvent::DOM_VK_SHIFT
],
Box::new(|| {
web_sys::console::log_1(&"Key pressed: Shift-a".into());
})
);
std::mem::forget(handle2);
let handle3 = keyboard_engine.capture(
vec![
KeyEvent::DOM_VK_A,
KeyEvent::DOM_VK_SHIFT,
KeyEvent::DOM_VK_CONTROL
],
Box::new(|| {
web_sys::console::log_1(&"Key pressed: Control-Shift-a".into());
})
);
std::mem::forget(handle3);
let handle4 = keyboard_engine.capture(
vec![
KeyEvent::DOM_VK_SPACE,
KeyEvent::DOM_VK_CONTROL
],
Box::new(|| {
web_sys::console::log_1(&"Key pressed: Control-Space".into());
})
);
std::mem::forget(handle4);
let handle5 = keyboard_engine.capture(
vec![
KeyEvent::DOM_VK_SPACE,
KeyEvent::DOM_VK_ALT
],
Box::new(|| {
web_sys::console::log_1(&"Key pressed: Alt-Space".into());
})
);
std::mem::forget(handle5);
let handle6 = keyboard_engine.capture(
vec![
KeyEvent::DOM_VK_SPACE,
KeyEvent::DOM_VK_ALT
],
Box::new(|| {
web_sys::console::log_1(&"Roger that! Key pressed: Alt-Space".into());
})
);
std::mem::forget(handle6);
}
}


// =================
// === Utilities ===
Expand Down
11 changes: 10 additions & 1 deletion lib/system/web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ basegl-prelude = { version = "0.1.0" , path = "../../prelude" }
js-sys = { version = "0.3.28" }
wasm-bindgen = { version = "^0.2" , features = ["nightly"] }
failure = { version = "0.1.5" }
bigint = { version = "4.4.1" }

console_error_panic_hook = { version = "0.1.1", optional = true }

Expand All @@ -26,6 +27,12 @@ features = [
'HtmlElement',
'HtmlCollection',
'CssStyleDeclaration',
'Event',
'EventTarget',
'KeyEvent',
'KeyboardEvent',
'KeyboardEventInit',
'Node',
'HtmlCanvasElement',
'WebGlBuffer',
'WebGlRenderingContext',
Expand All @@ -38,4 +45,6 @@ features = [
]

[dev-dependencies]
wasm-bindgen-test = "0.2"
wasm-bindgen-test = "0.3.4"
wasm-bindgen-futures = "0.4.4"
futures = "0.3.1"
46 changes: 46 additions & 0 deletions lib/system/web/src/keyboard_engine/bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use bigint::uint::U256;
use std::cell::RefCell;
use std::collections::HashMap;

use crate::keyboard_engine::callback_registry::*;

type BindingsMap = HashMap<U256, CallbackRegistry>;
gurobokum marked this conversation as resolved.
Show resolved Hide resolved

#[derive(Default)]
pub struct Bindings {
pub data: RefCell<BindingsMap>,
}

impl Bindings {
pub fn new() -> Self{
Self {
data: RefCell::new(HashMap::new())
}
gurobokum marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn add<F: CallbackMut>
(&self, key: U256, callback: F) -> CallbackHandle {
let mut data = self.data.borrow_mut();
gurobokum marked this conversation as resolved.
Show resolved Hide resolved
match data.get_mut(&key) {
Some(registry) => {
registry.add(callback)
}
None => {
let mut registry = CallbackRegistry::new();
let handle = registry.add(callback);
data.insert(key, registry);
handle
}
}
}

pub fn remove(&self, key: U256) {
self.data.borrow_mut().remove(&key);
}

pub fn call_by_key(&self, key: U256) {
if let Some(registry) = self.data.borrow_mut().get_mut(&key) {
registry.call();
}
}
}
56 changes: 56 additions & 0 deletions lib/system/web/src/keyboard_engine/callback_registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::rc::{Rc, Weak};

pub trait CallbackMut = FnMut() + 'static;

pub struct Guard {
weak: Weak<()>
}

impl Guard {
pub fn exists(&self) -> bool {
self.weak.upgrade().is_some()
}
}

#[derive(Default)]
pub struct CallbackHandle {
rc: Rc<()>
}

impl CallbackHandle {
pub fn guard(&self) -> Guard {
let weak = Rc::downgrade(&self.rc);
Guard {weak}
}
}

pub struct CallbackRegistry {
pub registry: Vec<(Guard, Box<dyn CallbackMut>)>
}

impl CallbackRegistry {
pub fn new() -> Self {
Self {
registry: Vec::new()
}
}

pub fn call(&mut self) {
self.drop_orphaned_callbacks();
self.registry
.iter_mut()
.for_each(|(_, func)| func());
}

pub fn add<F: CallbackMut>
(&mut self, callback: F) -> CallbackHandle {
let handle = CallbackHandle::default();
let guard = handle.guard();
self.registry.push( (guard, Box::new(callback)) );
handle
}

pub fn drop_orphaned_callbacks(&mut self) {
self.registry.retain(|(guard, _)| guard.exists());
}
}
117 changes: 117 additions & 0 deletions lib/system/web/src/keyboard_engine/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
pub mod callback_registry;
pub mod bindings;

use std::cell::Cell;
use bigint::uint::U256;
use web_sys::{KeyboardEvent};
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;

use crate::*;
use self::bindings::*;
use self::callback_registry::*;

/// Keyboard Engine that handles user keyboard combinations
///
/// Keyboard Engine stores bindings as HashMap:
/// {
/// U256: [Callback1, Callback2],
/// U256: [Callback1],
/// }
/// when the combination is pressed related callbacks are invoked
/// Pressed combination is encoded into U256 bitstring
pub struct KeyboardEngine {
pub bindings: Rc<Bindings>,
pub state: Rc<Cell<U256>>,
pub target: web_sys::EventTarget
}

impl KeyboardEngine {
pub fn new(target: &web_sys::EventTarget) -> Self {
let mut this = Self {
bindings: Rc::new(Bindings::new()),
state: Rc::new(Cell::new(U256::from(0))),
target: target.clone()
};
this.init();
this
}

pub fn from_tag_name(html_tag: &str) -> Result<Self> {
let element = document()?
.get_elements_by_tag_name(html_tag)
.item(0).ok_or_else(|| Error::missing("there is no such element"));

Ok(KeyboardEngine::new(&element.unwrap()))
}

fn init(&mut self) {
self.bind_keydown_event();
self.bind_keyup_event();
}

fn bind_keydown_event(&self) {
let state = Rc::clone(&self.state);
let bindings = Rc::clone(&self.bindings);
let callback = Box::new(move |event: web_sys::KeyboardEvent| {
if event.repeat() {
return;
}

let key_code = event.key_code();
let bit = U256::from(1) << (key_code as usize);
let key = state.get() | bit;
state.set(key);
bindings.call_by_key(key);
});
let callback = Closure::wrap(callback as Box<dyn FnMut(_)>);

let callback_ref = callback.as_ref().unchecked_ref();
self.target
.add_event_listener_with_callback("keydown", callback_ref).unwrap();
callback.forget();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it leaking memory here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's, the main idea, that if I would like to avoid to use .forget() I must to make my KeyboardEngine instance static for having ownership for closures. I did few attempts to implement it, but all of them were looked ugly, so I stayed with forget()

}

fn bind_keyup_event(&self) {
let bindings = Rc::clone(&self.bindings);
let state = Rc::clone(&self.state);
let callback = Box::new(move |event: KeyboardEvent| {
let key_code = event.key_code();
let bit = U256::from(1) << (key_code as usize);
let key = state.get() ^ bit;
state.set(key);
bindings.call_by_key(key);
});
let callback = Closure::wrap(callback as Box<dyn FnMut(_)>);

let callback_ref = callback.as_ref().unchecked_ref();
self.target
.add_event_listener_with_callback("keyup", callback_ref).unwrap();
callback.forget();
}

/// Returns U256 number that represents bitstring of pressed keys
fn get_key_bits(&self, combination: Vec<u32>) -> U256 {
combination.iter()
.fold(U256::from(0), |acc, x| {
let bit = U256::from(1) << (*x as usize);
acc | bit
})
}

/// Capture key combination
///
/// Uses u32 as key code from WebIDL
/// https://github.com/rustwasm/wasm-bindgen/blob/913fdbc3daff65952b5678a34b98e07d4e6e4fbb/crates/web-sys/webidls/enabled/KeyEvent.webidl
pub fn capture<F: CallbackMut>
(&self, combination: Vec<u32>, callback: F) -> CallbackHandle {
let key = self.get_key_bits(combination);
self.bindings.add(key, callback)
}

/// Drops watching specific combination
pub fn drop_capture(&self, combination: Vec<u32>) {
let key = self.get_key_bits(combination);
self.bindings.remove(key);
}
}
1 change: 1 addition & 0 deletions lib/system/web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![feature(trait_alias)]

pub mod keyboard_engine;
pub mod resize_observer;
pub mod animation_frame_loop;

Expand Down
Loading