Files
mousefree/src/wayland.rs
T

974 lines
32 KiB
Rust

//! Wayland backend: layer-shell overlay, virtual pointer, and keyboard input.
//!
//! Manages the Wayland connection, creates a full-screen layer-shell surface for
//! the overlay, and translates raw keyboard events into semantic [`KeyEvent`]s.
use std::collections::VecDeque;
use std::io::Write;
use std::os::fd::AsFd;
use std::time::{SystemTime, UNIX_EPOCH};
use wayland_client::protocol::wl_pointer::{Axis, AxisSource, ButtonState};
use wayland_client::{
delegate_noop,
protocol::{
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat, wl_shm,
wl_shm_pool, wl_surface,
},
Connection, Dispatch, EventQueue, QueueHandle, WEnum,
};
use wayland_protocols_wlr::{
layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1},
virtual_pointer::v1::client::{zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1},
};
use anyhow::{Context, Result};
// Linux evdev button codes.
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
// Linux evdev key codes for modifier keys.
const KEY_LCTRL: u32 = 29;
const KEY_LSHIFT: u32 = 42;
const KEY_LALT: u32 = 56;
const KEY_RSHIFT: u32 = 54;
const KEY_RCTRL: u32 = 97;
const KEY_RALT: u32 = 100;
// Modifier bitmasks in the Wayland `mods_depressed` field.
const MOD_SHIFT: u32 = 1;
const MOD_CTRL: u32 = 4;
const MOD_ALT: u32 = 8;
// Drag animation timing (~150ms total).
const DRAG_SETTLE_MS: u64 = 25;
const DRAG_PRESS_SETTLE_MS: u64 = 30;
const DRAG_INTERP_STEPS: u32 = 12;
const DRAG_STEP_DELAY_MS: u64 = 5;
const DRAG_RELEASE_SETTLE_MS: u64 = 35;
// Maximum number of event loop iterations to wait for a key release before
// giving up. Prevents an infinite hang if the compositor drops the event.
const KEY_RELEASE_MAX_DISPATCHES: u32 = 200;
// ===========================================================================
// Public key event type.
// ===========================================================================
/// Semantic key event emitted by the backend and consumed by the main loop.
pub enum KeyEvent {
/// A printable character (post-shift).
Char(char),
/// Ctrl held while pressing a character key.
CtrlChar(char),
/// Alt held while pressing a character key.
AltChar(char),
/// Space — mapped to left-click.
Click,
/// Enter — mapped to double-click.
DoubleClick,
/// Shift+Enter — mapped to triple-click.
TripleClick,
/// Period — mapped to right-click.
RightClick,
/// Escape or Ctrl-[ — close the overlay.
Close,
/// Backspace — undo one selection step.
Undo,
ScrollUp,
ScrollDown,
ScrollLeft,
ScrollRight,
}
// ===========================================================================
// Internal physical key abstraction.
// ===========================================================================
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PhysicalKey {
Char(char),
Space,
Enter,
Escape,
Backspace,
Up,
Down,
Left,
Right,
}
/// Maps a physical key to the semantic `KeyEvent` the main loop cares about.
fn physical_to_event(key: PhysicalKey) -> Option<KeyEvent> {
match key {
PhysicalKey::Space => Some(KeyEvent::Click),
PhysicalKey::Enter => Some(KeyEvent::DoubleClick),
PhysicalKey::Escape => Some(KeyEvent::Close),
PhysicalKey::Backspace => Some(KeyEvent::Undo),
PhysicalKey::Up => Some(KeyEvent::ScrollUp),
PhysicalKey::Down => Some(KeyEvent::ScrollDown),
PhysicalKey::Left => Some(KeyEvent::ScrollLeft),
PhysicalKey::Right => Some(KeyEvent::ScrollRight),
PhysicalKey::Char('.') => Some(KeyEvent::RightClick),
PhysicalKey::Char(c) => Some(KeyEvent::Char(c)),
}
}
// ===========================================================================
// WaylandBackend — public API for the main loop.
// ===========================================================================
/// Owns the Wayland connection, event queue, and all protocol objects.
///
/// Designed so that public methods can freely call `roundtrip` /
/// `blocking_dispatch` without borrowing conflicts.
pub struct WaylandBackend {
state: WaylandState,
event_queue: EventQueue<WaylandState>,
queue_handle: QueueHandle<WaylandState>,
}
impl WaylandBackend {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().context("connect to Wayland display")?;
let mut event_queue = conn.new_event_queue();
let queue_handle = event_queue.handle();
conn.display().get_registry(&queue_handle, ());
let mut state = WaylandState {
conn,
compositor: None,
shm: None,
surface: None,
layer_shell: None,
layer_surface: None,
seat: None,
vp_manager: None,
virtual_pointer: None,
screen_w: 0,
screen_h: 0,
configured: false,
pending_keys: VecDeque::new(),
shift_held: false,
ctrl_held: false,
alt_held: false,
awaiting_key_release: false,
};
event_queue
.roundtrip(&mut state)
.context("initial roundtrip")?;
// Verify required protocol support before proceeding.
if state.compositor.is_none() {
anyhow::bail!("compositor does not advertise wl_compositor");
}
if state.shm.is_none() {
anyhow::bail!("compositor does not advertise wl_shm");
}
if state.layer_shell.is_none() {
anyhow::bail!(
"compositor does not support wlr-layer-shell-unstable-v1. \
Supported compositors: Sway, Hyprland, river, etc."
);
}
if state.vp_manager.is_none() {
anyhow::bail!(
"compositor does not support wlr-virtual-pointer-unstable-v1. \
Supported compositors: Sway, Hyprland, river, etc."
);
}
// Bind the virtual pointer now that we know which globals are available.
if let Some(manager) = state.vp_manager.take() {
state.virtual_pointer =
Some(manager.create_virtual_pointer(state.seat.as_ref(), &queue_handle, ()));
}
state.init_layer_surface(&queue_handle)?;
event_queue
.roundtrip(&mut state)
.context("roundtrip after layer surface")?;
while !state.configured {
event_queue
.blocking_dispatch(&mut state)
.context("waiting for configure")?;
}
Ok(Self {
state,
event_queue,
queue_handle,
})
}
// -- Queries ------------------------------------------------------------
pub fn screen_size(&self) -> (u32, u32) {
(self.state.screen_w, self.state.screen_h)
}
// -- Event loop ---------------------------------------------------------
/// Blocks until the next key event arrives, or returns `None` when the
/// surface has been torn down (i.e. the overlay is closed).
pub fn next_key(&mut self) -> Result<Option<KeyEvent>> {
loop {
if let Some(key) = self.state.pending_keys.pop_front() {
return Ok(Some(key));
}
if self.state.surface.is_none() {
return Ok(None);
}
self.event_queue
.blocking_dispatch(&mut self.state)
.context("blocking_dispatch")?;
}
}
// -- Rendering ----------------------------------------------------------
/// Copy an ARGB8888 pixel buffer to the overlay surface via wl_shm.
pub fn present(&mut self, pixels: &[u8], width: u32, height: u32) -> Result<()> {
// Check availability before allocating resources.
let shm = self.state.shm.as_ref().context("wl_shm not available")?;
let surface = self
.state
.surface
.as_ref()
.context("wl_surface not available")?;
let stride = width * 4;
let mut file = tempfile::tempfile().context("create shm tempfile")?;
file.write_all(pixels).context("write pixel buffer")?;
let pool = shm.create_pool(file.as_fd(), pixels.len() as i32, &self.queue_handle, ());
let buffer = pool.create_buffer(
0,
width as i32,
height as i32,
stride as i32,
wl_shm::Format::Argb8888,
&self.queue_handle,
(),
);
surface.attach(Some(&buffer), 0, 0);
surface.damage_buffer(0, 0, width as i32, height as i32);
surface.commit();
pool.destroy();
Ok(())
}
// -- Mouse actions ------------------------------------------------------
/// Warp the virtual pointer to absolute screen coordinates.
pub fn move_mouse(&mut self, x: u32, y: u32) -> Result<()> {
if let Some(vp) = &self.state.virtual_pointer {
vp.motion_absolute(timestamp(), x, y, self.state.screen_w, self.state.screen_h);
vp.frame();
}
self.state.conn.flush().context("flush after move_mouse")?;
Ok(())
}
/// Tear down the overlay, move to `(x, y)`, and left-click once.
pub fn click(&mut self, x: u32, y: u32) -> Result<()> {
self.click_at(x, y, BTN_LEFT, 1)
}
/// Tear down the overlay, move to `(x, y)`, and left-click twice.
pub fn double_click(&mut self, x: u32, y: u32) -> Result<()> {
self.click_at(x, y, BTN_LEFT, 2)
}
/// Tear down the overlay, move to `(x, y)`, and left-click three times.
pub fn triple_click(&mut self, x: u32, y: u32) -> Result<()> {
self.click_at(x, y, BTN_LEFT, 3)
}
/// Tear down the overlay, move to `(x, y)`, and right-click once.
pub fn right_click(&mut self, x: u32, y: u32) -> Result<()> {
self.click_at(x, y, BTN_RIGHT, 1)
}
/// Tear down the overlay, then perform a click-drag from `(x1, y1)` to
/// `(x2, y2)`.
///
/// Simulates a realistic drag by sending intermediate motion events
/// over time so that compositors and applications recognize the gesture.
pub fn drag_select(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) -> Result<()> {
use std::thread::sleep;
use std::time::Duration;
self.teardown_surface()?;
let (sw, sh) = (self.state.screen_w, self.state.screen_h);
// 1. Move to the drag origin and let focus settle.
self.send_motion(x1, y1, sw, sh)?;
self.roundtrip("motion to drag start")?;
sleep(Duration::from_millis(DRAG_SETTLE_MS));
// 2. Press and hold — give the app time to register the press.
self.send_button(BTN_LEFT, ButtonState::Pressed)?;
self.roundtrip("press at drag start")?;
sleep(Duration::from_millis(DRAG_PRESS_SETTLE_MS));
// 3. Animate the pointer from start to end so apps see continuous
// motion and cross their drag threshold.
let step_delay = Duration::from_millis(DRAG_STEP_DELAY_MS);
for i in 1..=DRAG_INTERP_STEPS {
let t = i as f64 / DRAG_INTERP_STEPS as f64;
let x = x1 as f64 + (x2 as f64 - x1 as f64) * t;
let y = y1 as f64 + (y2 as f64 - y1 as f64) * t;
let cx = (x.clamp(0.0, sw.saturating_sub(1) as f64)) as u32;
let cy = (y.clamp(0.0, sh.saturating_sub(1) as f64)) as u32;
self.send_motion(cx, cy, sw, sh)?;
self.state
.conn
.flush()
.context("flush during drag motion")?;
sleep(step_delay);
}
self.roundtrip("motion to drag end")?;
sleep(Duration::from_millis(DRAG_RELEASE_SETTLE_MS));
// 4. Release the button at the destination.
self.send_button(BTN_LEFT, ButtonState::Released)?;
self.roundtrip("release at drag end")
}
// -- Scroll -------------------------------------------------------------
pub fn scroll_up(&mut self) -> Result<()> {
self.scroll(Axis::VerticalScroll, -15.0, -1)
}
pub fn scroll_down(&mut self) -> Result<()> {
self.scroll(Axis::VerticalScroll, 15.0, 1)
}
pub fn scroll_left(&mut self) -> Result<()> {
self.scroll(Axis::HorizontalScroll, -15.0, -1)
}
pub fn scroll_right(&mut self) -> Result<()> {
self.scroll(Axis::HorizontalScroll, 15.0, 1)
}
// -- Lifecycle ----------------------------------------------------------
/// Block until the key that triggered the current action is released.
/// This prevents the key-up event from leaking to the underlying window
/// after the overlay is torn down.
pub fn wait_for_key_release(&mut self) -> Result<()> {
self.state.awaiting_key_release = true;
let mut dispatches = 0u32;
while self.state.awaiting_key_release {
self.event_queue
.blocking_dispatch(&mut self.state)
.context("waiting for key release")?;
dispatches += 1;
if dispatches >= KEY_RELEASE_MAX_DISPATCHES {
self.state.awaiting_key_release = false;
break;
}
}
Ok(())
}
/// Destroy the virtual pointer and tear down the overlay surface.
///
/// Does NOT send safety button releases — the compositor releases all
/// buttons when the virtual pointer object is destroyed. Sending
/// redundant releases here caused spurious extra clicks (e.g. double-click
/// turning into triple-click).
pub fn exit(&mut self) -> Result<()> {
if let Some(vp) = self.state.virtual_pointer.take() {
vp.destroy();
}
self.state.conn.flush().context("flush vp destroy")?;
self.teardown_surface()
}
/// Re-create the overlay after it was torn down (e.g. for scroll).
pub fn reopen(&mut self) -> Result<()> {
self.state.configured = false;
self.state.init_layer_surface(&self.queue_handle)?;
self.roundtrip("reopen")?;
while !self.state.configured {
self.event_queue
.blocking_dispatch(&mut self.state)
.context("waiting for configure after reopen")?;
}
Ok(())
}
// -- Private helpers ----------------------------------------------------
/// Shared implementation for click, double_click, and right_click.
///
/// Tears down the overlay so the compositor updates focus, moves the
/// virtual pointer to `(x, y)`, then sends `count` press/release pairs
/// on `button`.
fn click_at(&mut self, x: u32, y: u32, button: u32, count: u32) -> Result<()> {
self.teardown_surface()?;
self.send_motion(x, y, self.state.screen_w, self.state.screen_h)?;
self.roundtrip("motion before click")?;
if let Some(vp) = &self.state.virtual_pointer {
for _ in 0..count {
let ts = timestamp();
vp.button(ts, button, ButtonState::Pressed);
vp.frame();
vp.button(ts, button, ButtonState::Released);
vp.frame();
}
}
self.roundtrip("click")
}
fn scroll(&mut self, axis: Axis, value: f64, discrete: i32) -> Result<()> {
self.teardown_surface()?;
if let Some(vp) = &self.state.virtual_pointer {
// axis_source tells the compositor this is a wheel event, not a
// touchpad gesture. axis_discrete provides the notch count that
// many compositors require alongside the continuous value.
vp.axis_source(AxisSource::Wheel);
vp.axis_discrete(timestamp(), axis, value, discrete);
vp.frame();
}
self.state.conn.flush().context("flush after scroll")?;
self.reopen()
}
fn teardown_surface(&mut self) -> Result<()> {
if let Some(layer_surface) = self.state.layer_surface.take() {
layer_surface.destroy();
}
if let Some(surface) = self.state.surface.take() {
surface.destroy();
}
self.roundtrip("surface teardown")
}
/// Convenience wrapper around `event_queue.roundtrip` with context.
fn roundtrip(&mut self, context: &str) -> Result<()> {
self.event_queue
.roundtrip(&mut self.state)
.with_context(|| format!("roundtrip ({context})"))?;
Ok(())
}
fn send_motion(&self, x: u32, y: u32, sw: u32, sh: u32) -> Result<()> {
if let Some(vp) = &self.state.virtual_pointer {
vp.motion_absolute(timestamp(), x, y, sw, sh);
vp.frame();
}
Ok(())
}
fn send_button(&self, button: u32, button_state: ButtonState) -> Result<()> {
if let Some(vp) = &self.state.virtual_pointer {
vp.button(timestamp(), button, button_state);
vp.frame();
}
Ok(())
}
}
impl Drop for WaylandBackend {
fn drop(&mut self) {
let _ = self.exit();
}
}
// ===========================================================================
// Internal Wayland dispatch state.
// ===========================================================================
struct WaylandState {
conn: Connection,
// Globals bound during registry enumeration.
compositor: Option<wl_compositor::WlCompositor>,
shm: Option<wl_shm::WlShm>,
seat: Option<wl_seat::WlSeat>,
layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
vp_manager: Option<zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1>,
// Per-session objects.
surface: Option<wl_surface::WlSurface>,
layer_surface: Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,
virtual_pointer: Option<zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1>,
// Screen geometry reported by the layer-surface configure.
screen_w: u32,
screen_h: u32,
configured: bool,
// Keyboard state.
pending_keys: VecDeque<KeyEvent>,
shift_held: bool,
ctrl_held: bool,
alt_held: bool,
awaiting_key_release: bool,
}
impl WaylandState {
/// Create and configure a full-screen layer-shell overlay surface.
fn init_layer_surface(&mut self, qh: &QueueHandle<Self>) -> Result<()> {
let compositor = self
.compositor
.as_ref()
.context("wl_compositor not available")?;
let layer_shell = self
.layer_shell
.as_ref()
.context("zwlr_layer_shell_v1 not available")?;
let surface = compositor.create_surface(qh, ());
let layer_surface = layer_shell.get_layer_surface(
&surface,
None,
zwlr_layer_shell_v1::Layer::Overlay,
"mousefree".to_string(),
qh,
(),
);
layer_surface.set_size(0, 0);
layer_surface.set_anchor(
zwlr_layer_surface_v1::Anchor::Top
| zwlr_layer_surface_v1::Anchor::Bottom
| zwlr_layer_surface_v1::Anchor::Left
| zwlr_layer_surface_v1::Anchor::Right,
);
layer_surface.set_exclusive_zone(-1);
layer_surface
.set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive);
surface.commit();
self.surface = Some(surface);
self.layer_surface = Some(layer_surface);
Ok(())
}
}
// ===========================================================================
// Wayland dispatch implementations.
// ===========================================================================
impl Dispatch<wl_registry::WlRegistry, ()> for WaylandState {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
let wl_registry::Event::Global {
name,
interface,
version,
} = event
else {
return;
};
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version.min(4), qh, ()));
}
"wl_shm" => {
state.shm = Some(registry.bind(name, 1, qh, ()));
}
"wl_seat" => {
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
}
"zwlr_layer_shell_v1" => {
state.layer_shell = Some(registry.bind(name, version.min(4), qh, ()));
}
"zwlr_virtual_pointer_manager_v1" => {
state.vp_manager = Some(registry.bind(name, version.min(2), qh, ()));
}
_ => {}
}
}
}
impl Dispatch<wl_seat::WlSeat, ()> for WaylandState {
fn event(
_state: &mut Self,
seat: &wl_seat::WlSeat,
event: wl_seat::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(caps),
} = event
{
if caps.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qh, ());
}
}
}
}
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandState {
fn event(
state: &mut Self,
_: &wl_keyboard::WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
wl_keyboard::Event::Modifiers { mods_depressed, .. } => {
state.shift_held = (mods_depressed & MOD_SHIFT) != 0;
state.ctrl_held = (mods_depressed & MOD_CTRL) != 0;
state.alt_held = (mods_depressed & MOD_ALT) != 0;
}
wl_keyboard::Event::Key {
key,
state: WEnum::Value(wl_keyboard::KeyState::Pressed),
..
} => {
// Ignore bare modifier press events; we track modifiers via
// the Modifiers event instead.
if matches!(
key,
KEY_LSHIFT | KEY_RSHIFT | KEY_LCTRL | KEY_RCTRL | KEY_LALT | KEY_RALT
) {
return;
}
// Ctrl-[ acts as Escape (evdev keycode 26 = '[').
if key == 26 && state.ctrl_held {
state.pending_keys.push_back(KeyEvent::Close);
return;
}
if let Some(physical) = keycode_to_key(key, state.shift_held) {
// Shift+Enter → triple-click.
if physical == PhysicalKey::Enter && state.shift_held {
state.pending_keys.push_back(KeyEvent::TripleClick);
return;
}
// Modifier+letter emits CtrlChar/AltChar so the main loop
// can distinguish nudge tiers.
if let PhysicalKey::Char(c) = physical {
if state.ctrl_held {
state.pending_keys.push_back(KeyEvent::CtrlChar(c));
return;
}
if state.alt_held {
state.pending_keys.push_back(KeyEvent::AltChar(c));
return;
}
}
if let Some(ev) = physical_to_event(physical) {
state.pending_keys.push_back(ev);
}
}
}
wl_keyboard::Event::Key {
state: WEnum::Value(wl_keyboard::KeyState::Released),
..
} => {
state.awaiting_key_release = false;
}
_ => {}
}
}
}
impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for WaylandState {
fn event(
state: &mut Self,
layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
event: zwlr_layer_surface_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zwlr_layer_surface_v1::Event::Configure {
serial,
width,
height,
} => {
layer_surface.ack_configure(serial);
const MAX_SCREEN_DIM: u32 = 16384;
if width > 0 && height > 0 && width <= MAX_SCREEN_DIM && height <= MAX_SCREEN_DIM {
state.screen_w = width;
state.screen_h = height;
state.configured = true;
}
}
zwlr_layer_surface_v1::Event::Closed => {
state.pending_keys.push_back(KeyEvent::Close);
}
_ => {}
}
}
}
delegate_noop!(WaylandState: ignore wl_compositor::WlCompositor);
delegate_noop!(WaylandState: ignore wl_region::WlRegion);
delegate_noop!(WaylandState: ignore wl_surface::WlSurface);
delegate_noop!(WaylandState: ignore wl_shm::WlShm);
delegate_noop!(WaylandState: ignore wl_shm_pool::WlShmPool);
delegate_noop!(WaylandState: ignore wl_buffer::WlBuffer);
delegate_noop!(WaylandState: ignore wl_pointer::WlPointer);
delegate_noop!(WaylandState: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);
delegate_noop!(WaylandState: ignore zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1);
delegate_noop!(WaylandState: ignore zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1);
// ===========================================================================
// Helpers.
// ===========================================================================
/// Returns a u32 millisecond timestamp for Wayland pointer events.
///
/// Wayland timestamps are u32 milliseconds from an arbitrary epoch.
/// Wrapping every ~49 days is expected and harmless per the protocol.
fn timestamp() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u32
}
/// Translates a Linux evdev key code into a [`PhysicalKey`].
///
/// **Note:** This table assumes a US QWERTY layout. Other layouts will
/// produce incorrect characters. A proper fix requires xkb integration.
fn keycode_to_key(kc: u32, shift_held: bool) -> Option<PhysicalKey> {
// Non-character keys — independent of shift state.
match kc {
1 => return Some(PhysicalKey::Escape),
14 => return Some(PhysicalKey::Backspace),
28 => return Some(PhysicalKey::Enter),
57 => return Some(PhysicalKey::Space),
104 => return Some(PhysicalKey::Up),
105 => return Some(PhysicalKey::Left),
106 => return Some(PhysicalKey::Right),
109 => return Some(PhysicalKey::Down),
_ => {}
}
// Character keys — shift selects the alternate mapping.
let ch = if shift_held {
match kc {
2 => '!',
3 => '@',
4 => '#',
5 => '$',
6 => '%',
7 => '^',
8 => '&',
9 => '*',
10 => '(',
11 => ')',
12 => '_',
13 => '+',
26 => '{',
27 => '}',
43 => '|',
39 => ':',
40 => '"',
41 => '~',
51 => '<',
52 => '>',
53 => '?',
16 => 'Q',
17 => 'W',
18 => 'E',
19 => 'R',
20 => 'T',
21 => 'Y',
22 => 'U',
23 => 'I',
24 => 'O',
25 => 'P',
30 => 'A',
31 => 'S',
32 => 'D',
33 => 'F',
34 => 'G',
35 => 'H',
36 => 'J',
37 => 'K',
38 => 'L',
44 => 'Z',
45 => 'X',
46 => 'C',
47 => 'V',
48 => 'B',
49 => 'N',
50 => 'M',
_ => return None,
}
} else {
match kc {
2 => '1',
3 => '2',
4 => '3',
5 => '4',
6 => '5',
7 => '6',
8 => '7',
9 => '8',
10 => '9',
11 => '0',
12 => '-',
13 => '=',
26 => '[',
27 => ']',
43 => '\\',
39 => ';',
40 => '\'',
41 => '`',
51 => ',',
52 => '.',
53 => '/',
16 => 'q',
17 => 'w',
18 => 'e',
19 => 'r',
20 => 't',
21 => 'y',
22 => 'u',
23 => 'i',
24 => 'o',
25 => 'p',
30 => 'a',
31 => 's',
32 => 'd',
33 => 'f',
34 => 'g',
35 => 'h',
36 => 'j',
37 => 'k',
38 => 'l',
44 => 'z',
45 => 'x',
46 => 'c',
47 => 'v',
48 => 'b',
49 => 'n',
50 => 'm',
_ => return None,
}
};
Some(PhysicalKey::Char(ch))
}
// ===========================================================================
// Tests.
// ===========================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keycode_letters_unshifted() {
assert_eq!(keycode_to_key(16, false), Some(PhysicalKey::Char('q')));
assert_eq!(keycode_to_key(30, false), Some(PhysicalKey::Char('a')));
assert_eq!(keycode_to_key(44, false), Some(PhysicalKey::Char('z')));
assert_eq!(keycode_to_key(50, false), Some(PhysicalKey::Char('m')));
}
#[test]
fn keycode_letters_shifted() {
assert_eq!(keycode_to_key(16, true), Some(PhysicalKey::Char('Q')));
assert_eq!(keycode_to_key(30, true), Some(PhysicalKey::Char('A')));
assert_eq!(keycode_to_key(50, true), Some(PhysicalKey::Char('M')));
}
#[test]
fn keycode_digits() {
assert_eq!(keycode_to_key(2, false), Some(PhysicalKey::Char('1')));
assert_eq!(keycode_to_key(11, false), Some(PhysicalKey::Char('0')));
assert_eq!(keycode_to_key(2, true), Some(PhysicalKey::Char('!')));
assert_eq!(keycode_to_key(11, true), Some(PhysicalKey::Char(')')));
}
#[test]
fn keycode_special_keys() {
assert_eq!(keycode_to_key(1, false), Some(PhysicalKey::Escape));
assert_eq!(keycode_to_key(14, false), Some(PhysicalKey::Backspace));
assert_eq!(keycode_to_key(28, false), Some(PhysicalKey::Enter));
assert_eq!(keycode_to_key(57, false), Some(PhysicalKey::Space));
// Delete (111) is intentionally unmapped — returns None so the key is
// consumed by the overlay without triggering any action.
assert_eq!(keycode_to_key(111, false), None);
}
#[test]
fn keycode_special_keys_ignore_shift() {
assert_eq!(keycode_to_key(1, true), Some(PhysicalKey::Escape));
assert_eq!(keycode_to_key(57, true), Some(PhysicalKey::Space));
}
#[test]
fn keycode_unknown_returns_none() {
assert_eq!(keycode_to_key(999, false), None);
assert_eq!(keycode_to_key(999, true), None);
}
#[test]
fn keycode_punctuation() {
assert_eq!(keycode_to_key(52, false), Some(PhysicalKey::Char('.')));
assert_eq!(keycode_to_key(53, false), Some(PhysicalKey::Char('/')));
assert_eq!(keycode_to_key(52, true), Some(PhysicalKey::Char('>')));
assert_eq!(keycode_to_key(53, true), Some(PhysicalKey::Char('?')));
}
#[test]
fn physical_to_event_mappings() {
assert!(matches!(
physical_to_event(PhysicalKey::Space),
Some(KeyEvent::Click)
));
assert!(matches!(
physical_to_event(PhysicalKey::Enter),
Some(KeyEvent::DoubleClick)
));
assert!(matches!(
physical_to_event(PhysicalKey::Escape),
Some(KeyEvent::Close)
));
assert!(matches!(
physical_to_event(PhysicalKey::Backspace),
Some(KeyEvent::Undo)
));
assert!(matches!(
physical_to_event(PhysicalKey::Char('.')),
Some(KeyEvent::RightClick)
));
assert!(matches!(
physical_to_event(PhysicalKey::Char('a')),
Some(KeyEvent::Char('a'))
));
// All PhysicalKey variants now map to a KeyEvent (no dead paths).
}
#[test]
fn timestamp_does_not_panic() {
// Mainly confirms unwrap_or_default works even if the clock is weird.
let _ = timestamp();
}
}