diff --git a/src/input.rs b/src/input.rs
index 2dccc4613ef3dc878732a95f9749b46714bd0fcf..aa3d228aa05f43d4ef44638d755dc36f7c481897 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -12,6 +12,7 @@ use crate::{
     event_dispatcher::EventDispatcher,
     input_event::InputEvent,
 };
+use crate::unproject::UnprojectManager;
 
 pub(crate) fn process_events(world: &mut World) {
     let mut input_events = Vec::new();
@@ -28,6 +29,7 @@ pub(crate) fn process_events(world: &mut World) {
             ResMut<CustomEventReader<MouseWheel>>,
             ResMut<CustomEventReader<ReceivedCharacter>>,
             ResMut<CustomEventReader<KeyboardInput>>,
+            UnprojectManager,
         ),
         _,
         _,
@@ -43,6 +45,7 @@ pub(crate) fn process_events(world: &mut World) {
             mut custom_event_mouse_wheel,
             mut custom_event_char_input,
             mut custom_event_keyboard,
+            projector,
         )| {
             if let Some(event) = custom_event_reader_cursor
                 .0
@@ -50,7 +53,9 @@ pub(crate) fn process_events(world: &mut World) {
                 .last()
             {
                 // Currently, we can only handle a single MouseMoved event at a time so everything but the last needs to be skipped
-                input_events.push(InputEvent::MouseMoved(event.position.into()));
+                if let Ok(pos) = projector.unproject(event.position) {
+                    input_events.push(InputEvent::MouseMoved(pos.into()));
+                }
             }
 
             for event in custom_event_mouse_button.0.read(&mouse_button_input_events) {
diff --git a/src/lib.rs b/src/lib.rs
index 0929cb70b4eb0d1a3d1b441c2d89cb6918a06227..12cbb1f791c50d5d92986126960c828d46544bd9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,6 +29,8 @@ mod widget_state;
 pub mod widgets;
 mod window_size;
 
+pub(crate) mod unproject;
+
 use context::KayakRootContext;
 pub use window_size::WindowSize;
 
diff --git a/src/unproject.rs b/src/unproject.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dff2687f6422436bd1a9846f9ecc3c1056fccaf5
--- /dev/null
+++ b/src/unproject.rs
@@ -0,0 +1,47 @@
+use bevy::ecs::system::SystemParam;
+use bevy::math::Vec2;
+use bevy::prelude::{OrthographicProjection, Query, Vec4Swizzles, Window, With};
+use bevy::utils::thiserror::Error;
+use bevy::window::PrimaryWindow;
+use crate::CameraUIKayak;
+
+pub fn unproject_from_physical(
+    point: Vec2,
+    window: &Window,
+    projection: &OrthographicProjection,
+) -> Vec2 {
+    let projection_area = projection.area;
+    let projection_width = projection_area.width();
+    let projection_height = projection_area.height();
+
+    let window_width = window.width();
+    let window_height = window.height();
+
+    let width_ratio = projection_width / window_width;
+    let height_ratio = projection_height / window_height;
+
+    point * Vec2::new(width_ratio, height_ratio)
+}
+
+#[derive(SystemParam)]
+pub struct UnprojectManager<'w, 's> {
+    windows: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
+    camera: Query<'w, 's, &'static OrthographicProjection, With<CameraUIKayak>>,
+}
+
+#[derive(Debug, Error)]
+#[error("Invalid ECS state to perform unprojection")]
+pub struct InvalidUnprojection;
+
+impl<'w, 's> UnprojectManager<'w, 's> {
+    pub fn unproject(&self, position: Vec2) -> Result<Vec2, InvalidUnprojection> {
+        let window = self.windows.get_single().map_err(|_| InvalidUnprojection)?;
+        let camera = self.camera.get_single().map_err(|_| InvalidUnprojection)?;
+
+        Ok(unproject_from_physical(
+            position,
+            window,
+            camera,
+        ))
+    }
+}