mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-29 13:24:48 +00:00

The situation differs depending if the widget show the virtual keyboard or not: For widget that don't show the virtual keyboard, we rely on the winit events, but for some reason we don't recieve WindowEvent::ModifiersChanged events with wasm. Since the event handling in winit is about to be rewriten, I did not bother reporting the bug upstream, but just work around by using the deprecated API. So that way shift + tab will no longer be understood as just tab. For widget using the virtual keyboard, then the event handling is in wasm_input_helper.rs. There is a couple of issues: - We must map shift+tab to backtab. - We need to prevent the default events to trigger, so that tab and other shortcuts don't take effect on the browser. Winit already inhibit these events so we must do the same otherwise tab and shift+tab would change the html focus. - By luck, tab used to give the focus back to the canvas before (see previous point) and that's why it worked. But now that we don't do that anymore, hiding the virtual keyboard should actually re-focus the canvas - That will cause the focus event to be intercepted by winit, and will cause recursions and borrow error, so we make sure that we do not recurse when getting the focus event
284 lines
11 KiB
Rust
284 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.
|
|
//! Since the slint core lib doesn't support composition yet, when we get
|
|
//! composition event, we just send that as key, and if the composition changes,
|
|
//! we just simulate a few backspaces.
|
|
|
|
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,
|
|
canvas: web_sys::HtmlCanvasElement,
|
|
}
|
|
|
|
#[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, canvas: canvas.clone() };
|
|
|
|
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)) {
|
|
e.prevent_default();
|
|
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)) {
|
|
e.prevent_default();
|
|
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) {
|
|
if self.has_focus() {
|
|
self.canvas.focus().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() {
|
|
"Tab" if e.shift_key() => return convert(i_slint_core::input::key_codes::Backtab),
|
|
$(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(),
|
|
}
|
|
}
|