use std::{
sync::atomic::{AtomicU64, Ordering::SeqCst},
task::{Context, Poll, Waker},
};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
AddEventListenerOptions, Event, EventTarget, HtmlElement, HtmlInputElement,
InputEvent, KeyboardEvent, MouseEvent, WheelEvent,
};
use crate::{Input, Key, Mod, Btn};
struct WebInput {
input: Option<Input>,
waker: Option<Waker>,
}
static mut WEB_INPUT: WebInput = WebInput {
input: None,
waker: None,
};
static KEYBOARD_STATE: AtomicU64 = AtomicU64::new(0);
static KEYBOARD_STATE_FN: AtomicU64 = AtomicU64::new(0);
fn key_up_state(key: Key) -> bool {
if key as i8 >= 64 {
let bitflag = 1 << (key as i8 - 64);
let new = KEYBOARD_STATE_FN.fetch_and(!bitflag, SeqCst);
let old = KEYBOARD_STATE_FN.load(SeqCst);
old != new
} else {
let bitflag = 1 << (key as i8);
let new = KEYBOARD_STATE.fetch_and(!bitflag, SeqCst);
let old = KEYBOARD_STATE.load(SeqCst);
old != new
}
}
fn key_down_state(key: Key) -> bool {
if key as i8 >= 64 {
let bitflag = 1 << (key as i8 - 64);
let new = KEYBOARD_STATE_FN.fetch_or(bitflag, SeqCst);
let old = KEYBOARD_STATE_FN.load(SeqCst);
old != new
} else {
let bitflag = 1 << (key as i8);
let new = KEYBOARD_STATE.fetch_or(bitflag, SeqCst);
let old = KEYBOARD_STATE.load(SeqCst);
old != new
}
}
#[allow(unsafe_code)]
fn state<'a>() -> &'a mut WebInput {
unsafe { &mut WEB_INPUT }
}
pub(crate) fn poll(cx: &mut Context<'_>) -> Poll<Input> {
let state = state();
if let Some(input) = state.input.take() {
Poll::Ready(input)
} else {
state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
fn is_printing(keycode: &str) -> bool {
keycode.starts_with("Digit")
|| keycode.starts_with("Numpad")
|| keycode.starts_with("Key")
|| matches!(
keycode,
"Space"
| "Minus"
| "Equal"
| "Backslash"
| "BracketRight"
| "BracketLeft"
| "Quote"
| "Semicolon"
| "Slash"
| "Period"
| "Comma"
| "Backquote"
)
}
fn ptr_modifier(event: &MouseEvent) -> Mod {
let mut mods = Mod::new();
let ctrl =
event.get_modifier_state("Control") || event.get_modifier_state("Meta");
let alt = event.get_modifier_state("Alt");
let shift = event.get_modifier_state("Shift");
if ctrl {
mods = mods.add_ctrl();
}
if alt {
mods = mods.add_alt();
}
if shift {
mods = mods.add_shift();
}
mods
}
fn key_modifier(event: &KeyboardEvent, mods: &mut Mod) -> bool {
let ctrl =
event.get_modifier_state("Control") || event.get_modifier_state("Meta");
let alt = event.get_modifier_state("Alt");
let shift = event.get_modifier_state("Shift");
if ctrl {
*mods = mods.add_ctrl();
}
if alt {
*mods = mods.add_alt();
}
if shift {
*mods = mods.add_shift();
}
ctrl || alt
}
fn keycode(keycode: &str) -> Option<(Key, Mod)> {
let key = match keycode {
"Digit0" | "Numpad0" => (Key::Zero, Mod::new()),
"Digit1" | "Numpad1" => (Key::One, Mod::new()),
"Digit2" | "Numpad2" => (Key::Two, Mod::new()),
"Digit3" | "Numpad3" => (Key::Three, Mod::new()),
"Digit4" | "Numpad4" => (Key::Four, Mod::new()),
"Digit5" | "Numpad5" => (Key::Five, Mod::new()),
"Digit6" | "Numpad6" => (Key::Six, Mod::new()),
"Digit7" | "Numpad7" => (Key::Seven, Mod::new()),
"Digit8" | "Numpad8" => (Key::Eight, Mod::new()),
"Digit9" | "Numpad9" => (Key::Nine, Mod::new()),
"Enter" | "NumpadEnter" => (Key::Enter, Mod::new()),
"KeyA" => (Key::A, Mod::new()),
"KeyB" => (Key::B, Mod::new()),
"KeyC" => (Key::C, Mod::new()),
"KeyD" => (Key::D, Mod::new()),
"KeyE" => (Key::E, Mod::new()),
"KeyF" => (Key::F, Mod::new()),
"KeyG" => (Key::G, Mod::new()),
"KeyH" => (Key::H, Mod::new()),
"KeyI" => (Key::I, Mod::new()),
"KeyJ" => (Key::J, Mod::new()),
"KeyK" => (Key::K, Mod::new()),
"KeyL" => (Key::L, Mod::new()),
"KeyM" => (Key::M, Mod::new()),
"KeyN" => (Key::N, Mod::new()),
"KeyO" => (Key::O, Mod::new()),
"KeyP" => (Key::P, Mod::new()),
"KeyQ" => (Key::Q, Mod::new()),
"KeyR" => (Key::R, Mod::new()),
"KeyS" => (Key::S, Mod::new()),
"KeyT" => (Key::T, Mod::new()),
"KeyU" => (Key::U, Mod::new()),
"KeyV" => (Key::V, Mod::new()),
"KeyW" => (Key::W, Mod::new()),
"KeyX" => (Key::X, Mod::new()),
"KeyY" => (Key::Y, Mod::new()),
"KeyZ" => (Key::Z, Mod::new()),
"ArrowUp" => (Key::Up, Mod::new()),
"ArrowDown" => (Key::Down, Mod::new()),
"ArrowLeft" => (Key::Left, Mod::new()),
"ArrowRight" => (Key::Right, Mod::new()),
"Space" => (Key::Space, Mod::new()),
"Tab" => (Key::Tab, Mod::new()),
"Backspace" => (Key::Backspace, Mod::new()),
"Delete" => (Key::Delete, Mod::new()),
"Escape" => (Key::Back, Mod::new()),
"Minus" => (Key::Minus, Mod::new()),
"Equal" => (Key::Equal, Mod::new()),
"Insert" => (Key::Insert, Mod::new()),
"PageUp" => (Key::PageUp, Mod::new()),
"PageDown" => (Key::PageDown, Mod::new()),
"Home" => (Key::Home, Mod::new()),
"End" => (Key::End, Mod::new()),
"Backslash" => (Key::Backslash, Mod::new()),
"BracketLeft" => (Key::BracketOpen, Mod::new()),
"BracketRight" => (Key::BracketClose, Mod::new()),
"Quote" => (Key::Apostrophe, Mod::new()),
"Semicolon" => (Key::Semicolon, Mod::new()),
"Slash" => (Key::Slash, Mod::new()),
"Period" | "NumpadDecimal" => (Key::Period, Mod::new()),
"Comma" => (Key::Comma, Mod::new()),
"Pause" => (Key::Pause, Mod::new()),
"ContextMenu" => (Key::Menu, Mod::new()),
"Backquote" => (Key::Backtick, Mod::new()),
"F1" => (Key::F1, Mod::new()),
"F2" => (Key::F2, Mod::new()),
"F3" => (Key::F3, Mod::new()),
"F4" => (Key::F4, Mod::new()),
"F5" => (Key::F5, Mod::new()),
"F6" => (Key::F6, Mod::new()),
"F7" => (Key::F7, Mod::new()),
"F8" => (Key::F8, Mod::new()),
"F9" => (Key::F9, Mod::new()),
"F10" => (Key::F10, Mod::new()),
"F11" => (Key::F11, Mod::new()),
"F12" => (Key::F12, Mod::new()),
"NumpadDivide" => (Key::Slash, Mod::new()),
"NumpadMultiply" => (Key::Eight, Mod::new().add_shift()),
"NumpadSubtract" => (Key::Minus, Mod::new()),
"NumpadAdd" => (Key::Equal, Mod::new().add_shift()),
"NumLock" => (Key::Compose, Mod::new()),
"CapsLock" => (Key::CapsLock, Mod::new()),
"ScrollLock" => (Key::Compose, Mod::new()),
"ShiftLeft" => (Key::LShift, Mod::new()),
"ShiftRight" => (Key::RShift, Mod::new()),
"" | "ControlLeft" | "MetaLeft" => (Key::LCtrl, Mod::new()),
"ControlRight" | "MetaRight" => (Key::RCtrl, Mod::new()),
"OSLeft" | "OSRight" => (Key::Env, Mod::new()),
"AltLeft" => (Key::LAlt, Mod::new()),
"AltRight" => (Key::RAlt, Mod::new()),
_x => return None,
};
Some(key)
}
pub(crate) fn delta(mode: u32, value: f32) -> f32 {
match mode {
0x00 => value,
0x01 => value * 16.0,
0x02 => {
value
* (web_sys::window()
.unwrap()
.inner_height()
.unwrap()
.as_f64()
.unwrap() as f32)
}
_ => unreachable!(),
}
}
pub(crate) fn init() {
let localized_input = web_sys::window()
.unwrap()
.document()
.unwrap()
.create_element("input")
.unwrap();
localized_input
.set_attribute(
"style",
"\
border:0;\
padding:0;\
margin:0;\
position:fixed;\
top:0;\
left:0;\
width:0;\
height:0;\
",
)
.unwrap();
localized_input
.set_attribute("id", "rust_crate_human__")
.unwrap();
let localized_input = localized_input.into();
web_sys::window()
.unwrap()
.document()
.unwrap()
.get_elements_by_tag_name("body")
.get_with_index(0)
.unwrap()
.append_child(&localized_input)
.unwrap();
let localized_input: EventTarget = localized_input.into();
let focus: Closure<dyn Fn()> = Closure::wrap(Box::new(move || {
let localized_input: HtmlElement = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("rust_crate_human__")
.unwrap()
.dyn_into()
.unwrap();
localized_input.focus().unwrap();
}));
let focus = focus.into_js_value();
let blur: Closure<dyn Fn()> = Closure::wrap(Box::new(move || {
web_sys::window()
.unwrap()
.set_timeout_with_callback(&focus.as_ref().unchecked_ref())
.unwrap();
}));
localized_input
.add_event_listener_with_callback(
"blur",
&blur.as_ref().unchecked_ref(),
)
.unwrap();
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"focus",
&blur.as_ref().unchecked_ref(),
)
.unwrap();
blur.forget();
let input: Closure<dyn Fn(InputEvent)> =
Closure::wrap(Box::new(move |event: InputEvent| {
if !event.is_composing() {
let localized_input = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("rust_crate_human__")
.unwrap();
let localized_input: HtmlInputElement =
localized_input.dyn_into().unwrap();
let value = localized_input.value();
localized_input.set_value("");
let mut char_iter = value.chars();
'a: while let Some(ref waker) = state().waker {
if let Some(c) = char_iter.next() {
state().input = Some(Input::Text(c));
waker.wake_by_ref();
} else {
break 'a;
}
}
}
event.stop_propagation();
}));
localized_input
.add_event_listener_with_callback(
"input",
&input.as_ref().unchecked_ref(),
)
.unwrap();
input.forget();
#[allow(trivial_casts)] let key_down: Closure<dyn Fn(KeyboardEvent)> =
Closure::wrap(Box::new(move |event: KeyboardEvent| {
if let Some(ref waker) = state().waker {
let mut sys_mods = false;
if let Some(mut keycode) = keycode(&event.code()) {
sys_mods = key_modifier(&event, &mut keycode.1);
if key_down_state(keycode.0) {
state().input =
Some(Input::Key(keycode.1, keycode.0, true));
} else {
state().input = None;
}
} else {
state().input = None;
}
event.stop_propagation();
if !is_printing(&event.code()) || sys_mods {
event.prevent_default();
}
waker.wake_by_ref();
}
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"keydown",
key_down.as_ref().unchecked_ref(),
)
.unwrap();
key_down.forget();
#[allow(trivial_casts)] let key_up: Closure<dyn Fn(KeyboardEvent)> =
Closure::wrap(Box::new(move |event: KeyboardEvent| {
if let Some(ref waker) = state().waker {
let mut sys_mods = false;
if let Some(mut keycode) = keycode(&event.code()) {
sys_mods = key_modifier(&event, &mut keycode.1);
if key_up_state(keycode.0) {
state().input =
Some(Input::Key(keycode.1, keycode.0, false));
} else {
state().input = None;
}
} else {
state().input = None;
}
event.stop_propagation();
if !is_printing(&event.code()) || sys_mods {
event.prevent_default();
}
waker.wake_by_ref();
}
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"keyup",
key_up.as_ref().unchecked_ref(),
)
.unwrap();
key_up.forget();
let localized_input: HtmlElement = localized_input.dyn_into().unwrap();
localized_input.focus().unwrap();
#[allow(trivial_casts)] let mouse_down: Closure<dyn Fn(MouseEvent)> =
Closure::wrap(Box::new(move |event: MouseEvent| {
let mods = ptr_modifier(&event);
if let Some(ref waker) = state().waker {
state().input = match event.button() {
0 => Some(Input::Click(mods, Btn::Left, true)),
1 => Some(Input::Click(mods, Btn::Middle, true)),
2 => Some(Input::Click(mods, Btn::Right, true)),
3 => Some(Input::Click(mods, Btn::Back, true)),
4 => Some(Input::Click(mods, Btn::Next, true)),
_ => Some(Input::Click(mods, Btn::Extra, true)),
};
waker.wake_by_ref();
}
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"mousedown",
mouse_down.as_ref().unchecked_ref(),
)
.unwrap();
mouse_down.forget();
#[allow(trivial_casts)] let mouse_up: Closure<dyn Fn(MouseEvent)> =
Closure::wrap(Box::new(move |event: MouseEvent| {
let mods = ptr_modifier(&event);
if let Some(ref waker) = state().waker {
state().input = match event.button() {
0 => Some(Input::Click(mods, Btn::Left, false)),
1 => Some(Input::Click(mods, Btn::Middle, false)),
2 => Some(Input::Click(mods, Btn::Right, false)),
3 => Some(Input::Click(mods, Btn::Back, false)),
4 => Some(Input::Click(mods, Btn::Next, false)),
_ => Some(Input::Click(mods, Btn::Extra, false)),
};
waker.wake_by_ref();
}
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"mouseup",
mouse_up.as_ref().unchecked_ref(),
)
.unwrap();
mouse_up.forget();
#[allow(trivial_casts)] let context_menu: Closure<dyn Fn(Event)> =
Closure::wrap(Box::new(move |event: Event| {
if state().waker.is_some() {
event.stop_propagation();
event.prevent_default();
}
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"contextmenu",
context_menu.as_ref().unchecked_ref(),
)
.unwrap();
context_menu.forget();
#[allow(trivial_casts)] let wheel: Closure<dyn Fn(WheelEvent)> =
Closure::wrap(Box::new(move |event: WheelEvent| {
let mods = ptr_modifier(&event);
let width = web_sys::window()
.unwrap()
.inner_width()
.unwrap()
.as_f64()
.unwrap();
let width = width as f32;
let delta_mode = event.delta_mode();
if let Some(ref waker) = state().waker {
let x = delta(delta_mode, event.delta_x() as f32) / width;
if x.abs() > f32::EPSILON {
state().input = Some(Input::ScrollX(mods, x));
waker.wake_by_ref();
}
}
if let Some(ref waker) = state().waker {
let y = delta(delta_mode, event.delta_y() as f32) / width;
if y.abs() > f32::EPSILON {
state().input = Some(Input::ScrollY(mods, y));
waker.wake_by_ref();
}
}
event.stop_propagation();
event.prevent_default();
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback_and_add_event_listener_options(
"wheel",
wheel.as_ref().unchecked_ref(),
&AddEventListenerOptions::new().passive(false),
)
.unwrap();
wheel.forget();
#[allow(trivial_casts)] let mouse_move: Closure<dyn Fn(MouseEvent)> =
Closure::wrap(Box::new(move |event: MouseEvent| {
let width = web_sys::window()
.unwrap()
.inner_width()
.unwrap()
.as_f64()
.unwrap();
let width = width as f32 - 1.0;
if let Some(ref waker) = state().waker {
let x = event.client_x() as f32 / width;
if x.abs() > f32::EPSILON {
state().input = Some(Input::PointerX(x));
waker.wake_by_ref();
}
}
if let Some(ref waker) = state().waker {
let y = event.client_y() as f32 / width;
if y.abs() > f32::EPSILON {
state().input = Some(Input::PointerY(y));
waker.wake_by_ref();
}
}
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"mousemove",
mouse_move.as_ref().unchecked_ref(),
)
.unwrap();
mouse_move.forget();
#[allow(trivial_casts)] let mouse_leave: Closure<dyn Fn(MouseEvent)> =
Closure::wrap(Box::new(move |_event: MouseEvent| {
if let Some(ref waker) = state().waker {
state().input = Some(Input::PointerLeave);
waker.wake_by_ref();
}
}));
web_sys::window()
.unwrap()
.add_event_listener_with_callback(
"mouseout",
mouse_leave.as_ref().unchecked_ref(),
)
.unwrap();
mouse_leave.forget();
}