use bevy::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect)]
pub enum InputDevice {
#[default]
Mouse,
Keyboard,
Gamepad(Entity),
}
impl InputDevice {
#[must_use]
pub fn is_gamepad(&self) -> bool {
matches!(self, Self::Gamepad(_))
}
#[must_use]
pub fn is_mouse(&self) -> bool {
matches!(self, Self::Mouse)
}
#[must_use]
pub fn is_keyboard(&self) -> bool {
matches!(self, Self::Keyboard)
}
#[must_use]
pub fn gamepad(&self) -> Option<Entity> {
match self {
Self::Gamepad(entity) => Some(*entity),
_ => None,
}
}
}
#[derive(Debug, Clone, Resource, Reflect)]
#[reflect(Resource)]
pub struct InputDeviceState {
pub active_device: InputDevice,
pub previous_device: InputDevice,
pub device_changed: bool,
pub connected_gamepads: Vec<Entity>,
pub primary_gamepad: Option<Entity>,
pub mouse_movement_threshold: f32,
pub auto_switch: bool,
}
impl Default for InputDeviceState {
fn default() -> Self {
Self {
active_device: InputDevice::Mouse,
previous_device: InputDevice::Mouse,
device_changed: false,
connected_gamepads: Vec::new(),
primary_gamepad: None,
mouse_movement_threshold: 1.0,
auto_switch: true,
}
}
}
impl InputDeviceState {
#[must_use]
pub fn using_mouse(&self) -> bool {
self.active_device.is_mouse()
}
#[must_use]
pub fn using_keyboard(&self) -> bool {
self.active_device.is_keyboard()
}
#[must_use]
pub fn using_gamepad(&self) -> bool {
self.active_device.is_gamepad()
}
#[must_use]
pub fn using_non_mouse(&self) -> bool {
!self.using_mouse()
}
#[must_use]
pub fn active_gamepad(&self) -> Option<Entity> {
self.active_device.gamepad()
}
fn set_active(&mut self, device: InputDevice) {
if self.active_device != device {
self.previous_device = self.active_device;
self.active_device = device;
self.device_changed = true;
}
}
}
#[derive(Debug, Clone, Message)]
pub struct InputDeviceChanged {
pub previous: InputDevice,
pub current: InputDevice,
}
#[derive(Debug, Clone, Message)]
pub struct GamepadConnected {
pub gamepad: Entity,
pub name: Option<String>,
}
#[derive(Debug, Clone, Message)]
pub struct GamepadDisconnected {
pub gamepad: Entity,
}
pub fn detect_input_device(
mut state: ResMut<InputDeviceState>,
mut device_changed_events: MessageWriter<InputDeviceChanged>,
mut mouse_motion: MessageReader<bevy::input::mouse::MouseMotion>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
keyboard: Res<ButtonInput<KeyCode>>,
gamepads: Query<(&Gamepad, Entity)>,
) {
state.device_changed = false;
if !state.auto_switch {
return;
}
let previous = state.active_device;
let mouse_moved = mouse_motion.read().count() > 0;
let mouse_clicked = mouse_buttons.get_just_pressed().next().is_some();
if mouse_moved || mouse_clicked {
state.set_active(InputDevice::Mouse);
}
if keyboard.get_just_pressed().next().is_some() {
state.set_active(InputDevice::Keyboard);
}
for (gamepad, gamepad_entity) in gamepads.iter() {
let has_button_input = gamepad.get_just_pressed().next().is_some();
let has_axis_input = false;
if has_button_input || has_axis_input {
state.set_active(InputDevice::Gamepad(gamepad_entity));
break;
}
}
if state.device_changed {
device_changed_events.write(InputDeviceChanged {
previous,
current: state.active_device,
});
}
}
pub fn track_gamepad_connections(
mut state: ResMut<InputDeviceState>,
mut connected_events: MessageWriter<GamepadConnected>,
mut disconnected_events: MessageWriter<GamepadDisconnected>,
gamepads: Query<(Entity, Option<&Name>), Added<Gamepad>>,
mut removed_gamepads: RemovedComponents<Gamepad>,
) {
for (entity, name) in gamepads.iter() {
if !state.connected_gamepads.contains(&entity) {
state.connected_gamepads.push(entity);
if state.primary_gamepad.is_none() {
state.primary_gamepad = Some(entity);
}
connected_events.write(GamepadConnected {
gamepad: entity,
name: name.map(std::string::ToString::to_string),
});
}
}
for entity in removed_gamepads.read() {
if let Some(pos) = state.connected_gamepads.iter().position(|&e| e == entity) {
state.connected_gamepads.remove(pos);
if state.primary_gamepad == Some(entity) {
state.primary_gamepad = state.connected_gamepads.first().copied();
}
if state.active_device == InputDevice::Gamepad(entity) {
state.active_device = state
.primary_gamepad
.map_or(InputDevice::Keyboard, InputDevice::Gamepad);
}
disconnected_events.write(GamepadDisconnected { gamepad: entity });
}
}
}
pub(crate) fn register_detection_types(app: &mut App) {
app.register_type::<InputDevice>()
.register_type::<InputDeviceState>()
.init_resource::<InputDeviceState>()
.add_message::<InputDeviceChanged>()
.add_message::<GamepadConnected>()
.add_message::<GamepadDisconnected>();
}
pub(crate) fn add_detection_systems(app: &mut App) {
app.add_systems(
PreUpdate,
(track_gamepad_connections, detect_input_device).chain(),
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_input_device_default() {
let device = InputDevice::default();
assert_eq!(device, InputDevice::Mouse);
}
#[test]
fn test_input_device_is_gamepad() {
assert!(InputDevice::Gamepad(Entity::PLACEHOLDER).is_gamepad());
assert!(!InputDevice::Mouse.is_gamepad());
assert!(!InputDevice::Keyboard.is_gamepad());
}
#[test]
fn test_input_device_is_mouse() {
assert!(InputDevice::Mouse.is_mouse());
assert!(!InputDevice::Keyboard.is_mouse());
assert!(!InputDevice::Gamepad(Entity::PLACEHOLDER).is_mouse());
}
#[test]
fn test_input_device_is_keyboard() {
assert!(InputDevice::Keyboard.is_keyboard());
assert!(!InputDevice::Mouse.is_keyboard());
assert!(!InputDevice::Gamepad(Entity::PLACEHOLDER).is_keyboard());
}
#[test]
fn test_input_device_gamepad_returns_entity() {
let entity = Entity::PLACEHOLDER;
let device = InputDevice::Gamepad(entity);
assert_eq!(device.gamepad(), Some(entity));
}
#[test]
fn test_input_device_gamepad_returns_none_for_mouse() {
assert!(InputDevice::Mouse.gamepad().is_none());
}
#[test]
fn test_input_device_gamepad_returns_none_for_keyboard() {
assert!(InputDevice::Keyboard.gamepad().is_none());
}
#[test]
fn test_input_device_equality() {
assert_eq!(InputDevice::Mouse, InputDevice::Mouse);
assert_eq!(InputDevice::Keyboard, InputDevice::Keyboard);
assert_ne!(InputDevice::Mouse, InputDevice::Keyboard);
let entity = Entity::PLACEHOLDER;
assert_eq!(InputDevice::Gamepad(entity), InputDevice::Gamepad(entity));
}
#[test]
fn test_input_device_state_default() {
let state = InputDeviceState::default();
assert_eq!(state.active_device, InputDevice::Mouse);
assert_eq!(state.previous_device, InputDevice::Mouse);
assert!(!state.device_changed);
assert!(state.connected_gamepads.is_empty());
assert!(state.primary_gamepad.is_none());
assert!(state.auto_switch);
}
#[test]
fn test_input_device_state_using_mouse() {
let mut state = InputDeviceState::default();
state.active_device = InputDevice::Mouse;
assert!(state.using_mouse());
assert!(!state.using_keyboard());
assert!(!state.using_gamepad());
}
#[test]
fn test_input_device_state_using_keyboard() {
let mut state = InputDeviceState::default();
state.active_device = InputDevice::Keyboard;
assert!(!state.using_mouse());
assert!(state.using_keyboard());
assert!(!state.using_gamepad());
}
#[test]
fn test_input_device_state_using_gamepad() {
let mut state = InputDeviceState::default();
state.active_device = InputDevice::Gamepad(Entity::PLACEHOLDER);
assert!(!state.using_mouse());
assert!(!state.using_keyboard());
assert!(state.using_gamepad());
}
#[test]
fn test_input_device_state_using_non_mouse() {
let mut state = InputDeviceState::default();
state.active_device = InputDevice::Mouse;
assert!(!state.using_non_mouse());
state.active_device = InputDevice::Keyboard;
assert!(state.using_non_mouse());
state.active_device = InputDevice::Gamepad(Entity::PLACEHOLDER);
assert!(state.using_non_mouse());
}
#[test]
fn test_input_device_state_active_gamepad() {
let mut state = InputDeviceState::default();
assert!(state.active_gamepad().is_none());
let entity = Entity::PLACEHOLDER;
state.active_device = InputDevice::Gamepad(entity);
assert_eq!(state.active_gamepad(), Some(entity));
}
#[test]
fn test_input_device_state_set_active_changes_device() {
let mut state = InputDeviceState::default();
state.set_active(InputDevice::Keyboard);
assert_eq!(state.active_device, InputDevice::Keyboard);
assert_eq!(state.previous_device, InputDevice::Mouse);
assert!(state.device_changed);
}
#[test]
fn test_input_device_state_set_active_same_device() {
let mut state = InputDeviceState::default();
state.device_changed = false;
state.set_active(InputDevice::Mouse);
assert_eq!(state.active_device, InputDevice::Mouse);
assert!(!state.device_changed);
}
#[test]
fn test_input_device_state_mouse_movement_threshold() {
let state = InputDeviceState::default();
assert!(state.mouse_movement_threshold > 0.0);
}
#[test]
fn test_input_device_changed_event() {
let event = InputDeviceChanged {
previous: InputDevice::Mouse,
current: InputDevice::Keyboard,
};
assert_eq!(event.previous, InputDevice::Mouse);
assert_eq!(event.current, InputDevice::Keyboard);
}
#[test]
fn test_gamepad_connected_event() {
let event = GamepadConnected {
gamepad: Entity::PLACEHOLDER,
name: Some("Xbox Controller".to_string()),
};
assert_eq!(event.gamepad, Entity::PLACEHOLDER);
assert_eq!(event.name, Some("Xbox Controller".to_string()));
}
#[test]
fn test_gamepad_connected_event_no_name() {
let event = GamepadConnected {
gamepad: Entity::PLACEHOLDER,
name: None,
};
assert!(event.name.is_none());
}
#[test]
fn test_gamepad_disconnected_event() {
let event = GamepadDisconnected {
gamepad: Entity::PLACEHOLDER,
};
assert_eq!(event.gamepad, Entity::PLACEHOLDER);
}
#[test]
fn test_connected_gamepads_tracking() {
let mut state = InputDeviceState::default();
let entity = Entity::PLACEHOLDER;
state.connected_gamepads.push(entity);
assert!(state.connected_gamepads.contains(&entity));
assert_eq!(state.connected_gamepads.len(), 1);
}
#[test]
fn test_primary_gamepad_assignment() {
let mut state = InputDeviceState::default();
let entity = Entity::PLACEHOLDER;
state.primary_gamepad = Some(entity);
assert_eq!(state.primary_gamepad, Some(entity));
}
}