use std::{fmt::Display, ops::Deref};
use bevy::{
ecs::system::EntityCommand,
prelude::*,
render::camera::RenderTarget,
window::{PrimaryWindow, WindowRef},
};
pub struct MousePosPlugin;
impl Plugin for MousePosPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(MousePos(default()))
.insert_resource(MousePosWorld(default()))
.add_systems(
Update,
(update_pos, update_pos_ortho, update_resources).chain(),
);
}
}
#[derive(Debug, Resource, Clone, Copy, PartialEq, Component)]
pub struct MousePos(Vec2);
impl Deref for MousePos {
type Target = Vec2;
fn deref(&self) -> &Vec2 {
&self.0
}
}
impl Display for MousePos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
pub struct InitMouseTracking;
impl EntityCommand for InitMouseTracking {
fn apply(self, entity: Entity, world: &mut World) {
#[track_caller]
#[cold]
fn no_camera(id: impl std::fmt::Debug) -> ! {
panic!("tried to call the command `InitMouseTracking` on non-camera entity '{id:?}'")
}
#[track_caller]
#[cold]
fn image_camera(id: impl std::fmt::Debug) -> ! {
panic!(
"tried to call the command `InitMouseTracking` on a camera ({id:?}) that renders to an image",
)
}
#[track_caller]
#[cold]
fn no_window(id: impl std::fmt::Debug) -> ! {
panic!("could not find the window '{id:?}'")
}
let primary_window = world
.query_filtered::<Entity, With<PrimaryWindow>>()
.get_single(world)
.ok();
let camera = world
.entity(entity)
.get::<Camera>()
.unwrap_or_else(|| no_camera(entity));
let RenderTarget::Window(window_id) = camera.target else {
image_camera(entity);
};
let window_id = window_id
.normalize(primary_window)
.expect("`PrimaryWindow` does not exist")
.entity();
let window = world
.query::<&Window>()
.get(world, window_id)
.unwrap_or_else(|_| no_window(window_id));
let mouse_pos = window.cursor_position().unwrap_or_default();
world.entity_mut(entity).insert(MousePos(mouse_pos));
}
}
fn update_pos(
mut movement: EventReader<CursorMoved>,
mut cameras: Query<(&Camera, &mut MousePos)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
let primary_window = primary_window.get_single().ok();
for &CursorMoved {
window, position, ..
} in movement.read()
{
let target = RenderTarget::Window(WindowRef::Entity(window)).normalize(None);
for (_, mut pos) in cameras
.iter_mut()
.filter(|(c, ..)| c.target.normalize(primary_window) == target)
{
pos.0 = position;
}
}
}
#[derive(Debug, Resource, Clone, Copy, PartialEq, Component)]
pub struct MousePosWorld(Vec3);
impl Display for MousePosWorld {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Deref for MousePosWorld {
type Target = Vec3;
fn deref(&self) -> &Vec3 {
&self.0
}
}
pub struct InitWorldTracking;
impl EntityCommand for InitWorldTracking {
fn apply(self, entity: Entity, world: &mut World) {
fn no_transform(id: impl std::fmt::Debug) -> ! {
panic!("tried to call the command `InitWorldTracking` on a camera ({id:?}) with no `GlobalTransform`")
}
fn no_proj(id: impl std::fmt::Debug) -> ! {
panic!("tried to call the command `InitWorldTracking` on a camera ({id:?}) with no `OrthographicProjection`")
}
InitMouseTracking.apply(entity, world);
let mut entity_mut = world.entity_mut(entity);
let screen_pos = entity_mut.get::<MousePos>().unwrap();
let &transform = entity_mut
.get::<GlobalTransform>()
.unwrap_or_else(|| no_transform(entity));
let proj = entity_mut
.get::<OrthographicProjection>()
.unwrap_or_else(|| no_proj(entity));
let world_pos = compute_world_pos_ortho(screen_pos.0, transform, proj);
entity_mut.insert(MousePosWorld(world_pos));
}
}
fn update_pos_ortho(
mut tracking: Query<
(Entity, &mut MousePosWorld, &MousePos),
Or<(Changed<MousePos>, Changed<GlobalTransform>)>,
>,
cameras: Query<(&GlobalTransform, &OrthographicProjection)>,
) {
for (camera, mut world, screen) in tracking.iter_mut() {
let (&camera, proj) = cameras
.get(camera)
.expect("only orthographic cameras are supported");
world.0 = compute_world_pos_ortho(screen.0, camera, proj);
}
}
fn compute_world_pos_ortho(
screen_pos: Vec2,
transform: GlobalTransform,
proj: &OrthographicProjection,
) -> Vec3 {
let offset = Vec2::new(proj.area.min.x, proj.area.max.y) / proj.scale;
transform * ((screen_pos * Vec2::new(1.0, -1.0) + offset) * proj.scale).extend(0.0)
}
#[derive(Component)]
pub struct MainCamera;
fn update_resources(
mut last_main: Local<Option<Entity>>,
added_main: Query<Entity, Added<MainCamera>>,
mut removed_main: RemovedComponents<MainCamera>,
mut screen_res: ResMut<MousePos>,
mut world_res: ResMut<MousePosWorld>,
screen: Query<&MousePos>,
world: Query<&MousePosWorld>,
) {
let mut with_marker: Vec<_> = Option::into_iter(*last_main).chain(&added_main).collect();
for rem in removed_main.read() {
if let Some(idx) = with_marker.iter().position(|&x| x == rem) {
with_marker.remove(idx);
}
}
match *with_marker {
[main] => {
*last_main = Some(main);
let screen = screen.get(main).map_or_else(|_| default(), |s| s.0);
if screen_res.0 != screen {
screen_res.0 = screen;
}
let world = world.get(main).map_or_else(|_| default(), |w| w.0);
if world_res.0 != world {
world_res.0 = world;
}
}
[] => {
if last_main.is_some() {
*last_main = None;
*screen_res = MousePos(default());
*world_res = MousePosWorld(default());
}
}
[..] => {
panic!("`bevy_mouse_tracking_plugin`: there cannot be more than one entity with a `MainCamera` component");
}
}
}