slint/internal/backends/gl/wasm_input_helper.rs
Olivier Goffart b4bbe0b6df
Wasm: Hack to get input working despite composition (#1101)
Since we don't have yet support for IME in Slint, simulate that by inserting
the text anyway, and rollback using backspaces.
2022-03-24 21:08:44 +01:00

275 lines
11 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
//! Helper for wasm that adds a hidden `<input>` and process its events
//!
//! Without it, the key event are sent to the canvas and processed by winit.
//! But this winit handling doesn't show the keyboard on mobile devices, and
//! also has bugs as the modifiers are not reported the same way and we don't
//! record them.
//!
//! This just interpret the keyup and keydown events. But this is not working
//! on mobile either as we only get these for a bunch of non-printable key
//! that do not interact with the composing input. For anything else we
//! check that we get input event when no normal key are pressed, and we send
//! that as text. Ignore the composition event until the end.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
use i_slint_core::input::{KeyEvent, KeyEventType, KeyboardModifiers};
use i_slint_core::SharedString;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::JsCast;
pub struct WasmInputHelper {
input: web_sys::HtmlInputElement,
}
#[derive(Default)]
struct WasmInputState {
/// If there was a "keydown" event recieved not part of a composition
has_key_down: bool,
/// The current composing text
composition: String,
}
impl WasmInputState {
/// Update the composition text and return the number of character to rollback and the string to add
fn text_from_compose(&mut self, data: String, is_end: bool) -> (SharedString, usize) {
let mut data_iter = data.char_indices().peekable();
let mut composition_iter = self.composition.chars().peekable();
// Skip common prefix
while let (Some(c), Some((_, d))) = (composition_iter.peek(), data_iter.peek()) {
if c != d {
break;
}
composition_iter.next();
data_iter.next();
}
let to_delete = composition_iter.count();
let result = if let Some((idx, _)) = data_iter.next() {
SharedString::from(&data[idx..])
} else {
SharedString::default()
};
self.composition = if is_end { String::new() } else { data };
(result, to_delete)
}
}
impl WasmInputHelper {
#[allow(unused)]
pub fn new(
window: Weak<i_slint_core::window::Window>,
canvas: web_sys::HtmlCanvasElement,
) -> Self {
let input = web_sys::window()
.unwrap()
.document()
.unwrap()
.create_element("input")
.unwrap()
.dyn_into::<web_sys::HtmlInputElement>()
.unwrap();
let style = input.style();
style.set_property("z-index", "-1").unwrap();
style.set_property("position", "absolute").unwrap();
style.set_property("left", &format!("{}px", canvas.offset_left())).unwrap();
style.set_property("top", &format!("{}px", canvas.offset_top())).unwrap();
style.set_property("width", &format!("{}px", canvas.offset_width())).unwrap();
style.set_property("height", &format!("{}px", canvas.offset_height())).unwrap();
style.set_property("opacity", "0").unwrap(); // Hide the cursor on mobile Safari
input.set_attribute("autocapitalize", "none").unwrap(); // Otherwise everything would be capitalized as we need to clear the input
canvas.before_with_node_1(&input).unwrap();
let mut h = Self { input };
let shared_state = Rc::new(RefCell::new(WasmInputState::default()));
let win = window.clone();
h.add_event_listener("blur", move |_: web_sys::Event| {
// Make sure that the window gets marked as unfocused when the focus leaves the input
if let Some(window) = win.upgrade() {
if !canvas.matches(":focus").unwrap_or(false) {
window.set_active(false);
window.set_focus(false);
}
}
});
let win = window.clone();
let shared_state2 = shared_state.clone();
h.add_event_listener("keydown", move |e: web_sys::KeyboardEvent| {
if let (Some(window), Some(text)) = (win.upgrade(), event_text(&e)) {
shared_state2.borrow_mut().has_key_down = true;
window.process_key_input(&KeyEvent {
modifiers: modifiers(&e),
text,
event_type: KeyEventType::KeyPressed,
});
}
});
let win = window.clone();
let shared_state2 = shared_state.clone();
h.add_event_listener("keyup", move |e: web_sys::KeyboardEvent| {
if let (Some(window), Some(text)) = (win.upgrade(), event_text(&e)) {
shared_state2.borrow_mut().has_key_down = false;
window.process_key_input(&KeyEvent {
modifiers: modifiers(&e),
text,
event_type: KeyEventType::KeyReleased,
});
}
});
let win = window.clone();
let shared_state2 = shared_state.clone();
let input = h.input.clone();
h.add_event_listener("input", move |e: web_sys::InputEvent| {
if let (Some(window), Some(data)) = (win.upgrade(), e.data()) {
if !e.is_composing() && e.input_type() != "insertCompositionText" {
if !shared_state2.borrow_mut().has_key_down {
let text = SharedString::from(data.as_str());
window.clone().process_key_input(&KeyEvent {
modifiers: Default::default(),
text: text.clone(),
event_type: KeyEventType::KeyPressed,
});
window.process_key_input(&KeyEvent {
modifiers: Default::default(),
text,
event_type: KeyEventType::KeyReleased,
});
shared_state2.borrow_mut().has_key_down = false;
}
input.set_value("");
}
}
});
for event in ["compositionend", "compositionupdate"] {
let win = window.clone();
let shared_state2 = shared_state.clone();
let input = h.input.clone();
h.add_event_listener(event, move |e: web_sys::CompositionEvent| {
if let (Some(window), Some(data)) = (win.upgrade(), e.data()) {
let is_end = event == "compositionend";
let (text, to_delete) =
shared_state2.borrow_mut().text_from_compose(data, is_end);
if to_delete > 0 {
let mut buffer = [0; 6];
let backspace = SharedString::from(
i_slint_core::input::key_codes::Backspace.encode_utf8(&mut buffer)
as &str,
);
for _ in 0..to_delete {
window.clone().process_key_input(&KeyEvent {
modifiers: Default::default(),
text: backspace.clone(),
event_type: KeyEventType::KeyPressed,
});
}
}
window.clone().process_key_input(&KeyEvent {
modifiers: Default::default(),
text: text.clone(),
event_type: KeyEventType::KeyPressed,
});
window.process_key_input(&KeyEvent {
modifiers: Default::default(),
text,
event_type: KeyEventType::KeyReleased,
});
if is_end {
input.set_value("");
}
}
});
}
h
}
/// Returns wether the fake input element has focus
pub fn has_focus(&self) -> bool {
self.input.matches(":focus").unwrap_or(false)
}
pub fn show(&self) {
self.input.style().set_property("visibility", "visible").unwrap();
self.input.focus().unwrap();
}
pub fn hide(&self) {
self.input.blur().unwrap();
self.input.style().set_property("visibility", "hidden").unwrap();
}
fn add_event_listener<Arg: FromWasmAbi + 'static>(
&mut self,
event: &str,
closure: impl Fn(Arg) + 'static,
) {
let closure = move |arg: Arg| {
closure(arg);
crate::event_loop::GLOBAL_PROXY.with(|global_proxy| {
if let Ok(mut x) = global_proxy.try_borrow_mut() {
if let Some(proxy) = &mut *x {
proxy.send_event(crate::event_loop::CustomEvent::WakeEventLoopWorkaround)
}
}
});
};
let closure = Closure::wrap(Box::new(closure) as Box<dyn Fn(_)>);
self.input
.add_event_listener_with_callback(event, closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
}
fn event_text(e: &web_sys::KeyboardEvent) -> Option<SharedString> {
if e.is_composing() {
return None;
}
let key = e.key();
let convert = |char: char| {
let mut buffer = [0; 6];
Some(SharedString::from(char.encode_utf8(&mut buffer) as &str))
};
macro_rules! check_non_printable_code {
($($char:literal # $name:ident # $($_qt:ident)|* # $($_winit:ident)|* ;)*) => {
match key.as_str() {
$(stringify!($name) => {
return convert($char);
})*
// Why did we diverge from DOM there?
"ArrowLeft" => return convert(i_slint_core::input::key_codes::LeftArrow),
"ArrowUp" => return convert(i_slint_core::input::key_codes::UpArrow),
"ArrowRight" => return convert(i_slint_core::input::key_codes::RightArrow),
"ArrowDown" => return convert(i_slint_core::input::key_codes::DownArrow),
"Enter" => return convert(i_slint_core::input::key_codes::Return),
_ => (),
}
};
}
i_slint_common::for_each_special_keys!(check_non_printable_code);
if key.chars().count() == 1 {
Some(key.as_str().into())
} else {
None
}
}
fn modifiers(e: &web_sys::KeyboardEvent) -> KeyboardModifiers {
KeyboardModifiers {
alt: e.alt_key(),
control: e.ctrl_key(),
meta: e.meta_key(),
shift: e.shift_key(),
}
}