Skip to content

Instantly share code, notes, and snippets.

@maroider
Last active March 18, 2021 18:36
Show Gist options
  • Save maroider/6c8bcff3642a07d111ed85bf077a4c8a to your computer and use it in GitHub Desktop.
Save maroider/6c8bcff3642a07d111ed85bf077a4c8a to your computer and use it in GitHub Desktop.
[Winit] New keyboard API test
cargo build --example keyboard --target=wasm32-unknown-unknown --features web-sys
wasm-bindgen --out-dir examples/keyboard --target web --no-typescript target\wasm32-unknown-unknown\debug\examples\keyboard.wasm
cargo build --example keyboard --target=wasm32-unknown-unknown --features web-sys
wasm-bindgen --out-dir examples/keyboard --target web --no-typescript target/wasm32-unknown-unknown/debug/examples/keyboard.wasm
<html>
<head>
<meta charset="UTF-8" />
<style>
body {
display: flex;
background: linear-gradient(
135deg,
white 0%,
white 49%,
black 49%,
black 51%,
white 51%,
white 100%
);
background-repeat: repeat;
background-size: 20px 20px;
}
canvas {
background-color: white;
}
#table-container {
background: #dddddddd;
overflow-y: scroll;
max-height: 768;
}
table {
border-collapse: collapse;
}
th,
td {
padding-left: 5px;
padding-right: 5px;
padding-top: 1px;
padding-bottom: 1px;
border: 1px solid black;
}
</style>
</head>
<script type="module">
import init from "./keyboard/keyboard.js";
init();
</script>
<body></body>
</html>
//! Test program for keyboard input.
//!
//! This program will print out markdown tables of sequences of key events.
//!
//! It will automatically terminate a table when it can see that all buttons have been released.
//! You can press the middle mouse button to terminate the table manually.
//! When the current table is empty, the middle mouse button can be used to switch between manual
//! and automatic mode. Manual mode is indicated in the title bar.
use std::collections::HashMap;
use winit::{
event::{DeviceEvent, ElementState, Event, KeyEvent, MouseButton, WindowEvent},
event_loop::{ControlFlow, EventLoop},
keyboard::{Key, KeyCode, NativeKeyCode},
window::WindowBuilder,
};
#[allow(dead_code)]
mod column {
pub const NUMBER: &str = "Number";
pub const KIND: &str = "Kind";
pub const SYNTH: &str = "Synth";
pub const STATE: &str = "State";
pub const KEY_CODE: &str = "KeyCode";
pub const KEY: &str = "Key";
pub const LOCATION: &str = "Location";
pub const TEXT: &str = "Text";
pub const MODIFIERS: &str = "Modifiers";
pub const KEY_NO_MOD: &str = "Key (no modifiers)";
pub const TEXT_ALL_MODS: &str = "Text (all modifiers)";
pub const SCAN_CODE: &str = "Scancode";
}
#[cfg(feature = "web-sys")]
mod wasm {
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn run() {
console_log::init_with_level(log::Level::Debug).unwrap();
super::main();
}
}
fn main() {
#[cfg(not(target_arch = "wasm32"))]
simple_logger::SimpleLogger::new().init().unwrap();
let event_loop = EventLoop::new();
let base_window_title = "A fantastic window!";
let window = WindowBuilder::new()
.with_title(base_window_title)
.build(&event_loop)
.unwrap();
#[rustfmt::skip]
let table = {
let mut table = Table::new();
table.add_column(TableColumn {
header: column::NUMBER.to_string() , normal_width: 0 , extended_width: 0 , use_extended_width: false, enabled: true,
});
table.add_column(TableColumn {
header: column::KIND.to_string() , normal_width: 6 , extended_width: 0 , use_extended_width: false, enabled: true,
});
table.add_column(TableColumn {
header: column::SYNTH.to_string() , normal_width: 5 , extended_width: 0 , use_extended_width: false, enabled: true,
});
table.add_column(TableColumn {
header: column::STATE.to_string() , normal_width: 8 , extended_width: 0 , use_extended_width: false, enabled: true,
});
table.add_column(TableColumn {
header: column::KEY_CODE.to_string() , normal_width: 20, extended_width: 37, use_extended_width: false, enabled: true,
});
table.add_column(TableColumn {
header: column::KEY.to_string() , normal_width: 25, extended_width: 42, use_extended_width: true , enabled: true,
});
table.add_column(TableColumn {
header: column::LOCATION.to_string() , normal_width: 0 , extended_width: 0 , use_extended_width: false, enabled: true,
});
table.add_column(TableColumn {
header: column::TEXT.to_string() , normal_width: 12, extended_width: 0 , use_extended_width: false, enabled: true,
});
table.add_column(TableColumn {
header: column::MODIFIERS.to_string() , normal_width: 0 , extended_width: 0 , use_extended_width: false, enabled: true,
});
#[cfg(not(target_arch = "wasm32"))]
table.add_column(TableColumn {
header: column::KEY_NO_MOD.to_string() , normal_width: 25, extended_width: 42, use_extended_width: false, enabled: true,
});
#[cfg(not(target_arch = "wasm32"))]
table.add_column(TableColumn {
header: column::TEXT_ALL_MODS.to_string(), normal_width: 0 , extended_width: 0 , use_extended_width: false, enabled: true,
});
#[cfg(not(target_arch = "wasm32"))]
table.add_column(TableColumn {
header: column::SCAN_CODE.to_string() , normal_width: 0 , extended_width: 0 , use_extended_width: false, enabled: false,
});
table
};
#[cfg(feature = "web-sys")]
let mut table_printer = {
use winit::platform::web::WindowExtWebSys;
let canvas = window.canvas();
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
body.append_child(&canvas)
.expect("Append canvas to HTML body");
HtmlTablePrinter::new(document, &body, &table)
};
#[cfg(not(feature = "web-sys"))]
let mut table_printer = StdoutTablePrinter::new();
let mut focused = true;
let mut event_number = 0u16;
let mut pressed_count = 0i32;
let mut modifiers = Default::default();
let mut manual_mode = false;
table_printer.begin_new_table(&table);
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::Focused(focus),
..
} => {
focused = focus;
}
Event::WindowEvent {
event:
WindowEvent::KeyboardInput {
event,
is_synthetic,
..
},
..
} => {
if !event.repeat {
table
.print_table_line()
.column(column::NUMBER, event_number)
.column(column::KIND, "Window")
.column(column::SYNTH, is_synthetic)
.column_with(column::STATE, || format!("{:?}", event.state))
.column_with(column::KEY_CODE, || key_code_to_string(event.physical_key))
.column_with(column::KEY, || key_to_string(event.logical_key))
.column_with(column::LOCATION, || format!("{:?}", event.location))
.column_with(column::TEXT, || {
event.text.map(nice_text).unwrap_or_else(|| "".to_string())
})
.column_with(column::KEY_NO_MOD, || key_without_modifiers(&event))
.column_with(column::TEXT_ALL_MODS, || text_with_all_modifiers(&event))
.print(&mut table_printer);
event_number += 1;
match event.state {
ElementState::Pressed => pressed_count += 1,
ElementState::Released => pressed_count -= 1,
}
}
}
Event::DeviceEvent {
event: DeviceEvent::Key(event),
..
} => {
if !event.repeat && (focused || pressed_count > 0) {
table
.print_table_line()
.column(column::NUMBER, event_number)
.column(column::KIND, "Device")
.column_with(column::STATE, || format!("{:?}", event.state))
.column_with(column::KEY_CODE, || key_code_to_string(event.physical_key))
.print(&mut table_printer);
event_number += 1;
match event.state {
ElementState::Pressed => pressed_count += 1,
ElementState::Released => pressed_count -= 1,
}
}
}
Event::WindowEvent {
event: WindowEvent::ModifiersChanged(new_modifiers),
..
} => {
modifiers = new_modifiers;
if !modifiers.is_empty() || event_number != 0 {
table
.print_table_line()
.column(column::NUMBER, event_number)
.column(column::KIND, "ModC")
.column_with(column::MODIFIERS, || {
format!("{:?}", modifiers).replace("|", "")
})
.print(&mut table_printer);
event_number += 1;
}
}
Event::WindowEvent {
event: WindowEvent::ReceivedImeText(text),
..
} => {
table
.print_table_line()
.column(column::NUMBER, event_number)
.column(column::KIND, "IME")
.column_with(column::TEXT, || format!("{:?}", text))
.print(&mut table_printer);
event_number += 1;
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Middle,
..
},
..
} => {
if manual_mode {
if event_number == 0 {
manual_mode = false;
} else {
table_printer.begin_new_table(&table);
event_number = 0;
pressed_count = 0;
modifiers = Default::default();
}
} else {
if event_number == 0 {
manual_mode = true;
} else {
pressed_count = 0;
modifiers = Default::default();
}
}
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
if !manual_mode {
*control_flow = ControlFlow::Exit
}
}
Event::MainEventsCleared => {
if manual_mode {
window.set_title(&format!("{} - Manual Mode", base_window_title));
} else {
window.set_title(base_window_title);
}
}
_ => (),
}
if !manual_mode {
if pressed_count == 0 && modifiers.is_empty() {
if event_number != 0 {
table_printer.begin_new_table(&table);
event_number = 0;
}
}
}
});
}
fn key_to_string(key: Key) -> String {
match key {
Key::Unidentified(native_key_code) => format!(
"Unidentified({})",
native_key_code_to_string(native_key_code)
),
_ => format!("{:?}", key),
}
}
fn key_code_to_string(code: KeyCode) -> String {
match code {
KeyCode::Unidentified(native_key_code) => format!(
"Unidentified({})",
native_key_code_to_string(native_key_code)
),
_ => format!("{:?}", code),
}
}
fn native_key_code_to_string(native_key_code: NativeKeyCode) -> String {
match native_key_code {
winit::keyboard::NativeKeyCode::Windows(scancode) => {
format!("Windows({:#X})", scancode as u32)
}
winit::keyboard::NativeKeyCode::MacOS(keycode) => {
format!("MacOS({:#X})", keycode)
}
winit::keyboard::NativeKeyCode::XKB(keycode) => {
format!("XKB({:#X})", keycode)
}
_ => unimplemented!(),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn key_without_modifiers(event: &KeyEvent) -> String {
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
format!("{:?}", event.key_without_modifiers())
}
#[cfg(target_arch = "wasm32")]
fn key_without_modifiers(_: &KeyEvent) -> &'static str {
""
}
#[cfg(not(target_arch = "wasm32"))]
fn text_with_all_modifiers(event: &KeyEvent) -> String {
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
event
.text_with_all_modifiers()
.map(nice_text)
.unwrap_or_else(String::new)
}
#[cfg(target_arch = "wasm32")]
fn text_with_all_modifiers(_: &KeyEvent) -> &'static str {
""
}
fn nice_text(text: &str) -> String {
if text.chars().any(|c| c.is_control()) {
format!("{:?}", text)
} else {
text.to_string()
}
}
struct Table {
columns: Vec<TableColumn>,
}
impl Table {
fn new() -> Self {
Self {
columns: Vec::new(),
}
}
fn add_column(&mut self, column: TableColumn) {
self.columns.push(column);
}
fn print_table_line(&self) -> RowBuilder<'_> {
RowBuilder::new(self)
}
}
struct TableColumn {
header: String,
normal_width: usize,
extended_width: usize,
use_extended_width: bool,
enabled: bool,
}
impl TableColumn {
fn width(&self) -> usize {
if self.use_extended_width {
self.extended_width
} else {
self.normal_width
}
.max(self.header.len())
}
}
struct RowBuilder<'a> {
table: &'a Table,
column_values: HashMap<String, String>,
}
impl<'a> RowBuilder<'a> {
fn new(table: &'a Table) -> Self {
Self {
table,
column_values: HashMap::new(),
}
}
fn column<T>(mut self, column: &str, value: T) -> Self
where
T: ToString,
{
if let Some(col) = self.table.columns.iter().find(|col| col.header == column) {
if col.enabled {
self.column_values
.insert(column.to_string(), value.to_string());
}
}
self
}
fn column_with<F: FnOnce() -> T, T>(mut self, column: &str, f: F) -> Self
where
T: ToString,
{
if let Some(col) = self.table.columns.iter().find(|col| col.header == column) {
if col.enabled {
self.column_values
.insert(column.to_string(), f().to_string());
}
}
self
}
fn print<P: TablePrinter>(self, printer: &mut P) {
printer.print_row(self)
}
}
trait TablePrinter {
fn begin_new_table(&mut self, table: &Table);
fn print_row(&mut self, row: RowBuilder<'_>);
}
#[cfg(not(target_arch = "wasm32"))]
struct StdoutTablePrinter {
ioprinter: IoWriteTablePrinter,
}
#[cfg(not(target_arch = "wasm32"))]
impl StdoutTablePrinter {
fn new() -> Self {
Self {
ioprinter: IoWriteTablePrinter::new(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl TablePrinter for StdoutTablePrinter {
fn begin_new_table(&mut self, table: &Table) {
use std::io::{self, Write as _};
let stdout = io::stdout();
let mut out = stdout.lock();
writeln!(out).unwrap();
self.ioprinter.begin_new_table(table, &mut out);
}
fn print_row(&mut self, row: RowBuilder<'_>) {
use std::io;
let stdout = io::stdout();
let mut out = stdout.lock();
self.ioprinter.print_row(row, &mut out);
}
}
#[cfg(target_arch = "wasm32")]
struct HtmlTablePrinter {
document: web_sys::Document,
table_container: web_sys::Element,
table_element: web_sys::Element,
tbody: web_sys::Element,
last_table: Option<web_sys::Element>,
ioprinter: IoWriteTablePrinter,
markdown_table_buffer: Vec<u8>,
}
#[cfg(target_arch = "wasm32")]
impl HtmlTablePrinter {
fn new(document: web_sys::Document, body: &web_sys::HtmlElement, table: &Table) -> Self {
let (table_element, tbody) = Self::create_new_table(&document, table);
let table_container = document.create_element("div").unwrap();
table_container.set_id("table-container");
table_container.append_child(&table_element).unwrap();
body.append_child(&table_container).unwrap();
Self {
document,
table_container,
table_element,
tbody,
last_table: None,
ioprinter: IoWriteTablePrinter::new(),
markdown_table_buffer: Vec::new(),
}
}
fn create_new_table(
document: &web_sys::Document,
table: &Table,
) -> (web_sys::Element, web_sys::Element) {
let table_element = document.create_element("table").unwrap();
let thead = document.create_element("thead").unwrap();
let header_row = document.create_element("tr").unwrap();
for column in table.columns.iter() {
let header = document.create_element("th").unwrap();
header.set_text_content(Some(&column.header));
header_row.append_child(&header).unwrap();
}
thead.append_child(&header_row).unwrap();
table_element.append_child(&thead).unwrap();
let tbody = document.create_element("tbody").unwrap();
table_element.append_child(&tbody).unwrap();
(table_element, tbody)
}
}
#[cfg(target_arch = "wasm32")]
impl TablePrinter for HtmlTablePrinter {
fn begin_new_table(&mut self, table: &Table) {
let mardown_table = std::str::from_utf8(&self.markdown_table_buffer)
.unwrap()
.to_string();
self.markdown_table_buffer.clear();
self.ioprinter
.begin_new_table(table, &mut self.markdown_table_buffer);
// TODO: Don't require this hack, maybe.
if self.tbody.child_element_count() == 0 {
return;
}
let (new_table, new_tbody) = Self::create_new_table(&self.document, table);
self.table_container
.replace_child(&new_table, &self.table_element)
.unwrap();
let details = self.document.create_element("details").unwrap();
details.set_attribute("open", "").unwrap();
let summary = self.document.create_element("summary").unwrap();
summary.set_text_content(Some("Event table"));
let button = self.document.create_element("button").unwrap();
button
.set_attribute(
"onclick",
&format!(r#"navigator.clipboard.writeText(`{}`)"#, mardown_table),
)
.unwrap();
button.set_class_name("copy-to-clipboard");
button.set_text_content(Some("Copy to clipboard"));
summary.append_child(&button).unwrap();
details.append_child(&summary).unwrap();
details.append_child(&self.table_element).unwrap();
self.table_container
.insert_before(&details, self.last_table.as_deref())
.unwrap();
self.table_element = new_table;
self.tbody = new_tbody;
self.last_table = Some(details);
}
fn print_row(&mut self, row: RowBuilder<'_>) {
let tr = self.document.create_element("tr").unwrap();
for column in row.table.columns.iter() {
if !column.enabled {
continue;
}
let td = self.document.create_element("td").unwrap();
if let Some(value) = row.column_values.get(&column.header) {
td.set_text_content(Some(value));
}
tr.append_child(&td).unwrap();
}
self.tbody.append_child(&tr).unwrap();
self.ioprinter
.print_row(row, &mut self.markdown_table_buffer);
}
}
struct IoWriteTablePrinter {}
impl IoWriteTablePrinter {
fn new() -> Self {
Self {}
}
}
impl IoWriteTablePrinter {
fn begin_new_table<W>(&mut self, table: &Table, out: &mut W)
where
W: std::io::Write,
{
for column in table.columns.iter() {
if !column.enabled {
continue;
}
write!(
out,
"| {:<length$} ",
column.header,
length = column.width(),
)
.unwrap();
}
writeln!(out, "|").unwrap();
for column in table.columns.iter() {
if !column.enabled {
continue;
}
let mut buf = String::new();
for _ in 0..column.width() {
buf.push('-');
}
write!(out, "| {} ", buf).unwrap();
}
writeln!(out, "|").unwrap();
out.flush().unwrap();
}
fn print_row<W>(&mut self, row: RowBuilder<'_>, out: &mut W)
where
W: std::io::Write,
{
for column in row.table.columns.iter() {
if !column.enabled {
continue;
}
write!(
out,
"| {:<length$} ",
row.column_values
.get(&column.header)
.map(AsRef::as_ref)
.unwrap_or(""),
length = column.width(),
)
.unwrap();
}
writeln!(out, "|").unwrap();
out.flush().unwrap();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment