use bevy::input::ButtonState;
use bevy::input::keyboard::{Key, KeyCode, KeyboardInput};
use bevy::prelude::*;
use serde::Serialize;
use ts_rs::TS;
use crate::event::ReactEvents;
use crate::react_event;
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct KeyboardEventData {
key: String,
code: String,
text: Option<String>,
repeat: bool,
ctrl_key: bool,
shift_key: bool,
alt_key: bool,
meta_key: bool,
}
#[react_event(name = "keyDown")]
pub struct KeyDown(pub KeyboardEventData);
#[react_event(name = "keyUp")]
pub struct KeyUp(pub KeyboardEventData);
fn logical_key_string(key: &Key) -> String {
match key {
Key::Character(s) => s.to_string(),
other => format!("{other:?}"),
}
}
pub fn collect_keyboard_events(
mut reader: MessageReader<KeyboardInput>,
keys: Res<ButtonInput<KeyCode>>,
events: ReactEvents,
) {
if reader.is_empty() {
return;
}
let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
let alt = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
let meta = keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]);
for ev in reader.read() {
let data = KeyboardEventData {
key: logical_key_string(&ev.logical_key),
code: format!("{:?}", ev.key_code),
text: ev.text.as_ref().map(|t| t.to_string()),
repeat: ev.repeat,
ctrl_key: ctrl,
shift_key: shift,
alt_key: alt,
meta_key: meta,
};
match ev.state {
ButtonState::Pressed => events.send(&KeyDown(data)),
ButtonState::Released => events.send(&KeyUp(data)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::OutboundResource;
use crate::protocol::Outbound;
use bevy::input::keyboard::{Key, KeyCode, KeyboardInput};
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
fn test_app() -> (App, UnboundedReceiver<Outbound>) {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_message::<KeyboardInput>();
app.init_resource::<ButtonInput<KeyCode>>();
let (tx, rx) = unbounded_channel::<Outbound>();
app.insert_resource(OutboundResource(tx));
app.add_systems(Update, collect_keyboard_events);
(app, rx)
}
fn write_key(app: &mut App, key_code: KeyCode, logical: Key, state: ButtonState) {
app.world_mut().write_message(KeyboardInput {
key_code,
logical_key: logical,
state,
text: match state {
ButtonState::Pressed => Some("a".into()),
ButtonState::Released => None,
},
repeat: false,
window: Entity::PLACEHOLDER,
});
}
#[test]
fn key_press_and_release_produce_named_events() {
let (mut app, mut rx) = test_app();
write_key(
&mut app,
KeyCode::KeyA,
Key::Character("a".into()),
ButtonState::Pressed,
);
app.update();
let ev = rx.try_recv().expect("a keyDown event");
match ev {
Outbound::Event { name, value } => {
assert_eq!(name, "keyDown");
assert_eq!(value["key"], "a");
assert_eq!(value["code"], "KeyA");
assert_eq!(value["text"], "a");
assert_eq!(value["repeat"], false);
assert_eq!(value["ctrlKey"], false);
}
other => panic!("expected Outbound::Event, got {other:?}"),
}
write_key(
&mut app,
KeyCode::KeyA,
Key::Character("a".into()),
ButtonState::Released,
);
app.update();
match rx.try_recv().expect("a keyUp event") {
Outbound::Event { name, .. } => assert_eq!(name, "keyUp"),
other => panic!("expected Outbound::Event, got {other:?}"),
}
}
#[test]
fn named_key_maps_to_variant_name() {
let (mut app, mut rx) = test_app();
write_key(&mut app, KeyCode::Escape, Key::Escape, ButtonState::Pressed);
app.update();
match rx.try_recv().expect("a keyDown event") {
Outbound::Event { value, .. } => {
assert_eq!(value["key"], "Escape");
assert_eq!(value["code"], "Escape");
}
other => panic!("expected Outbound::Event, got {other:?}"),
}
}
#[test]
fn idle_frame_sends_nothing() {
let (mut app, mut rx) = test_app();
app.update();
assert!(rx.try_recv().is_err());
}
}