use bevy::input::keyboard::{Key, KeyboardInput, NativeKey};
use bevy::input::InputSystems;
use bevy::prelude::*;
use bevy_fairy::{apply_pending_input, typed, DebugCursor, InputAction, PendingInput};
#[derive(Resource, Default)]
struct Seen(Vec<Edges>);
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
struct Edges {
just_pressed: bool,
pressed: bool,
just_released: bool,
}
fn observe(keys: Res<ButtonInput<KeyCode>>, mut seen: ResMut<Seen>) {
seen.0.push(Edges {
just_pressed: keys.just_pressed(KeyCode::KeyQ),
pressed: keys.pressed(KeyCode::KeyQ),
just_released: keys.just_released(KeyCode::KeyQ),
});
}
fn app_correctly_ordered() -> App {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_plugins(bevy::input::InputPlugin)
.init_resource::<PendingInput>()
.init_resource::<bevy_fairy::DebugCursor>()
.init_resource::<Seen>()
.add_systems(PreUpdate, apply_pending_input.before(InputSystems))
.add_systems(Update, observe);
app
}
#[derive(Resource, Default)]
struct Heard(Vec<Vec<Key>>);
fn listen(mut events: MessageReader<KeyboardInput>, mut heard: ResMut<Heard>) {
heard.0.push(
events
.read()
.filter(|e| e.state.is_pressed())
.map(|e| e.logical_key.clone())
.collect(),
);
}
fn app_listening() -> App {
let mut app = app_correctly_ordered();
app.init_resource::<Heard>().add_systems(Update, listen);
app
}
fn spelled(frame: &[Key]) -> String {
frame
.iter()
.map(|k| match k {
Key::Character(s) => s.to_string(),
Key::Space => " ".to_owned(),
other => format!("{other:?}"),
})
.collect()
}
fn queue_key(app: &mut App, key: KeyCode, action: InputAction) {
app.world_mut()
.resource_mut::<PendingInput>()
.queue_key(key, action);
}
fn seen(app: &App) -> Vec<Edges> {
app.world().resource::<Seen>().0.clone()
}
#[test]
fn a_tap_presses_for_one_frame_and_releases_the_next() {
let mut app = app_correctly_ordered();
queue_key(&mut app, KeyCode::KeyQ, InputAction::Tap);
app.update();
app.update();
app.update();
let frames = seen(&app);
assert!(
frames[0].just_pressed && frames[0].pressed,
"the tap must be visible to `just_pressed` on the frame it lands: {frames:?}"
);
assert!(
frames[1].just_released && !frames[1].pressed,
"and released on the very next frame, the way a physical key behaves: {frames:?}"
);
assert_eq!(
frames[2],
Edges::default(),
"and leave nothing behind afterwards: {frames:?}"
);
}
#[test]
fn ordering_after_input_systems_costs_a_frame() {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_plugins(bevy::input::InputPlugin)
.init_resource::<PendingInput>()
.init_resource::<bevy_fairy::DebugCursor>()
.init_resource::<Seen>()
.add_systems(PreUpdate, apply_pending_input.after(InputSystems))
.add_systems(Update, observe);
queue_key(&mut app, KeyCode::KeyQ, InputAction::Tap);
app.update();
app.update();
let frames = seen(&app);
assert!(
!frames[0].just_pressed,
"misordered, the press cannot be visible on the frame it was written: {frames:?}"
);
assert!(
frames[1].just_pressed,
"but it must still arrive on the next frame — a message survives the clear, where a direct \
`ButtonInput` write did not. If this is false, the write has moved back to the fold: \
{frames:?}"
);
}
#[test]
fn two_taps_in_one_batch_produce_two_separate_press_edges() {
let mut app = app_correctly_ordered();
queue_key(&mut app, KeyCode::KeyQ, InputAction::Tap);
queue_key(&mut app, KeyCode::KeyQ, InputAction::Tap);
for _ in 0..4 {
app.update();
}
let presses = seen(&app).iter().filter(|e| e.just_pressed).count();
assert_eq!(
presses,
2,
"two taps must produce two press edges, not one: {:?}",
seen(&app)
);
}
#[test]
fn a_tap_followed_by_a_press_leaves_the_key_held() {
let mut app = app_correctly_ordered();
queue_key(&mut app, KeyCode::KeyQ, InputAction::Tap);
queue_key(&mut app, KeyCode::KeyQ, InputAction::Press);
for _ in 0..4 {
app.update();
}
let frames = seen(&app);
let last = frames.last().copied().unwrap_or_default();
assert!(
last.pressed,
"the key must still be held after the tap's release has run: {frames:?}"
);
}
#[test]
fn a_tap_and_a_release_never_show_both_edges_with_the_key_unpressed() {
let mut app = app_correctly_ordered();
queue_key(&mut app, KeyCode::KeyQ, InputAction::Tap);
queue_key(&mut app, KeyCode::KeyQ, InputAction::Release);
for _ in 0..4 {
app.update();
}
for (n, frame) in seen(&app).iter().enumerate() {
assert!(
!(frame.just_pressed && frame.just_released && !frame.pressed),
"frame {n} shows a press and a release at once with the key unpressed: {frame:?}"
);
}
}
#[test]
fn a_cursor_move_lands_and_clearing_releases_it() {
let mut app = app_correctly_ordered();
app.world_mut()
.resource_mut::<PendingInput>()
.queue_cursor(Some(Vec2::new(120.0, 64.0)));
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, Some(Vec2::new(120.0, 64.0)));
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, Some(Vec2::new(120.0, 64.0)));
app.world_mut().resource_mut::<PendingInput>().queue_cursor(None);
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, None);
}
#[test]
fn a_move_before_an_untouched_button_applies_in_the_same_frame() {
let mut app = app_correctly_ordered();
{
let mut pending = app.world_mut().resource_mut::<PendingInput>();
pending.queue_cursor(Some(Vec2::new(10.0, 20.0)));
pending.queue_mouse(MouseButton::Left, InputAction::Press);
}
app.update();
assert_eq!(
app.world().resource::<DebugCursor>().0,
Some(Vec2::new(10.0, 20.0)),
"the aim has to land on the frame the press is read"
);
assert!(app.world().resource::<ButtonInput<MouseButton>>().pressed(MouseButton::Left));
}
#[test]
fn a_move_after_a_button_waits_for_the_next_frame() {
let mut app = app_correctly_ordered();
{
let mut pending = app.world_mut().resource_mut::<PendingInput>();
pending.queue_cursor(Some(Vec2::new(10.0, 10.0)));
pending.queue_mouse(MouseButton::Left, InputAction::Press);
pending.queue_cursor(Some(Vec2::new(90.0, 90.0)));
}
app.update();
assert_eq!(
app.world().resource::<DebugCursor>().0,
Some(Vec2::new(10.0, 10.0)),
"the press frame must still read the position it was aimed at"
);
app.update();
assert_eq!(
app.world().resource::<DebugCursor>().0,
Some(Vec2::new(90.0, 90.0)),
"and the drag continues on the next frame"
);
}
#[test]
fn two_moves_in_one_batch_are_spread_over_two_frames() {
let mut app = app_correctly_ordered();
{
let mut pending = app.world_mut().resource_mut::<PendingInput>();
pending.queue_cursor(Some(Vec2::new(1.0, 1.0)));
pending.queue_cursor(Some(Vec2::new(2.0, 2.0)));
}
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, Some(Vec2::new(1.0, 1.0)));
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, Some(Vec2::new(2.0, 2.0)));
}
#[test]
fn the_injected_pointer_takes_precedence_and_hands_back_when_cleared() {
use bevy::window::{Window, WindowResolution};
let window = Window {
resolution: WindowResolution::new(800, 600),
..Default::default()
};
assert_eq!(
bevy_fairy::cursor_position(&window, &DebugCursor(Some(Vec2::new(5.0, 6.0)))),
Some(Vec2::new(5.0, 6.0))
);
assert_eq!(
bevy_fairy::cursor_position(&window, &DebugCursor(None)),
window.cursor_position()
);
}
#[test]
fn a_release_does_not_overtake_the_moves_queued_before_it() {
let mut app = app_correctly_ordered();
{
let mut pending = app.world_mut().resource_mut::<PendingInput>();
pending.queue_cursor(Some(Vec2::new(100.0, 100.0)));
pending.queue_mouse(MouseButton::Left, InputAction::Press);
pending.queue_cursor(Some(Vec2::new(140.0, 130.0)));
pending.queue_cursor(Some(Vec2::new(180.0, 160.0)));
pending.queue_mouse(MouseButton::Left, InputAction::Release);
}
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, Some(Vec2::new(100.0, 100.0)));
assert!(app.world().resource::<ButtonInput<MouseButton>>().pressed(MouseButton::Left));
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, Some(Vec2::new(140.0, 130.0)));
assert!(
app.world().resource::<ButtonInput<MouseButton>>().pressed(MouseButton::Left),
"the release must not have jumped the queue"
);
app.update();
assert_eq!(app.world().resource::<DebugCursor>().0, Some(Vec2::new(180.0, 160.0)));
assert!(app.world().resource::<ButtonInput<MouseButton>>().just_released(MouseButton::Left));
}
fn queue_text(app: &mut App, text: &str) {
app.world_mut()
.resource_mut::<PendingInput>()
.queue_text(text, Entity::PLACEHOLDER)
.unwrap_or_else(|ch| panic!("no logical key for {ch:?}"));
}
fn heard(app: &App) -> Vec<Vec<Key>> {
app.world().resource::<Heard>().0.clone()
}
#[test]
fn a_whole_word_lands_in_one_frame() {
let mut app = app_listening();
queue_text(&mut app, "site_67");
app.update();
let frames = heard(&app);
assert_eq!(
spelled(&frames[0]),
"site_67",
"the whole string must arrive in order, in one frame: {frames:?}"
);
}
#[test]
fn every_typed_character_is_released_and_nothing_stays_pressed() {
let mut app = app_listening();
queue_text(&mut app, "wall");
for _ in 0..3 {
app.update();
}
let world = app.world();
assert_eq!(
world.resource::<ButtonInput<Key>>().get_pressed().count(),
0,
"a typed character that is never released stays pressed for the life of the app"
);
assert_eq!(
world.resource::<ButtonInput<KeyCode>>().get_pressed().count(),
0,
"and the physical side must be clean too"
);
}
#[test]
fn a_named_key_carries_both_halves() {
let mut app = app_listening();
queue_key(&mut app, KeyCode::Escape, InputAction::Tap);
app.update();
assert!(
app.world().resource::<ButtonInput<KeyCode>>().just_pressed(KeyCode::Escape),
"the physical half must reach a `ButtonInput` reader"
);
assert!(
heard(&app)[0].contains(&Key::Escape),
"and the logical half must reach the message stream, from the same request: {:?}",
heard(&app)
);
}
#[test]
fn a_physical_only_key_states_that_it_has_no_logical_value() {
let mut app = app_listening();
queue_key(&mut app, KeyCode::KeyW, InputAction::Tap);
app.update();
assert!(
app.world().resource::<ButtonInput<KeyCode>>().just_pressed(KeyCode::KeyW),
"the physical half is exact and must still land"
);
assert!(
heard(&app)[0].contains(&Key::Unidentified(NativeKey::Unidentified)),
"and the logical half must state the absence: {:?}",
heard(&app)
);
}
#[test]
fn a_space_is_the_space_bar_not_a_character() {
let mut app = app_listening();
queue_text(&mut app, "a b");
app.update();
let frame = &heard(&app)[0];
assert_eq!(frame.len(), 3, "three characters: {frame:?}");
assert_eq!(frame[1], Key::Space, "the middle one must be `Space`: {frame:?}");
}
#[test]
fn text_waits_for_the_frame_after_the_key_that_opened_the_field() {
let mut app = app_listening();
{
let mut pending = app.world_mut().resource_mut::<PendingInput>();
pending.queue_key(KeyCode::KeyM, InputAction::Tap);
pending
.queue_text("porch", Entity::PLACEHOLDER)
.unwrap_or_else(|ch| panic!("no logical key for {ch:?}"));
}
app.update();
app.update();
let frames = heard(&app);
assert_eq!(
spelled(&frames[0]),
"Unidentified(Unidentified)",
"frame 0 carries the opening key and no characters: {frames:?}"
);
assert_eq!(
spelled(&frames[1]),
"porch",
"and the characters follow on the next frame: {frames:?}"
);
}
#[test]
fn an_enter_after_text_commits_in_the_same_frame() {
let mut app = app_listening();
{
let mut pending = app.world_mut().resource_mut::<PendingInput>();
pending
.queue_text("porch_a", Entity::PLACEHOLDER)
.unwrap_or_else(|ch| panic!("no logical key for {ch:?}"));
pending.queue_key(KeyCode::Enter, InputAction::Tap);
}
app.update();
let frame = &heard(&app)[0];
assert_eq!(
spelled(frame),
"porch_aEnter",
"the name and its commit must land together, in that order: {frame:?}"
);
}
#[test]
fn two_text_commands_in_one_frame_concatenate_in_order() {
let mut app = app_listening();
queue_text(&mut app, "si");
queue_text(&mut app, "te");
app.update();
assert_eq!(spelled(&heard(&app)[0]), "site");
}
#[test]
fn a_typed_character_is_shaped_like_a_real_one() {
let message = typed('q', Entity::PLACEHOLDER).unwrap_or_else(|| panic!("`q` must build"));
assert_eq!(message.logical_key, Key::Character("q".into()));
assert_eq!(
message.text.as_deref(),
Some("q"),
"`text` carries the character produced, matching what a real keyboard event reports"
);
assert!(!message.repeat, "an injected key is never an OS auto-repeat");
assert_eq!(
message.key_code,
KeyCode::Unidentified(bevy::input::keyboard::NativeKeyCode::Unidentified),
"a character that arrived has no physical origin this process can know"
);
}