use std::{fmt::Display, ops::Deref};
use bevy::{
ecs::{
system::{Command, EntityCommands},
world::EntityMut,
},
prelude::*,
render::camera::RenderTarget,
};
pub struct MousePosPlugin;
impl Plugin for MousePosPlugin {
fn build(&self, app: &mut App) {
app.add_system_to_stage(
CoreStage::First,
update_pos.after(Events::<CursorMoved>::update_system),
);
app.add_system_to_stage(CoreStage::First, update_pos_ortho.after(update_pos));
app.insert_resource(MousePos(default()));
app.insert_resource(MousePosWorld(default()));
app.add_system_to_stage(CoreStage::First, update_resources.after(update_pos_ortho));
}
}
pub trait InsertExt {
fn add_mouse_tracking(&mut self) -> &mut Self;
fn add_world_tracking(&mut self) -> &mut Self;
}
impl<'w> InsertExt for EntityMut<'w> {
fn add_mouse_tracking(&mut self) -> &mut Self {
#[track_caller]
#[cold]
fn no_camera(id: impl std::fmt::Debug) -> ! {
panic!("tried to call the command `AddMouseTracking` on non-camera entity '{id:?}'")
}
#[track_caller]
#[cold]
fn image_camera(id: impl std::fmt::Debug) -> ! {
panic!(
"tried to call the command `AddMouseTracking` 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 camera = self.get::<Camera>().unwrap_or_else(|| no_camera(self.id()));
let window = match camera.target {
RenderTarget::Window(id) => id,
RenderTarget::Image(_) => image_camera(self.id()),
};
let window = self
.world()
.resource::<Windows>()
.get(window)
.unwrap_or_else(|| no_window(window));
let mouse_pos = window.cursor_position().unwrap_or_default();
self.insert(MousePos(mouse_pos))
}
fn add_world_tracking(&mut self) -> &mut Self {
fn no_transform(id: impl std::fmt::Debug) -> ! {
panic!("tried to call the command `AddWorldTracking` on a camera ({id:?}) with no `GlobalTransform`")
}
fn no_proj(id: impl std::fmt::Debug) -> ! {
panic!("tried to call the command `AddWorldTracking` on a camera ({id:?}) with no `OrthographicProjection`")
}
self.add_mouse_tracking();
let screen_pos = self.get::<MousePos>().unwrap();
let &transform = self
.get::<GlobalTransform>()
.unwrap_or_else(|| no_transform(self.id()));
let proj = self
.get::<OrthographicProjection>()
.unwrap_or_else(|| no_proj(self.id()));
let world_pos = compute_world_pos_ortho(screen_pos.0, transform, proj);
self.insert(MousePosWorld(world_pos))
}
}
impl<'w, 's, 'a> InsertExt for EntityCommands<'w, 's, 'a> {
fn add_mouse_tracking(&mut self) -> &mut Self {
let cmd = AddMouseTracking(self.id());
self.commands().add(cmd);
self
}
fn add_world_tracking(&mut self) -> &mut Self {
let cmd = AddWorldTracking(self.id());
self.commands().add(cmd);
self
}
}
#[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 AddMouseTracking(Entity);
impl AddMouseTracking {
pub const fn new(id: Entity) -> Self {
Self(id)
}
}
impl Command for AddMouseTracking {
fn write(self, world: &mut World) {
world.entity_mut(self.0).add_mouse_tracking();
}
}
fn update_pos(
mut movement: EventReader<CursorMoved>,
mut cameras: Query<(&Camera, &mut MousePos)>,
) {
for &CursorMoved { id, position } in movement.iter() {
for (_, mut pos) in cameras
.iter_mut()
.filter(|(c, ..)| c.target == RenderTarget::Window(id))
{
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 AddWorldTracking(Entity);
impl AddWorldTracking {
pub fn new(id: Entity) -> Self {
Self(id)
}
}
impl Command for AddWorldTracking {
fn write(self, world: &mut World) {
world.entity_mut(self.0).add_world_tracking();
}
}
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(Vec2::new(screen.0.x, -screen.0.y), camera, proj);
}
}
fn compute_world_pos_ortho(
screen_pos: Vec2,
transform: GlobalTransform,
proj: &OrthographicProjection,
) -> Vec3 {
let offset = Vec2::new(proj.left, proj.top);
transform * (((screen_pos + offset) * proj.scale) * Vec2::new(1.0, -1.0)).extend(0.0)
}
#[derive(Component)]
pub struct MainCamera;
fn update_resources(
mut last_main: Local<Option<Entity>>,
added_main: Query<Entity, Added<MainCamera>>,
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.iter() {
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");
}
}
}