use std::{
collections::HashSet,
hash::{Hash, Hasher},
time::Duration,
};
use bevy::{
input::{ButtonState, keyboard::KeyboardInput},
prelude::*,
};
use crossterm::event::KeyModifiers;
use smol_str::SmolStr;
use crate::event::{InputSet, KeyEvent};
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Capability: u8 {
const KEY_RELEASE = 0b0000_0001;
const MODIFIER = 0b0000_0010;
const ALL = Self::KEY_RELEASE.bits() | Self::MODIFIER.bits();
}
}
#[derive(Debug, Default, Resource, Clone, Copy)]
pub enum EmulationPolicy {
#[default]
Automatic,
Manual(Capability),
}
impl EmulationPolicy {
pub fn emulate_capabilities(&self, detected: &Detected) -> Capability {
match self {
EmulationPolicy::Automatic => !detected.0,
EmulationPolicy::Manual(capabilitiy) => *capabilitiy,
}
}
}
#[derive(Debug, Resource, Default, Deref)]
pub struct Detected(pub Capability);
pub struct KeyboardPlugin;
impl Plugin for KeyboardPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<bevy::input::InputPlugin>() {
app.add_plugins(bevy::input::InputPlugin);
}
if !app.is_plugin_added::<bevy::time::TimePlugin>() {
app.add_plugins(bevy::time::TimePlugin);
}
app.init_resource::<ReleaseKey>()
.init_resource::<Detected>()
.init_resource::<EmulationPolicy>()
.init_resource::<Emulate>()
.add_systems(Startup, setup_window)
.add_systems(
PreUpdate,
reset_emulation_check
.run_if(resource_changed::<EmulationPolicy>)
.in_set(InputSet::Pre),
)
.add_systems(
PreUpdate,
(detect_capabilities, check_for_emulation)
.chain()
.run_if(resource_exists::<Emulate>)
.in_set(InputSet::CheckEmulation),
)
.add_systems(
PreUpdate,
(
send_key_events_with_emulation.run_if(resource_exists::<Emulate>),
send_key_events_no_emulation.run_if(not(resource_exists::<Emulate>)),
)
.in_set(InputSet::EmitBevy),
);
}
}
#[derive(Debug, Deref, DerefMut)]
struct Modifiers(KeyModifiers);
impl Default for Modifiers {
fn default() -> Self {
Self(KeyModifiers::empty())
}
}
#[derive(Deref, DerefMut, PartialEq, Eq)]
struct KeyInput(KeyboardInput);
impl Hash for KeyInput {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
self.key_code.hash(state);
self.logical_key.hash(state);
self.state.hash(state);
self.window.hash(state);
}
}
#[derive(Default)]
struct LastPress(HashSet<KeyInput>, HashSet<KeyInput>);
impl LastPress {
fn swap(&mut self) {
std::mem::swap(&mut self.0, &mut self.1);
}
}
#[derive(Resource, Debug)]
pub enum ReleaseKey {
Duration(Duration),
FrameCount(u32),
Immediate,
OnNextKey,
}
impl Default for ReleaseKey {
fn default() -> Self {
ReleaseKey::Duration(Duration::from_secs(1))
}
}
#[derive(Default)]
enum ReleaseKeyState {
#[default]
None,
Count(u32),
Timer(Timer),
}
impl ReleaseKey {
fn tick(&self, state: &mut ReleaseKeyState, delta: Duration) {
use ReleaseKey::*;
match self {
FrameCount(_) | Immediate => {
if let ReleaseKeyState::Count(c) = state {
*c = c.saturating_add(1);
} else {
*state = ReleaseKeyState::Count(0);
}
}
Duration(d) => {
if let ReleaseKeyState::Timer(timer) = state {
timer.tick(delta);
} else {
*state = ReleaseKeyState::Timer(Timer::new(*d, TimerMode::Once));
}
}
_ => (),
}
}
fn finished(&self, state: &ReleaseKeyState) -> bool {
use ReleaseKey::*;
match self {
OnNextKey => false,
FrameCount(target) => {
let ReleaseKeyState::Count(count) = state else {
return false;
};
count >= target
}
Duration(_) => {
let ReleaseKeyState::Timer(timer) = state else {
return false;
};
timer.finished()
}
Immediate => true,
}
}
fn reset(&self, state: &mut ReleaseKeyState) {
use ReleaseKey::*;
match self {
FrameCount(_) | Immediate => *state = ReleaseKeyState::Count(0),
Duration(d) => {
if let ReleaseKeyState::Timer(timer) = state {
timer.reset();
} else {
*state = ReleaseKeyState::Timer(Timer::new(*d, TimerMode::Once));
}
}
_ => (),
}
}
}
#[allow(clippy::too_many_arguments)]
fn detect_capabilities(mut keys: EventReader<KeyEvent>, mut detected: ResMut<Detected>) {
for key_event in keys.read() {
if matches!(key_event.code, crossterm::event::KeyCode::Modifier(_)) {
detected.0 |= Capability::MODIFIER;
}
if matches!(key_event.kind, crossterm::event::KeyEventKind::Release) {
detected.0 |= Capability::KEY_RELEASE;
}
}
}
#[derive(Debug, Resource, Default)]
pub struct Emulate;
fn check_for_emulation(
detected: Res<Detected>,
policy: Res<EmulationPolicy>,
mut commands: Commands,
) {
if policy.emulate_capabilities(&detected).is_empty() {
commands.remove_resource::<Emulate>();
}
}
fn reset_emulation_check(mut commands: Commands) {
commands.insert_resource(Emulate);
}
#[allow(clippy::too_many_arguments)]
fn send_key_events_with_emulation(
mut keys: EventReader<KeyEvent>,
bevy_window: Single<Entity, With<DummyWindow>>,
mut modifiers: Local<Modifiers>,
mut last_pressed: Local<LastPress>,
mut keyboard_input: EventWriter<KeyboardInput>,
release_key: Res<ReleaseKey>,
mut release_key_state: Local<ReleaseKeyState>,
time: Res<Time>,
detected: Res<Detected>,
policy: Res<EmulationPolicy>,
) {
release_key.tick(&mut release_key_state, time.delta());
if keys.is_empty() && !release_key.finished(&release_key_state) {
return;
}
for key_event in keys.read() {
if let Some((bevy_event, mods, repeated)) = key_event_to_bevy(key_event, *bevy_window) {
let emulation = policy.emulate_capabilities(&detected);
if emulation.contains(Capability::MODIFIER) && mods != **modifiers {
let delta = mods.symmetric_difference(**modifiers);
for flag in delta {
let state = if mods.contains(flag) {
ButtonState::Pressed
} else {
ButtonState::Released
};
let modifier_event =
modifier_to_bevy(crossterm_modifier_to_bevy_key(flag), state, *bevy_window);
keyboard_input.write(modifier_event);
}
**modifiers = mods;
}
if repeated {
last_pressed.1.insert(KeyInput(bevy_event.clone()));
}
if emulation.contains(Capability::KEY_RELEASE) {
let wrapped = KeyInput(bevy_event.clone());
if let Some(last_press) = last_pressed.0.take(&wrapped) {
last_pressed.1.insert(last_press);
} else {
last_pressed.1.insert(wrapped);
keyboard_input.write(bevy_event);
}
} else {
keyboard_input.write(bevy_event);
}
}
}
for e in last_pressed.0.drain() {
let reciprocal_event = KeyboardInput {
state: match e.0.state {
ButtonState::Pressed => ButtonState::Released,
ButtonState::Released => ButtonState::Pressed,
},
repeat: e.0.state == ButtonState::Released,
..e.0
};
keyboard_input.write(reciprocal_event);
}
if release_key.finished(&release_key_state)
&& policy
.emulate_capabilities(&detected)
.contains(Capability::MODIFIER)
{
for flag in **modifiers {
let state = ButtonState::Released;
let modifier_event =
modifier_to_bevy(crossterm_modifier_to_bevy_key(flag), state, *bevy_window);
keyboard_input.write(modifier_event);
}
**modifiers = KeyModifiers::empty();
}
last_pressed.swap();
release_key.reset(&mut release_key_state);
}
fn send_key_events_no_emulation(
mut keys: EventReader<KeyEvent>,
bevy_window: Single<Entity, With<DummyWindow>>,
mut keyboard_input: EventWriter<KeyboardInput>,
mut key_repeat_queue: Local<Vec<KeyboardInput>>,
) {
for bevy_event in key_repeat_queue.drain(..) {
keyboard_input.write(bevy_event);
}
for key_event in keys.read() {
if let Some((bevy_event, _modifiers, repeated)) = key_event_to_bevy(key_event, *bevy_window)
{
if repeated {
key_repeat_queue.push(KeyboardInput {
state: ButtonState::Pressed,
..bevy_event.clone()
});
}
keyboard_input.write(bevy_event);
}
}
}
#[derive(Debug, Component)]
struct DummyWindow;
fn setup_window(mut commands: Commands) {
commands.spawn(DummyWindow);
}
fn modifier_to_bevy(
modifier: bevy::input::keyboard::Key,
state: bevy::input::ButtonState,
window: Entity,
) -> bevy::input::keyboard::KeyboardInput {
use bevy::input::keyboard::{Key as k, KeyCode as c};
let key_code = match modifier {
k::Control => c::ControlLeft,
k::Shift => c::ShiftLeft,
k::Alt => c::AltLeft,
k::Hyper => c::Hyper,
k::Meta => c::Meta,
k::Super => c::SuperLeft,
x => panic!("No such modifier {x:?}"),
};
let logical_key = modifier;
bevy::input::keyboard::KeyboardInput {
key_code,
state,
window,
logical_key,
text: None,
repeat: false,
}
}
fn key_event_to_bevy(
key_event: &crossterm::event::KeyEvent,
window: Entity,
) -> Option<(
bevy::input::keyboard::KeyboardInput,
crossterm::event::KeyModifiers,
bool,
)> {
let crossterm::event::KeyEvent {
code,
modifiers,
kind,
state: _state,
} = key_event;
let mut repeated = false;
let state = match kind {
crossterm::event::KeyEventKind::Press => bevy::input::ButtonState::Pressed,
crossterm::event::KeyEventKind::Repeat => {
repeated = true;
bevy::input::ButtonState::Released
}
crossterm::event::KeyEventKind::Release => bevy::input::ButtonState::Released,
};
let key_code = to_bevy_keycode(code);
let logical_key = to_bevy_key(code);
#[warn(clippy::useless_conversion)]
let text = logical_key
.as_ref()
.and_then(logical_key_to_text)
.map(|s| s.into());
key_code
.zip(logical_key)
.map(|((key_code, mods), logical_key)| {
(
bevy::input::keyboard::KeyboardInput {
key_code,
state,
window,
logical_key,
text,
repeat: false,
},
*modifiers | mods,
repeated,
)
})
}
fn to_bevy_keycode(
key_code: &crossterm::event::KeyCode,
) -> Option<(
bevy::input::keyboard::KeyCode,
crossterm::event::KeyModifiers,
)> {
use bevy::input::keyboard::KeyCode as b;
use crossterm::event::{KeyCode as c, KeyModifiers as m};
let mut mods = crossterm::event::KeyModifiers::empty();
match key_code {
c::Backspace => Some(b::Backspace),
c::Enter => Some(b::Enter),
c::Left => Some(b::ArrowLeft),
c::Right => Some(b::ArrowRight),
c::Up => Some(b::ArrowUp),
c::Down => Some(b::ArrowDown),
c::Home => Some(b::Home),
c::End => Some(b::End),
c::PageUp => Some(b::PageUp),
c::PageDown => Some(b::PageDown),
c::Tab => Some(b::Tab),
c::BackTab => {
mods |= m::SHIFT;
Some(b::Tab)
}
c::Delete => Some(b::Delete),
c::Insert => Some(b::Insert),
c::F(f) => match f {
1 => Some(b::F1),
2 => Some(b::F2),
3 => Some(b::F3),
4 => Some(b::F4),
5 => Some(b::F5),
6 => Some(b::F6),
7 => Some(b::F7),
8 => Some(b::F8),
9 => Some(b::F9),
10 => Some(b::F10),
11 => Some(b::F11),
12 => Some(b::F12),
13 => Some(b::F13),
14 => Some(b::F14),
15 => Some(b::F15),
16 => Some(b::F16),
17 => Some(b::F17),
18 => Some(b::F18),
19 => Some(b::F19),
20 => Some(b::F20),
31 => Some(b::F31),
32 => Some(b::F32),
33 => Some(b::F33),
34 => Some(b::F34),
35 => Some(b::F35),
_ => None,
},
c::Char(c) => match c {
'!' => {
mods |= m::SHIFT;
Some(b::Digit1)
}
'@' => {
mods |= m::SHIFT;
Some(b::Digit2)
}
'#' => {
mods |= m::SHIFT;
Some(b::Digit3)
}
'$' => {
mods |= m::SHIFT;
Some(b::Digit4)
}
'%' => {
mods |= m::SHIFT;
Some(b::Digit5)
}
'^' => {
mods |= m::SHIFT;
Some(b::Digit6)
}
'&' => {
mods |= m::SHIFT;
Some(b::Digit7)
}
'*' => {
mods |= m::SHIFT;
Some(b::Digit8)
}
'(' => {
mods |= m::SHIFT;
Some(b::Digit9)
}
')' => {
mods |= m::SHIFT;
Some(b::Digit0)
}
'-' => {
mods |= m::SHIFT;
Some(b::Minus)
}
'[' => Some(b::BracketLeft),
']' => Some(b::BracketRight),
'{' => {
mods |= m::SHIFT;
Some(b::BracketLeft)
}
'}' => {
mods |= m::SHIFT;
Some(b::BracketRight)
}
',' => Some(b::Comma),
'=' => Some(b::Equal),
'<' => {
mods |= m::SHIFT;
Some(b::Comma)
}
'+' => {
mods |= m::SHIFT;
Some(b::Equal)
}
'.' => Some(b::Period),
'>' => {
mods |= m::SHIFT;
Some(b::Period)
}
'\'' => Some(b::Quote),
'"' => {
mods |= m::SHIFT;
Some(b::Quote)
}
';' => Some(b::Semicolon),
':' => {
mods |= m::SHIFT;
Some(b::Semicolon)
}
'/' => Some(b::Slash),
'?' => {
mods |= m::SHIFT;
Some(b::Slash)
}
' ' => Some(b::Space),
'1' => Some(b::Digit1),
'2' => Some(b::Digit2),
'3' => Some(b::Digit3),
'4' => Some(b::Digit4),
'5' => Some(b::Digit5),
'6' => Some(b::Digit6),
'7' => Some(b::Digit7),
'8' => Some(b::Digit8),
'9' => Some(b::Digit9),
'0' => Some(b::Digit0),
'a' => Some(b::KeyA),
'b' => Some(b::KeyB),
'c' => Some(b::KeyC),
'd' => Some(b::KeyD),
'e' => Some(b::KeyE),
'f' => Some(b::KeyF),
'g' => Some(b::KeyG),
'h' => Some(b::KeyH),
'i' => Some(b::KeyI),
'j' => Some(b::KeyJ),
'k' => Some(b::KeyK),
'l' => Some(b::KeyL),
'm' => Some(b::KeyM),
'n' => Some(b::KeyN),
'o' => Some(b::KeyO),
'p' => Some(b::KeyP),
'q' => Some(b::KeyQ),
'r' => Some(b::KeyR),
's' => Some(b::KeyS),
't' => Some(b::KeyT),
'u' => Some(b::KeyU),
'v' => Some(b::KeyV),
'w' => Some(b::KeyW),
'x' => Some(b::KeyX),
'y' => Some(b::KeyY),
'z' => Some(b::KeyZ),
'A' => {
mods |= m::SHIFT;
Some(b::KeyA)
}
'B' => {
mods |= m::SHIFT;
Some(b::KeyB)
}
'C' => {
mods |= m::SHIFT;
Some(b::KeyC)
}
'D' => {
mods |= m::SHIFT;
Some(b::KeyD)
}
'E' => {
mods |= m::SHIFT;
Some(b::KeyE)
}
'F' => {
mods |= m::SHIFT;
Some(b::KeyF)
}
'G' => {
mods |= m::SHIFT;
Some(b::KeyG)
}
'H' => {
mods |= m::SHIFT;
Some(b::KeyH)
}
'I' => {
mods |= m::SHIFT;
Some(b::KeyI)
}
'J' => {
mods |= m::SHIFT;
Some(b::KeyJ)
}
'K' => {
mods |= m::SHIFT;
Some(b::KeyK)
}
'L' => {
mods |= m::SHIFT;
Some(b::KeyL)
}
'M' => {
mods |= m::SHIFT;
Some(b::KeyM)
}
'N' => {
mods |= m::SHIFT;
Some(b::KeyN)
}
'O' => {
mods |= m::SHIFT;
Some(b::KeyO)
}
'P' => {
mods |= m::SHIFT;
Some(b::KeyP)
}
'Q' => {
mods |= m::SHIFT;
Some(b::KeyQ)
}
'R' => {
mods |= m::SHIFT;
Some(b::KeyR)
}
'S' => {
mods |= m::SHIFT;
Some(b::KeyS)
}
'T' => {
mods |= m::SHIFT;
Some(b::KeyT)
}
'U' => {
mods |= m::SHIFT;
Some(b::KeyU)
}
'V' => {
mods |= m::SHIFT;
Some(b::KeyV)
}
'W' => {
mods |= m::SHIFT;
Some(b::KeyW)
}
'X' => {
mods |= m::SHIFT;
Some(b::KeyX)
}
'Y' => {
mods |= m::SHIFT;
Some(b::KeyY)
}
'Z' => {
mods |= m::SHIFT;
Some(b::KeyZ)
}
_ => None,
},
c::Null => None,
c::Esc => Some(b::Escape),
c::CapsLock => Some(b::CapsLock),
c::ScrollLock => Some(b::ScrollLock),
c::NumLock => Some(b::NumLock),
c::PrintScreen => Some(b::PrintScreen),
c::Pause => Some(b::Pause),
c::Menu => Some(b::ContextMenu),
c::KeypadBegin => None,
c::Media(media) => {
use crossterm::event::MediaKeyCode::*;
match media {
Play => Some(b::MediaPlayPause),
Pause => Some(b::Pause),
PlayPause => Some(b::MediaPlayPause),
Reverse => None,
Stop => Some(b::MediaStop),
FastForward => Some(b::MediaTrackNext),
Rewind => Some(b::MediaTrackPrevious),
TrackNext => Some(b::MediaTrackNext),
TrackPrevious => Some(b::MediaTrackPrevious),
Record => None,
LowerVolume => Some(b::AudioVolumeDown),
RaiseVolume => Some(b::AudioVolumeUp),
MuteVolume => Some(b::AudioVolumeMute),
}
}
c::Modifier(modifier) => {
use crossterm::event::ModifierKeyCode::*;
match modifier {
LeftShift => Some(b::ShiftLeft),
LeftControl => Some(b::ControlLeft),
LeftAlt => Some(b::AltLeft),
LeftSuper => Some(b::SuperLeft),
LeftHyper => Some(b::Hyper),
LeftMeta => Some(b::Meta),
RightShift => Some(b::ShiftRight),
RightControl => Some(b::ControlRight),
RightAlt => Some(b::AltRight),
RightSuper => Some(b::SuperRight),
RightHyper => Some(b::Hyper),
RightMeta => Some(b::Meta),
IsoLevel3Shift => None,
IsoLevel5Shift => None,
}
}
}
.map(|key_code| (key_code, mods))
}
fn to_bevy_key(key_code: &crossterm::event::KeyCode) -> Option<bevy::input::keyboard::Key> {
use bevy::input::keyboard::Key as b;
use crossterm::event::KeyCode as c;
match key_code {
c::Backspace => Some(b::Backspace),
c::Enter => Some(b::Enter),
c::Left => Some(b::ArrowLeft),
c::Right => Some(b::ArrowRight),
c::Up => Some(b::ArrowUp),
c::Down => Some(b::ArrowDown),
c::Home => Some(b::Home),
c::End => Some(b::End),
c::PageUp => Some(b::PageUp),
c::PageDown => Some(b::PageDown),
c::Tab => Some(b::Tab),
c::BackTab => {
Some(b::Tab)
}
c::Delete => Some(b::Delete),
c::Insert => Some(b::Insert),
c::F(f) => match f {
1 => Some(b::F1),
2 => Some(b::F2),
3 => Some(b::F3),
4 => Some(b::F4),
5 => Some(b::F5),
6 => Some(b::F6),
7 => Some(b::F7),
8 => Some(b::F8),
9 => Some(b::F9),
10 => Some(b::F10),
11 => Some(b::F11),
12 => Some(b::F12),
13 => Some(b::F13),
14 => Some(b::F14),
15 => Some(b::F15),
16 => Some(b::F16),
17 => Some(b::F17),
18 => Some(b::F18),
19 => Some(b::F19),
20 => Some(b::F20),
31 => Some(b::F31),
32 => Some(b::F32),
33 => Some(b::F33),
34 => Some(b::F34),
35 => Some(b::F35),
_ => None,
},
c::Char(c) => Some({
let mut tmp = [0u8; 4];
let s = c.encode_utf8(&mut tmp);
b::Character(s.into())
}),
c::Null => None,
c::Esc => Some(b::Escape),
c::CapsLock => Some(b::CapsLock),
c::ScrollLock => Some(b::ScrollLock),
c::NumLock => Some(b::NumLock),
c::PrintScreen => Some(b::PrintScreen),
c::Pause => Some(b::Pause),
c::Menu => Some(b::ContextMenu),
c::KeypadBegin => None,
c::Media(media) => {
use crossterm::event::MediaKeyCode::*;
match media {
Play => Some(b::MediaPlay),
Pause => Some(b::Pause),
PlayPause => Some(b::MediaPlayPause),
Reverse => None,
Stop => Some(b::MediaStop),
FastForward => Some(b::MediaFastForward),
Rewind => Some(b::MediaRewind),
TrackNext => Some(b::MediaTrackNext),
TrackPrevious => Some(b::MediaTrackPrevious),
Record => Some(b::MediaRecord),
LowerVolume => Some(b::AudioVolumeDown),
RaiseVolume => Some(b::AudioVolumeUp),
MuteVolume => Some(b::AudioVolumeMute),
}
}
c::Modifier(modifier) => {
use crossterm::event::ModifierKeyCode::*;
match modifier {
LeftShift => Some(b::Shift),
LeftControl => Some(b::Control),
LeftAlt => Some(b::Alt),
LeftSuper => Some(b::Super),
LeftHyper => Some(b::Hyper),
LeftMeta => Some(b::Meta),
RightShift => Some(b::Shift),
RightControl => Some(b::Control),
RightAlt => Some(b::Alt),
RightSuper => Some(b::Super),
RightHyper => Some(b::Hyper),
RightMeta => Some(b::Meta),
IsoLevel3Shift => Some(b::AltGraph),
IsoLevel5Shift => None,
}
}
}
}
fn crossterm_modifier_to_bevy_key(
modifier: crossterm::event::KeyModifiers,
) -> bevy::input::keyboard::Key {
let mut i = modifier.into_iter();
let modifier = i.next().expect("mod");
use bevy::input::keyboard::Key as k;
use crossterm::event::KeyModifiers as c;
let result = match modifier {
c::SHIFT => k::Shift,
c::CONTROL => k::Control,
c::ALT => k::Alt,
c::SUPER => k::Super,
c::HYPER => k::Hyper,
c::META => k::Meta,
x => panic!("Given a modifier of {x:?}"),
};
assert!(i.next().is_none());
result
}
fn logical_key_to_text(logical_key: &bevy::input::keyboard::Key) -> Option<SmolStr> {
if let bevy::input::keyboard::Key::Character(character) = logical_key {
Some(SmolStr::new(character.clone()))
} else {
None
}
}