|
| 1 | +// EndBASIC |
| 2 | +// Copyright 2021 Julio Merino |
| 3 | +// |
| 4 | +// Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 5 | +// use this file except in compliance with the License. You may obtain a copy |
| 6 | +// of the License at: |
| 7 | +// |
| 8 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +// |
| 10 | +// Unless required by applicable law or agreed to in writing, software |
| 11 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | +// License for the specific language governing permissions and limitations |
| 14 | +// under the License. |
| 15 | + |
| 16 | +//! Keyboard input tools for the web UI. |
| 17 | +
|
| 18 | +use async_channel::{self, Receiver, Sender, TryRecvError}; |
| 19 | +use endbasic_std::console::Key; |
| 20 | +use std::io; |
| 21 | +use wasm_bindgen::prelude::*; |
| 22 | +use xterm_js_rs::OnKeyEvent; |
| 23 | + |
| 24 | +/// Converts an xterm.js key event into our own `Key` representation. |
| 25 | +fn on_key_event_into_key(event: OnKeyEvent) -> Key { |
| 26 | + let dom_event = event.dom_event(); |
| 27 | + match dom_event.key_code() as u8 { |
| 28 | + 8 => Key::Backspace, |
| 29 | + 9 => Key::Tab, |
| 30 | + 10 => Key::NewLine, |
| 31 | + 13 => Key::CarriageReturn, |
| 32 | + 27 => Key::Escape, |
| 33 | + 35 => Key::End, |
| 34 | + 36 => Key::Home, |
| 35 | + 37 => Key::ArrowLeft, |
| 36 | + 38 => Key::ArrowUp, |
| 37 | + 39 => Key::ArrowRight, |
| 38 | + 40 => Key::ArrowDown, |
| 39 | + b'A' if dom_event.ctrl_key() => Key::Home, |
| 40 | + b'B' if dom_event.ctrl_key() => Key::ArrowLeft, |
| 41 | + b'C' if dom_event.ctrl_key() => Key::Interrupt, |
| 42 | + b'D' if dom_event.ctrl_key() => Key::Eof, |
| 43 | + b'E' if dom_event.ctrl_key() => Key::End, |
| 44 | + b'F' if dom_event.ctrl_key() => Key::ArrowRight, |
| 45 | + b'J' if dom_event.ctrl_key() => Key::NewLine, |
| 46 | + b'M' if dom_event.ctrl_key() => Key::NewLine, |
| 47 | + b'N' if dom_event.ctrl_key() => Key::ArrowDown, |
| 48 | + b'P' if dom_event.ctrl_key() => Key::ArrowUp, |
| 49 | + _ => { |
| 50 | + let printable = !dom_event.alt_key() && !dom_event.ctrl_key() && !dom_event.meta_key(); |
| 51 | + let chars = event.key().chars().collect::<Vec<char>>(); |
| 52 | + if printable && chars.len() == 1 { |
| 53 | + Key::Char(chars[0]) |
| 54 | + } else { |
| 55 | + Key::Unknown(format!("<keycode={}>", dom_event.key_code())) |
| 56 | + } |
| 57 | + } |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +/// Interface to implement an on-screen keyboard to provide keys that may not be available on |
| 62 | +/// mobile keyboards. |
| 63 | +#[wasm_bindgen] |
| 64 | +pub struct OnScreenKeyboard { |
| 65 | + on_key_tx: Sender<Key>, |
| 66 | +} |
| 67 | + |
| 68 | +#[wasm_bindgen] |
| 69 | +impl OnScreenKeyboard { |
| 70 | + /// Generates a fake Escape key press. |
| 71 | + pub fn press_escape(&self) { |
| 72 | + self.on_key_tx.try_send(Key::Escape).expect("Send to unbounded channel must succeed") |
| 73 | + } |
| 74 | + |
| 75 | + /// Generates a fake arrow up key press. |
| 76 | + pub fn press_arrow_up(&self) { |
| 77 | + self.on_key_tx.try_send(Key::ArrowUp).expect("Send to unbounded channel must succeed") |
| 78 | + } |
| 79 | + |
| 80 | + /// Generates a fake arrow down key press. |
| 81 | + pub fn press_arrow_down(&self) { |
| 82 | + self.on_key_tx.try_send(Key::ArrowDown).expect("Send to unbounded channel must succeed") |
| 83 | + } |
| 84 | + |
| 85 | + /// Generates a fake arrow left key press. |
| 86 | + pub fn press_arrow_left(&self) { |
| 87 | + self.on_key_tx.try_send(Key::ArrowLeft).expect("Send to unbounded channel must succeed") |
| 88 | + } |
| 89 | + |
| 90 | + /// Generates a fake arrow up key press. |
| 91 | + pub fn press_arrow_right(&self) { |
| 92 | + self.on_key_tx.try_send(Key::ArrowRight).expect("Send to unbounded channel must succeed") |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +/// Interface to interact with the browser's input, be it via a real keyboard or our custom |
| 97 | +/// on-screen keyboard. |
| 98 | +pub struct WebInput { |
| 99 | + on_key_rx: Receiver<Key>, |
| 100 | + on_key_tx: Sender<Key>, |
| 101 | +} |
| 102 | + |
| 103 | +impl Default for WebInput { |
| 104 | + fn default() -> Self { |
| 105 | + let (on_key_tx, on_key_rx) = async_channel::unbounded(); |
| 106 | + Self { on_key_rx, on_key_tx } |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +impl WebInput { |
| 111 | + /// Generates a closure that xterm.js can use to handle key events. |
| 112 | + pub(crate) fn terminal_on_key(&self) -> Closure<dyn FnMut(OnKeyEvent)> { |
| 113 | + let on_key_tx = self.on_key_tx.clone(); |
| 114 | + Closure::wrap(Box::new(move |e| { |
| 115 | + on_key_tx |
| 116 | + .try_send(on_key_event_into_key(e)) |
| 117 | + .expect("Send to unbounded channel must succeed") |
| 118 | + }) as Box<dyn FnMut(OnKeyEvent)>) |
| 119 | + } |
| 120 | + |
| 121 | + /// Generates a new `OnScreenKeyboard` that can inject key events. |
| 122 | + pub(crate) fn on_screen_keyboard(&self) -> OnScreenKeyboard { |
| 123 | + OnScreenKeyboard { on_key_tx: self.on_key_tx.clone() } |
| 124 | + } |
| 125 | + |
| 126 | + /// Gets the next key event, if one is available. |
| 127 | + pub(crate) async fn try_recv(&mut self) -> io::Result<Option<Key>> { |
| 128 | + match self.on_key_rx.try_recv() { |
| 129 | + Ok(k) => Ok(Some(k)), |
| 130 | + Err(TryRecvError::Empty) => Ok(None), |
| 131 | + Err(TryRecvError::Closed) => panic!("Channel unexpectedly closed"), |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + /// Gets the next key event, waiting until one is available. |
| 136 | + pub(crate) async fn recv(&mut self) -> io::Result<Key> { |
| 137 | + Ok(self.on_key_rx.recv().await.unwrap()) |
| 138 | + } |
| 139 | +} |
0 commit comments