use crate::*;
use bevy::{input::{gamepad::GamepadButtonChangedEvent, mouse::MouseButtonInput, ButtonState}, picking::{pointer::{Location, PointerAction, PointerId, PointerInput, PointerLocation, PressDirection}, PickSet}, render::camera::{NormalizedRenderTarget, RenderTarget}, utils::HashMap, window::{PrimaryWindow, SystemCursorIcon, WindowRef}, winit::cursor::CursorIcon};
#[derive(Resource, Reflect, Clone, PartialEq, Debug, Default)]
pub struct CursorIconQueue {
pointers: HashMap<PointerId, CursorQueueData>
}
impl CursorIconQueue {
pub fn request_cursor(&mut self, pointer: PointerId, window: Entity, requestee: Entity, request: SystemCursorIcon, priority: usize) {
if let Some(data) = self.pointers.get_mut(&pointer) {
data.window = window;
data.queue.insert(requestee, (request, priority));
} else {
let mut queue = HashMap::new();
queue.insert(requestee, (request, priority));
self.pointers.insert(pointer, CursorQueueData { window, queue });
}
}
pub fn cancel_cursor(&mut self, pointer: PointerId, requestee: &Entity) {
if let Some(data) = self.pointers.get_mut(&pointer) {
data.queue.remove(requestee);
}
}
}
#[derive(Reflect, Clone, PartialEq, Debug)]
struct CursorQueueData {
window: Entity,
queue: HashMap<Entity, (SystemCursorIcon, usize)>
}
fn system_cursor_icon_queue_apply(
queue: Res<CursorIconQueue>,
mut windows: Query<(&Window, Option<&mut CursorIcon>)>,
mut commands: Commands,
) {
if !queue.is_changed() { return; }
for (_, data) in &queue.pointers {
if let Ok((_window, window_cursor_option)) = windows.get_mut(data.window) {
let mut top_priority = 0;
let mut top_request = SystemCursorIcon::Default;
for (_, (icon, priority)) in &data.queue {
if *priority > top_priority {
top_priority = *priority;
top_request = *icon;
}
}
if let Some(mut window_cursor) = window_cursor_option {
#[allow(clippy::single_match)]
match window_cursor.as_mut() {
CursorIcon::System(ref mut previous) => {
if *previous != top_request {
*previous = top_request;
}
},
_ => {},
}
} else {
commands.entity(data.window).insert(CursorIcon::System(top_request));
}
}
}
}
fn system_cursor_icon_queue_purge(
mut queue: ResMut<CursorIconQueue>,
mut windows: Query<&Window>,
entities: Query<Entity>,
) {
let mut to_remove = Vec::new();
for (pointer, data) in &mut queue.pointers {
if windows.get_mut(data.window).is_err() {
to_remove.push(*pointer);
}
let mut to_remove = Vec::new();
for (entity, _) in &data.queue {
if entities.get(*entity).is_err() {
to_remove.push(*entity);
}
}
for entity in to_remove {
data.queue.remove(&entity);
}
}
for pointer in to_remove {
queue.pointers.remove(&pointer);
}
}
#[derive(Component, Reflect, Clone, PartialEq, Debug)]
pub struct OnHoverSetCursor {
pub cursor: SystemCursorIcon,
}
impl OnHoverSetCursor {
pub fn new(cursor: SystemCursorIcon) -> Self {
OnHoverSetCursor {
cursor,
}
}
}
fn observer_cursor_request_cursor_icon(mut trigger: Trigger<Pointer<Over>>, mut pointers: Query<(&PointerId, &PointerLocation)>, query: Query<&OnHoverSetCursor>, mut queue: ResMut<CursorIconQueue>) {
let id = trigger.pointer_id;
for (pointer, location) in pointers.iter_mut().filter(|(p_id, _)| id == **p_id) {
if let Some(location) = &location.location {
if let NormalizedRenderTarget::Window(window) = location.target {
if let Ok(requestee) = query.get(trigger.target) {
trigger.propagate(false);
queue.request_cursor(*pointer, window.entity(), trigger.target, requestee.cursor, 1);
}
}
}
}
}
fn observer_cursor_cancel_cursor_icon(mut trigger: Trigger<Pointer<Out>>, mut pointers: Query<(&PointerId, &PointerLocation)>, query: Query<&OnHoverSetCursor>, mut queue: ResMut<CursorIconQueue>) {
let id = trigger.pointer_id;
for (pointer, location) in pointers.iter_mut().filter(|(p_id, _)| id == **p_id) {
if let Some(location) = &location.location {
if matches!(location.target, NormalizedRenderTarget::Window(_)) {
if query.get(trigger.target).is_ok() {
trigger.propagate(false);
queue.cancel_cursor(*pointer, &trigger.target);
}
}
}
}
}
#[derive(Component, Reflect, Clone, PartialEq, Debug, Default)]
#[require(PointerId, PickingBehavior(|| PickingBehavior::IGNORE))]
pub struct SoftwareCursor {
cursor_request: SystemCursorIcon,
cursor_request_priority: f32,
cursor_atlas_map: HashMap<SystemCursorIcon, (usize, Vec2)>,
pub location: Vec2,
}
impl SoftwareCursor {
pub fn new() -> SoftwareCursor {
SoftwareCursor {
cursor_request: SystemCursorIcon::Default,
cursor_request_priority: 0.0,
cursor_atlas_map: HashMap::new(),
location: Vec2::ZERO,
}
}
pub fn request_cursor(&mut self, request: SystemCursorIcon, priority: f32) {
if priority > self.cursor_request_priority {
self.cursor_request = request;
self.cursor_request_priority = priority;
}
}
pub fn set_index(mut self, icon: SystemCursorIcon, index: usize, offset: impl Into<Vec2>) -> Self {
self.cursor_atlas_map.insert(icon, (index, offset.into()));
self
}
}
#[derive(Component, Reflect, Clone, PartialEq, Debug)]
pub struct GamepadCursor {
pub mode: GamepadCursorMode,
pub speed: f32,
}
impl GamepadCursor {
pub fn new() -> Self {
Self::default()
}
}
impl Default for GamepadCursor {
fn default() -> Self {
Self { mode: Default::default(), speed: 1.0 }
}
}
#[derive(Debug, Clone, Default, PartialEq, Reflect)]
pub enum GamepadCursorMode {
#[default]
Free,
Snap,
}
#[derive(Component, Reflect, Clone, PartialEq, Debug)]
pub struct GamepadAttachedCursor(pub Entity);
fn system_cursor_hide_native(
mut windows: Query<&mut Window>,
query: Query<(&PointerLocation, Has<GamepadCursor>), With<SoftwareCursor>>
) {
for (pointer_location, is_gamepad) in &query {
if let Some(location) = &pointer_location.location {
if let NormalizedRenderTarget::Window(window) = location.target {
if let Ok(mut window) = windows.get_mut(window.entity()) {
window.cursor_options.visible = is_gamepad;
}
}
}
}
}
fn system_cursor_software_change_icon(
windows: Query<&CursorIcon, With<Window>>,
mut query: Query<(&PointerLocation, &SoftwareCursor, &mut Sprite), With<SoftwareCursor>>
) {
for (pointer_location, software_cursor, mut sprite) in &mut query {
if let Some(location) = &pointer_location.location {
if let NormalizedRenderTarget::Window(window) = location.target {
if let Ok(cursor_icon) = windows.get(window.entity()) {
if let Some(atlas) = &mut sprite.texture_atlas {
#[allow(clippy::single_match)]
match *cursor_icon {
CursorIcon::System(icon) => {
atlas.index = software_cursor.cursor_atlas_map.get(&icon).unwrap_or(&(0, Vec2::ZERO)).0;
},
_ => {},
}
}
}
}
}
}
}
fn system_cursor_gamepad_assign(
mut commands: Commands,
cursors: Query<(Entity, &SoftwareCursor, &GamepadCursor), Without<GamepadAttachedCursor>>,
gamepads: Query<(Entity, &Gamepad), Without<GamepadAttachedCursor>>,
) {
let mut gamepads = gamepads.iter();
if let Some((cursor, _, _)) = cursors.iter().next() {
if let Some((gamepad, _)) = gamepads.next() {
commands.entity(cursor).insert(GamepadAttachedCursor(gamepad));
commands.entity(gamepad).insert(GamepadAttachedCursor(cursor));
info!("Gamepad {gamepad} bound to cursor {cursor}");
}
}
}
fn system_cursor_gamepad_move(
time: Res<Time>,
gamepads: Query<&Gamepad, With<GamepadAttachedCursor>>,
mut cursors: Query<(&mut SoftwareCursor, &GamepadCursor, &GamepadAttachedCursor), Without<Gamepad>>,
) {
for (mut cursor, gamepad_settings, attached_gamepad) in &mut cursors {
if let Ok(gamepad) = gamepads.get(attached_gamepad.0) {
let mut input = Vec2::new(
gamepad.get(GamepadAxis::LeftStickX).unwrap_or(0.0),
gamepad.get(GamepadAxis::LeftStickY).unwrap_or(0.0),
);
if input.length_squared() < 0.1 { input *= 0.0; }
let x = input.x * gamepad_settings.speed * time.delta_secs() * 500.0;
let y = input.y * gamepad_settings.speed * time.delta_secs() * 500.0;
if x != 0.0 { cursor.location.x += x; }
if y != 0.0 { cursor.location.y += y; }
}
}
}
fn system_cursor_mouse_move(
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<&OrthographicProjection>,
mut query: Query<(&mut SoftwareCursor, Option<&Parent>), Without<GamepadCursor>>
) {
if let Ok(window) = windows.get_single() {
for (mut cursor, parent_option) in &mut query {
if let Some(position) = window.cursor_position() {
let scale = if let Some(parent) = parent_option {
if let Ok(projection) = cameras.get(**parent) { projection.scale } else { 1.0 }
} else { 1.0 };
let x = (position.x - window.width()*0.5) * scale;
let y = -((position.y - window.height()*0.5) * scale);
if x != cursor.location.x { cursor.location.x = (position.x - window.width()*0.5) * scale; }
if y != cursor.location.y { cursor.location.y = -((position.y - window.height()*0.5) * scale); }
}
}
}
}
fn system_cursor_update_tranform(
mut query: Query<(&SoftwareCursor, &mut Transform)>
) {
for (cursor, mut transform) in &mut query {
let sprite_offset = cursor.cursor_atlas_map.get(&cursor.cursor_request).unwrap_or(&(0, Vec2::ZERO)).1;
transform.translation.x = cursor.location.x - sprite_offset.x * transform.scale.x;
transform.translation.y = cursor.location.y + sprite_offset.y * transform.scale.y;
}
}
fn system_cursor_move_pointer(
windows: Query<(Entity, &Window), With<PrimaryWindow>>,
mut query: Query<(&mut PointerLocation, &SoftwareCursor)>,
) {
if let Ok((win_entity, window)) = windows.get_single() {
for (mut pointer, cursor) in query.iter_mut() {
pointer.location = Some(Location {
target: RenderTarget::Window(WindowRef::Primary).normalize(Some(win_entity)).unwrap(),
position: Vec2 {
x: cursor.location.x + window.width()/2.0,
y: -cursor.location.y + window.height()/2.0,
}.round(),
});
}
}
}
fn system_cursor_send_move_events(
mut cursor_last: Local<HashMap<PointerId, Vec2>>,
pointers: Query<(&PointerId, &PointerLocation), With<SoftwareCursor>>,
mut pointer_output: EventWriter<PointerInput>,
) {
for (pointer, location) in &pointers {
if let Some(location) = &location.location {
let last = cursor_last.get(pointer).unwrap_or(&Vec2::ZERO);
if *last == location.position { continue; }
pointer_output.send(PointerInput::new(
*pointer,
Location {
target: location.target.clone(),
position: location.position,
},
PointerAction::Moved {
delta: location.position - *last,
},
));
cursor_last.insert(*pointer, location.position);
}
}
}
fn system_cursor_mouse_send_pick_events(
pointers: Query<&PointerLocation, (With<SoftwareCursor>, Without<GamepadCursor>)>,
mut mouse_inputs: EventReader<MouseButtonInput>,
mut pointer_output: EventWriter<PointerInput>,
) {
for location in &pointers {
if let Some(location) = &location.location {
for input in mouse_inputs.read() {
let button = match input.button {
MouseButton::Left => PointerButton::Primary,
MouseButton::Right => PointerButton::Secondary,
MouseButton::Middle => PointerButton::Middle,
MouseButton::Other(_) | MouseButton::Back | MouseButton::Forward => continue,
};
let direction = match input.state {
ButtonState::Pressed => PressDirection::Down,
ButtonState::Released => PressDirection::Up,
};
pointer_output.send(PointerInput::new(
PointerId::Mouse,
Location {
target: location.target.clone(),
position: location.position,
},
PointerAction::Pressed { direction, button },
));
}
}
}
}
fn system_cursor_gamepad_send_pick_events(
pointers: Query<&PointerLocation, (With<SoftwareCursor>, Without<GamepadCursor>)>,
mut mouse_inputs: EventReader<GamepadButtonChangedEvent>,
mut pointer_output: EventWriter<PointerInput>,
) {
for location in &pointers {
if let Some(location) = &location.location {
for input in mouse_inputs.read() {
let button = match input.button {
GamepadButton::South => PointerButton::Primary,
GamepadButton::East => PointerButton::Secondary,
GamepadButton::West => PointerButton::Middle,
_ => continue,
};
let direction = match input.state {
ButtonState::Pressed => PressDirection::Down,
ButtonState::Released => PressDirection::Up,
};
pointer_output.send(PointerInput::new(
PointerId::Mouse,
Location {
target: location.target.clone(),
position: location.position,
},
PointerAction::Pressed { direction, button },
));
}
}
}
}
pub struct CursorPlugin;
impl Plugin for CursorPlugin {
fn build(&self, app: &mut App) {
app
.insert_resource(CursorIconQueue::default())
.add_systems(PostUpdate, (
system_cursor_icon_queue_purge,
system_cursor_icon_queue_apply,
))
.add_observer(observer_cursor_request_cursor_icon)
.add_observer(observer_cursor_cancel_cursor_icon)
.add_systems(First, (
system_cursor_send_move_events,
system_cursor_mouse_send_pick_events,
system_cursor_gamepad_send_pick_events,
apply_deferred
).chain().in_set(PickSet::Input))
.add_systems(PreUpdate, (
system_cursor_gamepad_move,
system_cursor_mouse_move,
system_cursor_update_tranform,
system_cursor_move_pointer,
).chain())
.add_systems(Update, (
system_cursor_hide_native,
system_cursor_software_change_icon,
system_cursor_gamepad_assign,
));
}
}