use iced::keyboard::key::{Code, Named, NativeCode, Physical};
use iced::keyboard::{Key, Location, Modifiers};
use keyboard_types::{KeyState, KeyboardEvent};
pub(crate) fn to_iced_event(kb: &KeyboardEvent) -> iced::keyboard::Event {
let modifiers = convert_modifiers(kb.modifiers);
let key = convert_key(&kb.key);
let physical_key = convert_code(kb.code);
let location = convert_location(kb.location);
match kb.state {
KeyState::Down => iced::keyboard::Event::KeyPressed {
modified_key: key.clone(),
key,
physical_key,
location,
text: match &kb.key {
keyboard_types::Key::Character(s)
if !modifiers.contains(Modifiers::CTRL)
&& !modifiers.contains(Modifiers::LOGO) =>
{
Some(s.as_str().into())
}
_ => None,
},
modifiers,
repeat: kb.repeat,
},
KeyState::Up => iced::keyboard::Event::KeyReleased {
modified_key: key.clone(),
key,
physical_key,
location,
modifiers,
},
}
}
fn convert_modifiers(m: keyboard_types::Modifiers) -> Modifiers {
let mut out = Modifiers::empty();
if m.contains(keyboard_types::Modifiers::SHIFT) {
out |= Modifiers::SHIFT;
}
if m.contains(keyboard_types::Modifiers::CONTROL) {
out |= Modifiers::CTRL;
}
if m.contains(keyboard_types::Modifiers::ALT) {
out |= Modifiers::ALT;
}
if m.contains(keyboard_types::Modifiers::META) {
out |= Modifiers::LOGO;
}
out
}
fn convert_location(loc: keyboard_types::Location) -> Location {
match loc {
keyboard_types::Location::Standard => Location::Standard,
keyboard_types::Location::Left => Location::Left,
keyboard_types::Location::Right => Location::Right,
keyboard_types::Location::Numpad => Location::Numpad,
}
}
macro_rules! map_code {
($code:expr; $($v:ident),+ $(,)?) => {
match $code {
$( keyboard_types::Code::$v => Physical::Code(Code::$v), )+
_ => Physical::Unidentified(NativeCode::Unidentified),
}
};
}
fn convert_code(code: keyboard_types::Code) -> Physical {
map_code!(code;
Abort, Again, AltLeft, AltRight, ArrowDown, ArrowLeft, ArrowRight, ArrowUp,
AudioVolumeDown, AudioVolumeMute, AudioVolumeUp, Backquote, Backslash, Backspace,
BracketLeft, BracketRight, BrowserBack, BrowserFavorites, BrowserForward,
BrowserHome, BrowserRefresh, BrowserSearch, BrowserStop, CapsLock, Comma,
ContextMenu, ControlLeft, ControlRight, Convert, Copy, Cut, Delete, Digit0, Digit1,
Digit2, Digit3, Digit4, Digit5, Digit6, Digit7, Digit8, Digit9, Eject, End, Enter,
Equal, Escape, F1, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F2, F20, F21,
F22, F23, F24, F25, F26, F27, F28, F29, F3, F30, F31, F32, F33, F34, F35, F4, F5,
F6, F7, F8, F9, Find, Fn, FnLock, Help, Hiragana, Home, Hyper, Insert,
IntlBackslash, IntlRo, IntlYen, KanaMode, Katakana, KeyA, KeyB, KeyC, KeyD, KeyE,
KeyF, KeyG, KeyH, KeyI, KeyJ, KeyK, KeyL, KeyM, KeyN, KeyO, KeyP, KeyQ, KeyR, KeyS,
KeyT, KeyU, KeyV, KeyW, KeyX, KeyY, KeyZ, Lang1, Lang2, Lang3, Lang4, Lang5,
LaunchApp1, LaunchApp2, LaunchMail, MediaPlayPause, MediaSelect, MediaStop,
MediaTrackNext, MediaTrackPrevious, Minus, NonConvert, NumLock, Numpad0, Numpad1,
Numpad2, Numpad3, Numpad4, Numpad5, Numpad6, Numpad7, Numpad8, Numpad9, NumpadAdd,
NumpadBackspace, NumpadClear, NumpadClearEntry, NumpadComma, NumpadDecimal,
NumpadDivide, NumpadEnter, NumpadEqual, NumpadHash, NumpadMemoryAdd,
NumpadMemoryClear, NumpadMemoryRecall, NumpadMemoryStore, NumpadMemorySubtract,
NumpadMultiply, NumpadParenLeft, NumpadParenRight, NumpadStar, NumpadSubtract,
Open, PageDown, PageUp, Paste, Pause, Period, Power, PrintScreen, Props, Quote,
Resume, ScrollLock, Select, Semicolon, ShiftLeft, ShiftRight, Slash, Sleep, Space,
Suspend, Tab, Turbo, Undo, WakeUp,
)
}
macro_rules! map_named {
($key:expr; $($v:ident),+ $(,)?) => {
match $key {
keyboard_types::Key::Character(s) => Key::Character(s.as_str().into()),
$( keyboard_types::Key::$v => Key::Named(Named::$v), )+
_ => Key::Unidentified,
}
};
}
fn convert_key(key: &keyboard_types::Key) -> Key {
map_named!(key;
Accept, Again, AllCandidates, Alphanumeric, Alt, AltGraph, AppSwitch, ArrowDown,
ArrowLeft, ArrowRight, ArrowUp, Attn, AudioBalanceLeft, AudioBalanceRight,
AudioBassBoostDown, AudioBassBoostToggle, AudioBassBoostUp, AudioFaderFront,
AudioFaderRear, AudioSurroundModeNext, AudioTrebleDown, AudioTrebleUp,
AudioVolumeDown, AudioVolumeMute, AudioVolumeUp, AVRInput, AVRPower, Backspace,
BrightnessDown, BrightnessUp, BrowserBack, BrowserFavorites, BrowserForward,
BrowserHome, BrowserRefresh, BrowserSearch, BrowserStop, Call, Camera, CameraFocus,
Cancel, CapsLock, ChannelDown, ChannelUp, Clear, Close, ClosedCaptionToggle,
CodeInput, ColorF0Red, ColorF1Green, ColorF2Yellow, ColorF3Blue, ColorF4Grey,
ColorF5Brown, Compose, ContextMenu, Control, Convert, Copy, CrSel, Cut, Delete,
Dimmer, DisplaySwap, DVR, Eisu, Eject, End, EndCall, Enter, EraseEof, Escape,
Execute, Exit, ExSel, F1, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F2,
F20, F21, F22, F23, F24, F25, F26, F27, F28, F29, F3, F30, F31, F32, F33, F34, F35,
F4, F5, F6, F7, F8, F9, FavoriteClear0, FavoriteClear1, FavoriteClear2,
FavoriteClear3, FavoriteRecall0, FavoriteRecall1, FavoriteRecall2, FavoriteRecall3,
FavoriteStore0, FavoriteStore1, FavoriteStore2, FavoriteStore3, FinalMode, Find,
Fn, FnLock, GoBack, GoHome, GroupFirst, GroupLast, GroupNext, GroupPrevious, Guide,
GuideNextDay, GuidePreviousDay, HangulMode, HanjaMode, Hankaku, HeadsetHook, Help,
Hibernate, Hiragana, HiraganaKatakana, Home, Hyper, Info, Insert, InstantReplay,
JunjaMode, KanaMode, KanjiMode, Katakana, Key11, Key12, LastNumberRedial,
LaunchApplication1, LaunchApplication2, LaunchCalendar, LaunchContacts, LaunchMail,
LaunchMediaPlayer, LaunchMusicPlayer, LaunchPhone, LaunchScreenSaver,
LaunchSpreadsheet, LaunchWebBrowser, LaunchWebCam, LaunchWordProcessor, Link,
ListProgram, LiveContent, Lock, LogOff, MailForward, MailReply, MailSend,
MannerMode, MediaApps, MediaAudioTrack, MediaClose, MediaFastForward, MediaLast,
MediaPause, MediaPlay, MediaPlayPause, MediaRecord, MediaRewind, MediaSkipBackward,
MediaSkipForward, MediaStepBackward, MediaStepForward, MediaStop, MediaTopMenu,
MediaTrackNext, MediaTrackPrevious, Meta, MicrophoneToggle, MicrophoneVolumeDown,
MicrophoneVolumeMute, MicrophoneVolumeUp, ModeChange, NavigateIn, NavigateNext,
NavigateOut, NavigatePrevious, New, NextCandidate, NextFavoriteChannel,
NextUserProfile, NonConvert, Notification, NumLock, OnDemand, Open, PageDown,
PageUp, Pairing, Paste, Pause, PinPDown, PinPMove, PinPToggle, PinPUp, Play,
PlaySpeedDown, PlaySpeedReset, PlaySpeedUp, Power, PowerOff, PreviousCandidate,
Print, PrintScreen, Process, Props, RandomToggle, RcLowBattery, RecordSpeedNext,
Redo, RfBypass, Romaji, Save, ScanChannelsToggle, ScreenModeNext, ScrollLock,
Select, Settings, Shift, SingleCandidate, Soft1, Soft2, Soft3, Soft4,
SpeechCorrectionList, SpeechInputToggle, SpellCheck, SplitScreenToggle, Standby,
STBInput, STBPower, Subtitle, Super, Symbol, SymbolLock, Tab, Teletext, TV,
TV3DMode, TVAntennaCable, TVAudioDescription, TVAudioDescriptionMixDown,
TVAudioDescriptionMixUp, TVContentsMenu, TVDataService, TVInput, TVInputComponent1,
TVInputComponent2, TVInputComposite1, TVInputComposite2, TVInputHDMI1,
TVInputHDMI2, TVInputHDMI3, TVInputHDMI4, TVInputVGA1, TVMediaContext, TVNetwork,
TVNumberEntry, TVPower, TVRadioService, TVSatellite, TVSatelliteBS, TVSatelliteCS,
TVSatelliteToggle, TVTerrestrialAnalog, TVTerrestrialDigital, TVTimer, Undo,
VideoModeNext, VoiceDial, WakeUp, Wink, Zenkaku, ZenkakuHankaku, ZoomIn, ZoomOut,
ZoomToggle,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(state: KeyState, key: keyboard_types::Key, code: keyboard_types::Code) -> KeyboardEvent {
KeyboardEvent {
state,
key,
code,
location: keyboard_types::Location::Standard,
modifiers: keyboard_types::Modifiers::empty(),
repeat: false,
is_composing: false,
}
}
#[test]
fn character_down_carries_text_and_physical_code() {
let e = ev(
KeyState::Down,
keyboard_types::Key::Character("a".into()),
keyboard_types::Code::KeyA,
);
let iced::keyboard::Event::KeyPressed {
key,
physical_key,
text,
..
} = to_iced_event(&e)
else {
panic!("expected KeyPressed");
};
assert_eq!(key, Key::Character("a".into()));
assert_eq!(physical_key, Physical::Code(Code::KeyA));
assert_eq!(text.as_deref(), Some("a"));
}
#[test]
fn named_key_maps_by_name() {
let e = ev(
KeyState::Down,
keyboard_types::Key::Enter,
keyboard_types::Code::Enter,
);
let iced::keyboard::Event::KeyPressed { key, .. } = to_iced_event(&e) else {
panic!("expected KeyPressed");
};
assert_eq!(key, Key::Named(Named::Enter));
}
#[test]
fn ctrl_suppresses_text_but_keeps_modifier() {
let mut e = ev(
KeyState::Down,
keyboard_types::Key::Character("s".into()),
keyboard_types::Code::KeyS,
);
e.modifiers = keyboard_types::Modifiers::CONTROL;
let iced::keyboard::Event::KeyPressed {
text, modifiers, ..
} = to_iced_event(&e)
else {
panic!("expected KeyPressed");
};
assert_eq!(text, None);
assert!(modifiers.contains(Modifiers::CTRL));
}
#[test]
fn key_up_is_a_release() {
let e = ev(
KeyState::Up,
keyboard_types::Key::Character("a".into()),
keyboard_types::Code::KeyA,
);
assert!(matches!(
to_iced_event(&e),
iced::keyboard::Event::KeyReleased { .. }
));
}
#[test]
fn subscription_pump_delivers_keyboard_events() {
use iced_runtime::futures::{Runtime, subscription};
use std::time::{Duration, Instant};
let executor = iced::futures::executor::ThreadPool::builder()
.pool_size(1)
.create()
.expect("executor");
let (tx, mut rx) = iced::futures::channel::mpsc::unbounded::<u32>();
let mut runtime = Runtime::new(executor, tx);
let sub = iced::keyboard::listen().map(|_event| 7u32);
runtime.track(subscription::into_recipes(sub));
let key = to_iced_event(&ev(
KeyState::Down,
keyboard_types::Key::Character("a".into()),
keyboard_types::Code::KeyA,
));
runtime.broadcast(subscription::Event::Interaction {
window: iced::window::Id::unique(),
event: iced::Event::Keyboard(key),
status: iced::event::Status::Ignored,
});
let deadline = Instant::now() + Duration::from_secs(2);
let got = loop {
if let Ok(m) = rx.try_recv() {
break Some(m);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(5));
};
assert_eq!(got, Some(7));
}
}