use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use crate::cursor::CursorIcon;
use crate::event::{Key, Modifiers};
pub fn key(name: &str) -> Option<Key> {
Some(match name {
"Backspace" => Key::Backspace,
"Delete" => Key::Delete,
"Insert" => Key::Insert,
"ArrowLeft" => Key::ArrowLeft,
"ArrowRight" => Key::ArrowRight,
"ArrowUp" => Key::ArrowUp,
"ArrowDown" => Key::ArrowDown,
"Home" => Key::Home,
"End" => Key::End,
"Tab" => Key::Tab,
"Enter" => Key::Enter,
"Escape" => Key::Escape,
" " => Key::Char(' '),
s if s.chars().count() == 1 => Key::Char(s.chars().next()?),
s => Key::Other(s.to_string()),
})
}
pub fn cursor_style(icon: CursorIcon) -> String {
format!("cursor:{}", icon.to_css())
}
fn targets_dom_editor(event: &web_sys::Event) -> bool {
let Some(target) = event.target() else {
return false;
};
if let Some(el) = target.dyn_ref::<web_sys::Element>() {
let tag = el.tag_name();
if tag.eq_ignore_ascii_case("input")
|| tag.eq_ignore_ascii_case("textarea")
|| tag.eq_ignore_ascii_case("select")
{
return true;
}
}
if let Some(html) = target.dyn_ref::<web_sys::HtmlElement>() {
if html.is_content_editable() {
return true;
}
}
false
}
fn modifiers_of(e: &web_sys::KeyboardEvent) -> Modifiers {
Modifiers {
shift: e.shift_key(),
ctrl: e.ctrl_key(),
alt: e.alt_key(),
meta: e.meta_key(),
}
}
fn should_prevent_default(k: &Key, mods: Modifiers) -> bool {
if mods.ctrl || mods.meta {
return matches!(k, Key::Char(c)
if matches!(c.to_ascii_lowercase(), 'c' | 'x' | 'a' | 'z' | 'y'))
&& !mods.alt;
}
if mods.alt {
return false;
}
matches!(
k,
Key::Char(_)
| Key::ArrowLeft
| Key::ArrowRight
| Key::ArrowUp
| Key::ArrowDown
| Key::Backspace
| Key::Delete
| Key::Home
| Key::End
| Key::Enter
)
}
pub fn install_keyboard_listeners(on_key: impl FnMut(Key, Modifiers, bool) + 'static) {
let Some(window) = web_sys::window() else {
return;
};
let on_key: Rc<RefCell<dyn FnMut(Key, Modifiers, bool)>> = Rc::new(RefCell::new(on_key));
let down_cb = {
let on_key = Rc::clone(&on_key);
Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(move |e: web_sys::KeyboardEvent| {
if targets_dom_editor(&e) || e.is_composing() {
return;
}
let mods = modifiers_of(&e);
let name = e.key();
if (mods.ctrl || mods.meta) && name.eq_ignore_ascii_case("v") {
return;
}
let Some(k) = key(&name) else {
return;
};
if should_prevent_default(&k, mods) {
e.prevent_default();
}
(on_key.borrow_mut())(k, mods, true);
})
};
let _ = window
.add_event_listener_with_callback("keydown", down_cb.as_ref().unchecked_ref());
down_cb.forget();
let up_cb = {
let on_key = Rc::clone(&on_key);
Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(move |e: web_sys::KeyboardEvent| {
if targets_dom_editor(&e) {
return;
}
if let Some(k) = key(&e.key()) {
(on_key.borrow_mut())(k, modifiers_of(&e), false);
}
})
};
let _ = window.add_event_listener_with_callback("keyup", up_cb.as_ref().unchecked_ref());
up_cb.forget();
for event_name in ["copy", "cut"] {
let cb = Closure::<dyn FnMut(web_sys::ClipboardEvent)>::new(
move |e: web_sys::ClipboardEvent| {
let Some(text) = crate::wasm_clipboard::get() else {
return;
};
let Some(data) = e.clipboard_data() else {
return;
};
let _ = data.set_data("text/plain", &text);
if let Some(html) = crate::wasm_clipboard::get_html() {
let _ = data.set_data("text/html", &html);
}
e.prevent_default();
},
);
let _ = window.add_event_listener_with_callback(event_name, cb.as_ref().unchecked_ref());
cb.forget();
}
let paste_cb = {
let on_key = Rc::clone(&on_key);
Closure::<dyn FnMut(web_sys::ClipboardEvent)>::new(move |e: web_sys::ClipboardEvent| {
if targets_dom_editor(&e) {
return;
}
let Some(data) = e.clipboard_data() else {
return;
};
let Ok(text) = data.get_data("text/plain") else {
return;
};
if text.is_empty() {
return;
}
e.prevent_default();
crate::wasm_clipboard::set(&text);
let mods = Modifiers {
ctrl: true,
..Modifiers::default()
};
(on_key.borrow_mut())(Key::Char('v'), mods, true);
})
};
let _ = window.add_event_listener_with_callback("paste", paste_cb.as_ref().unchecked_ref());
paste_cb.forget();
}