use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::config::ControllerConfig;
use crate::detection::InputDeviceState;
#[derive(Debug, Clone, Component)]
pub struct VirtualCursor {
pub position: Vec2,
pub speed: f32,
pub visible: bool,
pub use_left_stick: bool,
}
impl Default for VirtualCursor {
fn default() -> Self {
Self {
position: Vec2::ZERO,
speed: 600.0, visible: false,
use_left_stick: false, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClickState {
#[default]
Idle,
JustPressed,
Held,
JustReleased,
}
#[derive(Debug, Clone, Default, Resource)]
pub struct VirtualCursorState {
pub active: bool,
pub position: Vec2,
pub click_state: ClickState,
}
impl VirtualCursorState {
pub fn reset_frame_state(&mut self) {
self.click_state = match self.click_state {
ClickState::JustPressed => ClickState::Held,
ClickState::JustReleased => ClickState::Idle,
other => other,
};
}
pub fn start_click(&mut self) {
if self.click_state == ClickState::Idle {
self.click_state = ClickState::JustPressed;
}
}
pub fn end_click(&mut self) {
if matches!(self.click_state, ClickState::Held | ClickState::JustPressed) {
self.click_state = ClickState::JustReleased;
}
}
#[must_use]
pub const fn is_clicking(&self) -> bool {
matches!(self.click_state, ClickState::Held | ClickState::JustPressed)
}
#[must_use]
pub const fn just_clicked(&self) -> bool {
matches!(self.click_state, ClickState::JustPressed)
}
#[must_use]
pub const fn just_released(&self) -> bool {
matches!(self.click_state, ClickState::JustReleased)
}
}
pub fn update_virtual_cursor(
time: Res<Time>,
config: Res<ControllerConfig>,
input_state: Res<InputDeviceState>,
mut cursor_state: ResMut<VirtualCursorState>,
gamepads: Query<&Gamepad>,
window_query: Query<&Window, With<PrimaryWindow>>,
mut cursor_query: Query<(&mut Transform, &VirtualCursor)>,
) {
if !input_state.using_gamepad() {
cursor_state.active = false;
return;
}
let Ok(window) = window_query.single() else {
return;
};
let mut cursor_delta = Vec2::ZERO;
for gamepad in gamepads.iter() {
if let Some(_active_gamepad) = input_state.active_gamepad() {
if cursor_query.is_empty() {
continue;
}
for (_transform, virtual_cursor) in &mut cursor_query {
let (x_axis, y_axis) = if virtual_cursor.use_left_stick {
(GamepadAxis::LeftStickX, GamepadAxis::LeftStickY)
} else {
(GamepadAxis::RightStickX, GamepadAxis::RightStickY)
};
if let (Some(x), Some(y)) = (gamepad.get(x_axis), gamepad.get(y_axis)) {
let mut input = config.apply_deadzone_2d(x, y, virtual_cursor.use_left_stick);
input = config.apply_inversion(input, virtual_cursor.use_left_stick);
cursor_delta = input * virtual_cursor.speed * time.delta_secs();
}
}
}
}
if let Ok((mut transform, _)) = cursor_query.single_mut() {
let new_pos = transform.translation.truncate() + cursor_delta;
let half_width: f32 = window.width() / 2.0;
let half_height: f32 = window.height() / 2.0;
let clamped = Vec2::new(
new_pos.x.clamp(-half_width, half_width),
new_pos.y.clamp(-half_height, half_height),
);
transform.translation = clamped.extend(transform.translation.z);
cursor_state.position = clamped;
cursor_state.active = cursor_delta.length() > 0.01;
}
}
pub fn handle_virtual_cursor_clicks(
mut cursor_state: ResMut<VirtualCursorState>,
gamepads: Query<&Gamepad>,
) {
cursor_state.reset_frame_state();
for gamepad in gamepads.iter() {
if gamepad.just_pressed(GamepadButton::South) {
cursor_state.start_click();
}
if gamepad.just_released(GamepadButton::South) {
cursor_state.end_click();
}
}
}
pub fn toggle_virtual_cursor_visibility(
input_state: Res<InputDeviceState>,
mut cursor_query: Query<&mut Visibility, With<VirtualCursor>>,
) {
let should_show = input_state.using_gamepad();
for mut visibility in &mut cursor_query {
*visibility = if should_show {
Visibility::Visible
} else {
Visibility::Hidden
};
}
}
#[derive(Debug, Clone, Message)]
pub struct VirtualCursorClick {
pub position: Vec2,
}
pub fn fire_virtual_cursor_events(
cursor_state: Res<VirtualCursorState>,
mut click_events: MessageWriter<VirtualCursorClick>,
) {
if cursor_state.just_clicked() {
click_events.write(VirtualCursorClick {
position: cursor_state.position,
});
}
}
#[must_use]
pub fn spawn_virtual_cursor(
commands: &mut Commands,
asset_server: &AssetServer,
cursor_image: Option<Handle<Image>>,
) -> Entity {
let image = cursor_image.unwrap_or_else(|| asset_server.load("cursor.png"));
commands
.spawn((
VirtualCursor::default(),
Sprite {
image,
custom_size: Some(Vec2::new(32.0, 32.0)),
..default()
},
Transform::from_xyz(0.0, 0.0, 1000.0), Visibility::Hidden,
))
.id()
}
pub(crate) fn register_virtual_cursor_types(app: &mut App) {
app.init_resource::<VirtualCursorState>()
.add_message::<VirtualCursorClick>();
}
pub(crate) fn add_virtual_cursor_systems(app: &mut App) {
app.add_systems(
Update,
(
update_virtual_cursor,
handle_virtual_cursor_clicks,
toggle_virtual_cursor_visibility,
fire_virtual_cursor_events,
)
.chain(),
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_virtual_cursor_default() {
let cursor = VirtualCursor::default();
assert_eq!(cursor.position, Vec2::ZERO);
assert_eq!(cursor.speed, 600.0);
assert!(!cursor.visible);
assert!(!cursor.use_left_stick);
}
#[test]
fn test_click_state_variants() {
assert_ne!(ClickState::Idle, ClickState::JustPressed);
assert_ne!(ClickState::Held, ClickState::JustReleased);
}
#[test]
fn test_click_state_default() {
let state = ClickState::default();
assert_eq!(state, ClickState::Idle);
}
#[test]
fn test_virtual_cursor_state_default() {
let state = VirtualCursorState::default();
assert!(!state.active);
assert_eq!(state.position, Vec2::ZERO);
assert_eq!(state.click_state, ClickState::Idle);
}
#[test]
fn test_virtual_cursor_state_reset_frame_state() {
let mut state = VirtualCursorState::default();
state.click_state = ClickState::JustPressed;
state.reset_frame_state();
assert_eq!(state.click_state, ClickState::Held);
state.click_state = ClickState::JustReleased;
state.reset_frame_state();
assert_eq!(state.click_state, ClickState::Idle);
state.click_state = ClickState::Held;
state.reset_frame_state();
assert_eq!(state.click_state, ClickState::Held);
}
#[test]
fn test_virtual_cursor_state_start_click() {
let mut state = VirtualCursorState::default();
state.start_click();
assert_eq!(state.click_state, ClickState::JustPressed);
state.click_state = ClickState::Held;
state.start_click();
assert_eq!(state.click_state, ClickState::Held);
}
#[test]
fn test_virtual_cursor_state_end_click() {
let mut state = VirtualCursorState::default();
state.click_state = ClickState::Held;
state.end_click();
assert_eq!(state.click_state, ClickState::JustReleased);
state.click_state = ClickState::Idle;
state.end_click();
assert_eq!(state.click_state, ClickState::Idle);
}
#[test]
fn test_virtual_cursor_click_event() {
let event = VirtualCursorClick {
position: Vec2::new(100.0, 200.0),
};
assert_eq!(event.position.x, 100.0);
assert_eq!(event.position.y, 200.0);
}
}