use ahash::AHashMap;
use bevy::{
input::{mouse::MouseButtonInput, ButtonState},
prelude::*,
window::PrimaryWindow,
};
use de_core::{
gamestate::GameState, schedule::InputSchedule, screengeom::ScreenRect, state::AppState,
};
use crate::hud::HudNodes;
const DRAGGING_THRESHOLD: f32 = 0.02;
const DOUBLE_CLICK_TIME: f64 = 0.5;
pub(super) struct InputPlugin;
impl Plugin for InputPlugin {
fn build(&self, app: &mut App) {
app.add_event::<MouseClickedEvent>()
.add_event::<MouseDoubleClickedEvent>()
.add_event::<MouseDraggedEvent>()
.add_systems(OnEnter(AppState::InGame), setup)
.add_systems(OnExit(AppState::InGame), cleanup)
.add_systems(
InputSchedule,
(
update_position.in_set(MouseSet::Position),
update_drags
.run_if(resource_exists_and_changed::<MousePosition>)
.in_set(MouseSet::Drags)
.after(MouseSet::Position),
update_buttons
.in_set(MouseSet::SingeButton)
.after(MouseSet::Drags),
check_double_click
.in_set(MouseSet::Buttons)
.after(MouseSet::SingeButton),
)
.run_if(in_state(GameState::Playing)),
);
}
}
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, SystemSet)]
pub(crate) enum MouseSet {
Position,
Drags,
SingeButton,
Buttons,
}
#[derive(Event)]
pub(crate) struct MouseClickedEvent {
button: MouseButton,
position: Vec2,
}
impl MouseClickedEvent {
fn new(button: MouseButton, position: Vec2) -> Self {
Self { button, position }
}
pub(crate) fn button(&self) -> MouseButton {
self.button
}
pub(crate) fn position(&self) -> Vec2 {
self.position
}
}
#[derive(Event)]
pub(crate) struct MouseDoubleClickedEvent {
button: MouseButton,
}
impl MouseDoubleClickedEvent {
fn new(button: MouseButton) -> Self {
Self { button }
}
pub(crate) fn button(&self) -> MouseButton {
self.button
}
}
#[derive(Event)]
pub(crate) struct MouseDraggedEvent {
button: MouseButton,
rect: Option<ScreenRect>,
update_type: DragUpdateType,
}
impl MouseDraggedEvent {
fn new(button: MouseButton, rect: Option<ScreenRect>, update_type: DragUpdateType) -> Self {
Self {
button,
rect,
update_type,
}
}
pub(crate) fn button(&self) -> MouseButton {
self.button
}
pub(crate) fn rect(&self) -> Option<ScreenRect> {
self.rect
}
pub(crate) fn update_type(&self) -> DragUpdateType {
self.update_type
}
}
#[derive(Clone, Copy)]
pub(crate) enum DragUpdateType {
Moved,
Released,
}
#[derive(Default, Resource)]
pub(crate) struct MousePosition(Option<Vec2>);
impl MousePosition {
pub(crate) fn ndc(&self) -> Option<Vec2> {
self.0.map(|p| Vec2::new(2. * p.x - 1., 1. - 2. * p.y))
}
fn position(&self) -> Option<Vec2> {
self.0
}
fn set_position(&mut self, position: Option<Vec2>) {
self.0 = position;
}
}
#[derive(Default, Resource)]
struct MouseDragStates(AHashMap<MouseButton, DragState>);
impl MouseDragStates {
fn set(&mut self, button: MouseButton, position: Option<Vec2>) {
self.0.insert(button, DragState::new(position));
}
fn resolve(&mut self, button: MouseButton) -> Option<DragResolution> {
self.0.remove(&button).and_then(DragState::resolve)
}
fn update(&mut self, position: Option<Vec2>) -> AHashMap<MouseButton, Option<ScreenRect>> {
let mut updates = AHashMap::new();
for (&button, drag) in self.0.iter_mut() {
if let Some(update) = drag.update(position) {
updates.insert(button, update);
}
}
updates
}
}
struct DragState {
start: Option<Vec2>,
stop: Option<Vec2>,
active: bool,
}
impl DragState {
fn new(start: Option<Vec2>) -> Self {
Self {
start,
stop: start,
active: false,
}
}
fn resolve(self) -> Option<DragResolution> {
match self.start {
Some(start) => match (self.active, self.stop) {
(true, Some(stop)) => Some(DragResolution::Rect(Some(ScreenRect::from_points(
start, stop,
)))),
(true, None) => Some(DragResolution::Rect(None)),
(false, Some(stop)) => Some(DragResolution::Point(stop)),
(false, None) => None,
},
None => None,
}
}
fn update(&mut self, position: Option<Vec2>) -> Option<Option<ScreenRect>> {
let changed = self.stop != position;
self.stop = position;
if let Some(start) = self.start {
let rect = match self.stop {
Some(stop) => {
self.active |= start.distance(stop) >= DRAGGING_THRESHOLD;
Some(ScreenRect::from_points(start, stop))
}
None => None,
};
if self.active && changed {
return Some(rect);
}
}
None
}
}
enum DragResolution {
Point(Vec2),
Rect(Option<ScreenRect>),
}
fn setup(mut commands: Commands) {
commands.init_resource::<MousePosition>();
commands.init_resource::<MouseDragStates>();
}
fn cleanup(mut commands: Commands) {
commands.remove_resource::<MousePosition>();
commands.remove_resource::<MouseDragStates>();
}
fn update_position(
window_query: Query<&Window, With<PrimaryWindow>>,
hud: HudNodes,
mut mouse: ResMut<MousePosition>,
) {
let window = window_query.single();
let position = window
.cursor_position()
.filter(|&position| !hud.contains_point(position))
.map(|position| position / Vec2::new(window.width(), window.height()))
.map(|normalised_position| normalised_position.clamp(Vec2::ZERO, Vec2::ONE));
if mouse.position() != position {
mouse.set_position(position)
}
}
fn update_drags(
mouse_position: Res<MousePosition>,
mut mouse_state: ResMut<MouseDragStates>,
mut drags: EventWriter<MouseDraggedEvent>,
) {
let resolutions = mouse_state.update(mouse_position.ndc());
for (&button, &rect) in resolutions.iter() {
drags.send(MouseDraggedEvent::new(button, rect, DragUpdateType::Moved));
}
}
fn update_buttons(
mouse_position: Res<MousePosition>,
mut mouse_state: ResMut<MouseDragStates>,
mut input_events: EventReader<MouseButtonInput>,
mut clicks: EventWriter<MouseClickedEvent>,
mut drags: EventWriter<MouseDraggedEvent>,
) {
for event in input_events.read() {
match event.state {
ButtonState::Released => {
if let Some(drag_resolution) = mouse_state.resolve(event.button) {
match drag_resolution {
DragResolution::Point(position) => {
clicks.send(MouseClickedEvent::new(event.button, position));
}
DragResolution::Rect(rect) => {
drags.send(MouseDraggedEvent::new(
event.button,
rect,
DragUpdateType::Released,
));
}
}
}
}
ButtonState::Pressed => {
mouse_state.set(event.button, mouse_position.ndc());
}
}
}
}
fn check_double_click(
mut clicks: EventReader<MouseClickedEvent>,
mut double_clicks: EventWriter<MouseDoubleClickedEvent>,
mut last_click_position: Local<Option<Vec2>>,
mut last_click_time: Local<f64>,
time: Res<Time>,
) {
for mouse_clicked in clicks.read() {
let current_time = time.elapsed_seconds_f64();
if last_click_position.map_or(true, |p| {
p.distance(mouse_clicked.position()) < DRAGGING_THRESHOLD
}) {
if (current_time - *last_click_time) < DOUBLE_CLICK_TIME {
double_clicks.send(MouseDoubleClickedEvent::new(mouse_clicked.button()));
}
}
*last_click_time = time.elapsed_seconds_f64();
*last_click_position = Some(mouse_clicked.position());
}
}