use bevy::input::keyboard::{Key, KeyCode, KeyboardInput, NativeKey, NativeKeyCode};
use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseScrollUnit, MouseWheel};
use bevy::input::touch::TouchPhase;
use bevy::input::ButtonState;
use bevy::prelude::*;
use bevy::remote::{error_codes, BrpError, BrpResult};
use bevy::window::PrimaryWindow;
use serde::Deserialize;
use serde_json::{json, Value};
#[derive(Debug, Deserialize)]
pub struct InputCommand {
pub kind: InputKind,
#[serde(default)]
pub action: InputAction,
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub button: Option<Value>,
#[serde(default)]
pub x: Option<f32>,
#[serde(default)]
pub y: Option<f32>,
#[serde(default)]
pub unit: Option<String>,
#[serde(default)]
pub clear: bool,
}
#[derive(Debug, Deserialize)]
pub enum InputKind {
Keyboard,
Mouse,
Scroll,
Cursor,
}
#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum InputAction {
Press,
Release,
#[default]
Tap,
}
fn invalid_params(message: String) -> BrpError {
BrpError { code: error_codes::INVALID_PARAMS, message, data: None }
}
enum Injected {
Key { key_code: KeyCode, logical: Key, action: InputAction, window: Entity },
Typed(KeyboardInput),
Mouse(MouseButton, InputAction, Entity),
Scroll { unit: MouseScrollUnit, x: f32, y: f32, window: Entity },
Cursor(Option<Vec2>),
}
pub fn logical_for(key_code: KeyCode) -> Key {
match serde_json::to_value(key_code) {
Ok(Value::String(name)) => serde_json::from_value::<Key>(Value::String(name))
.unwrap_or(Key::Unidentified(NativeKey::Unidentified)),
_ => Key::Unidentified(NativeKey::Unidentified),
}
}
pub fn typed(ch: char, window: Entity) -> Option<KeyboardInput> {
let logical_key = if ch == ' ' {
Key::Space
} else {
let mut buf = [0u8; 4];
serde_json::from_value::<Key>(json!({ "Character": ch.encode_utf8(&mut buf) })).ok()?
};
let text = match &logical_key {
Key::Character(s) => Some(s.clone()),
_ => None,
};
Some(KeyboardInput {
key_code: KeyCode::Unidentified(NativeKeyCode::Unidentified),
logical_key,
state: ButtonState::Pressed,
text,
repeat: false,
window,
})
}
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq)]
pub struct DebugCursor(pub Option<Vec2>);
pub fn cursor_position(window: &Window, injected: &DebugCursor) -> Option<Vec2> {
injected.0.or_else(|| window.cursor_position())
}
#[derive(Resource, Default)]
pub struct PendingInput {
queue: Vec<Injected>,
release_next_keys: Vec<KeyboardInput>,
release_next_buttons: Vec<MouseButtonInput>,
}
impl PendingInput {
pub fn queue_key(&mut self, key: KeyCode, action: InputAction) {
self.queue.push(Injected::Key {
key_code: key,
logical: logical_for(key),
action,
window: Entity::PLACEHOLDER,
});
}
pub fn queue_typed(&mut self, message: KeyboardInput) {
self.queue.push(Injected::Typed(message));
}
pub fn queue_text(&mut self, text: &str, window: Entity) -> Result<(), char> {
let built: Result<Vec<_>, char> =
text.chars().map(|ch| typed(ch, window).ok_or(ch)).collect();
self.queue.extend(built?.into_iter().map(Injected::Typed));
Ok(())
}
pub fn queue_tap_key(&mut self, key: KeyCode) {
self.queue_key(key, InputAction::Tap);
}
pub fn queue_mouse(&mut self, button: MouseButton, action: InputAction) {
self.queue.push(Injected::Mouse(button, action, Entity::PLACEHOLDER));
}
pub fn queue_cursor(&mut self, at: Option<Vec2>) {
self.queue.push(Injected::Cursor(at));
}
}
pub fn apply_pending_input(
mut pending: ResMut<PendingInput>,
mut keyboard: MessageWriter<KeyboardInput>,
mut mouse: MessageWriter<MouseButtonInput>,
mut wheel: MessageWriter<MouseWheel>,
mut cursor: ResMut<DebugCursor>,
) {
let release_keys = core::mem::take(&mut pending.release_next_keys);
let release_buttons = core::mem::take(&mut pending.release_next_buttons);
let queue = core::mem::take(&mut pending.queue);
for message in release_keys {
keyboard.write(message);
}
for message in release_buttons {
mouse.write(message);
}
let mut keys_touched: Vec<KeyCode> = Vec::new();
let mut buttons_touched: Vec<MouseButton> = Vec::new();
let mut cursor_moved = false;
let mut deferred: Vec<Injected> = Vec::new();
for cmd in queue {
if !deferred.is_empty() {
deferred.push(cmd);
continue;
}
match &cmd {
Injected::Key { key_code, .. } if keys_touched.contains(key_code) => {
deferred.push(cmd);
continue;
}
Injected::Mouse(button, _, _) if buttons_touched.contains(button) => {
deferred.push(cmd);
continue;
}
Injected::Typed(_) if !keys_touched.is_empty() || !buttons_touched.is_empty() => {
deferred.push(cmd);
continue;
}
Injected::Cursor(_) if cursor_moved || !buttons_touched.is_empty() => {
deferred.push(cmd);
continue;
}
_ => {}
}
match cmd {
Injected::Key { key_code, logical, action, window } => {
keys_touched.push(key_code);
let message = |state| KeyboardInput {
key_code,
logical_key: logical.clone(),
state,
text: None,
repeat: false,
window,
};
match action {
InputAction::Press => {
keyboard.write(message(ButtonState::Pressed));
}
InputAction::Release => {
keyboard.write(message(ButtonState::Released));
}
InputAction::Tap => {
keyboard.write(message(ButtonState::Pressed));
pending.release_next_keys.push(message(ButtonState::Released));
}
}
}
Injected::Typed(message) => {
let mut released = message.clone();
released.state = ButtonState::Released;
keyboard.write(message);
pending.release_next_keys.push(released);
}
Injected::Mouse(button, action, window) => {
buttons_touched.push(button);
let message = |state| MouseButtonInput { button, state, window };
match action {
InputAction::Press => {
mouse.write(message(ButtonState::Pressed));
}
InputAction::Release => {
mouse.write(message(ButtonState::Released));
}
InputAction::Tap => {
mouse.write(message(ButtonState::Pressed));
pending.release_next_buttons.push(message(ButtonState::Released));
}
}
}
Injected::Scroll { unit, x, y, window } => {
wheel.write(MouseWheel {
unit,
x,
y,
window,
phase: TouchPhase::Moved,
});
}
Injected::Cursor(at) => {
cursor_moved = true;
cursor.0 = at;
}
}
}
pending.queue = deferred;
}
pub fn handle_input(
In(params): In<Option<Value>>,
mut pending: ResMut<PendingInput>,
window: Option<Single<Entity, With<PrimaryWindow>>>,
) -> BrpResult {
let cmd: InputCommand = match params.as_ref() {
Some(p) => serde_json::from_value(p.clone())
.map_err(|e| invalid_params(format!("invalid input params: {e}")))?,
None => return Err(invalid_params("Missing input parameters".to_string())),
};
let addressed = window.as_ref().map_or(Entity::PLACEHOLDER, |w| **w);
match cmd.kind {
InputKind::Keyboard => {
if cmd.key.is_some() && cmd.text.is_some() {
return Err(invalid_params(
"send `key` or `text`, not both: `key` names a key on the keyboard, `text` is \
what should arrive, and there is no order between them"
.to_string(),
));
}
match (cmd.key, cmd.text) {
(Some(name), None) => {
let keycode: KeyCode = serde_json::from_value(Value::String(name.clone()))
.map_err(|e| invalid_params(format!("unknown key `{name}`: {e}")))?;
pending.queue.push(Injected::Key {
key_code: keycode,
logical: logical_for(keycode),
action: cmd.action,
window: addressed,
});
}
(None, Some(text)) => {
if cmd.action != InputAction::Tap {
return Err(invalid_params(
"`text` has no press and no release; drop `action`, or use `key` if you \
meant to hold something down"
.to_string(),
));
}
if text.is_empty() {
return Err(invalid_params(
"`text` is empty, which injects nothing — omit it or send a character"
.to_string(),
));
}
if let Some(ch) = text.chars().find(|c| c.is_control()) {
return Err(invalid_params(format!(
"`text` contains the control character {ch:?}; a text field matches \
`Key::Enter` and never `Key::Character(\"\\n\")`, so send \
`key: \"Enter\"` (or \"Tab\", \"Backspace\", \"Escape\") instead"
)));
}
pending.queue_text(&text, addressed).map_err(|ch| {
invalid_params(format!("no logical key exists for the character {ch:?}"))
})?;
}
(None, None) => {
return Err(invalid_params(
"keyboard input needs `key` (a key on the keyboard) or `text` (what should \
arrive)"
.to_string(),
))
}
(Some(_), Some(_)) => unreachable!("both-supplied is refused above"),
}
}
InputKind::Mouse => {
let raw = cmd
.button
.ok_or_else(|| invalid_params("Missing 'button' for mouse input".to_string()))?;
let button: MouseButton = serde_json::from_value(raw.clone())
.map_err(|e| invalid_params(format!("unknown mouse button `{raw}`: {e}")))?;
pending.queue.push(Injected::Mouse(button, cmd.action, addressed));
}
InputKind::Scroll => {
let window = window.ok_or_else(|| BrpError {
code: error_codes::INTERNAL_ERROR,
message: "scroll needs a primary window to address the MouseWheel message to, and \
there is none"
.to_string(),
data: None,
})?;
let unit = match cmd.unit.as_deref() {
None | Some("Line") => MouseScrollUnit::Line,
Some("Pixel") => MouseScrollUnit::Pixel,
Some(other) => {
return Err(invalid_params(format!(
"unknown scroll unit `{other}`; expected \"Line\" or \"Pixel\""
)))
}
};
let (x, y) = (cmd.x.unwrap_or(0.0), cmd.y.unwrap_or(0.0));
if !x.is_finite() || !y.is_finite() {
return Err(invalid_params(format!(
"scroll amounts must be finite, got x={x}, y={y}"
)));
}
pending.queue.push(Injected::Scroll { unit, x, y, window: *window });
}
InputKind::Cursor => {
if cmd.clear {
pending.queue.push(Injected::Cursor(None));
} else {
let x = cmd
.x
.ok_or_else(|| invalid_params("Missing 'x' for cursor input".to_string()))?;
let y = cmd
.y
.ok_or_else(|| invalid_params("Missing 'y' for cursor input".to_string()))?;
if !x.is_finite() || !y.is_finite() {
return Err(invalid_params(format!(
"cursor position must be finite, got x={x}, y={y}"
)));
}
pending.queue.push(Injected::Cursor(Some(Vec2::new(x, y))));
}
}
}
Ok(json!({
"success": true,
"message": "Input queued; applied on the next frame's PreUpdate",
}))
}