diff --git a/demo/main.cpp b/demo/main.cpp
index 83211bb2b92d142e4d0c646fad77b414952288c7..2c168f7a0430c7b714bc3a67285958911fb4bdd9 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -41,24 +41,8 @@ void discover(std::filesystem::path base) {
 
 }
 
-struct InputState {
-	double currCursor[2];
-	double lastCursor[2];
-	bool buttons[3];
-};
-
-void process_inputs(fggl::gfx::Window& window, InputState& state) {
-	state.lastCursor[0] = state.currCursor[0];
-	state.lastCursor[1] = state.currCursor[1];
-	glfwGetCursorPos(window.handle(), &state.currCursor[0], &state.currCursor[1]);
-
-	state.buttons[0] = glfwGetMouseButton(window.handle(), GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
-	state.buttons[1] = glfwGetMouseButton(window.handle(), GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS;
-	state.buttons[2] = glfwGetMouseButton(window.handle(), GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS;
-}
-
 //TODO proper input system
-void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputState& state, fggl::ecs::entity_t cam) {
+void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
 	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
 	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
 	float moveSpeed = 1.0f;
@@ -77,14 +61,16 @@ void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputState&
 		  motion += (forward * moveSpeed);
 		}
 	}
-	if ( glfwGetKey( window.handle(), GLFW_KEY_A ) == GLFW_PRESS ) {
-		
-	}
+
+	// scroll wheel
+	float delta = (float)input.scrollDeltaY();
+	if ( (glm::length( dir ) < 25.0f && delta < 0.0f) || (glm::length( dir ) > 2.5f && delta > 0.0f) )
+		motion -= (forward * delta);
 
 	camTransform->origin( camTransform->origin() + motion );
 }
 
-void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputState& state, fggl::ecs::entity_t cam) {
+void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
 	// see https://asliceofrendering.com/camera/2019/11/30/ArcballCamera/
 	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
 	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
@@ -97,8 +83,8 @@ void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputState&
 
 	float deltaAngleX = ( 2 * M_PI / window.width() ); 
 	float deltaAngleY = ( M_PI / window.height() );
-	float xAngle = ( state.lastCursor[0] - state.currCursor[0] ) * deltaAngleX;
-	float yAngle = ( state.lastCursor[1] - state.currCursor[1] ) * deltaAngleY;
+	float xAngle = ( input.cursorDeltaX() ) * deltaAngleX;
+	float yAngle = ( input.cursorDeltaY() ) * deltaAngleY;
 
 	auto cosAngle = glm::dot( viewDir, fggl::math::UP );
 	if ( cosAngle * sgn(deltaAngleY) > 0.99f ) {
@@ -122,7 +108,7 @@ int main(int argc, char* argv[]) {
 	fggl::gfx::Context ctx;
 
 	// build our main window
-	fggl::gfx::Window win;
+	fggl::gfx::Window win( fggl::gfx::Input::instance() );
 	win.fullscreen( true );
 
 	// opengl time
@@ -206,21 +192,22 @@ int main(int argc, char* argv[]) {
 		ecs.addComponent<fggl::gfx::MeshToken>(entity, token);
 	}
 
-	InputState inputs;
+	fggl::gfx::Input& input = fggl::gfx::Input::instance();
 
 	float time = 0.0f;
 	float dt = 16.0f;
 	while( !win.closeRequested() ) {
+		input.frame();
+
 		ctx.pollEvents();
 		debug.frameStart();
 
 		// update step
 		time += dt;
 
-		process_inputs(win, inputs);
-		process_camera(win, ecs, inputs, camEnt);
-		if ( inputs.buttons[2] ) {
-			process_arcball(win, ecs, inputs, camEnt);
+		process_camera(win, ecs, input, camEnt);
+		if ( input.mouseDown( fggl::gfx::MOUSE_2 ) ) {
+			process_arcball(win, ecs, input, camEnt);
 		}
 
 /*		float amount = glm::radians( time / 2048.0f * 360.0f );
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index e99b3582e318ea72dbbb2c00c5c9b1eb5a76e399..3311372b647c62197b5d553a3043f6b992ab9050 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -12,7 +12,7 @@ FetchContent_Declare(
 FetchContent_MakeAvailable( glfw3 )
 endif ()
 
-add_library(fggl fggl.cpp ecs/ecs.cpp gfx/window.cpp gfx/renderer.cpp gfx/shader.cpp gfx/ogl.cpp data/model.cpp data/procedural.cpp debug/debug.cpp)
+add_library(fggl fggl.cpp ecs/ecs.cpp gfx/window.cpp gfx/renderer.cpp gfx/input.cpp gfx/shader.cpp gfx/ogl.cpp data/model.cpp data/procedural.cpp debug/debug.cpp)
 target_include_directories(fggl PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../)
 
 # Graphics backend
diff --git a/fggl/gfx/input.cpp b/fggl/gfx/input.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..23dc9b997024e160c872a1c220dd56d68fbb986a
--- /dev/null
+++ b/fggl/gfx/input.cpp
@@ -0,0 +1,97 @@
+#include <fggl/gfx/input.hpp>
+#include <cassert>
+
+#include <bitset>
+#include <iostream>
+
+using fggl::gfx::Input;
+
+Input::Input() : m_mouse_curr(), m_mouse_last(), m_joydata(), m_joysticks() {
+	clear();
+}
+
+void Input::clear() {
+	m_mouse_curr.cursor[0] = 0.0;
+	m_mouse_curr.cursor[1] = 0.0;
+	m_mouse_curr.scroll[0] = 0.0;
+	m_mouse_curr.scroll[1] = 0.0;
+	m_mouse_curr.buttons = 0x00;
+	m_mouse_last = m_mouse_curr;
+}
+
+void Input::frame() {
+	m_mouse_last = m_mouse_curr;
+	m_mouse_curr.scroll[0] = 0.0;
+	m_mouse_curr.scroll[1] = 0.0;
+}
+
+void Input::mousePos(double x, double y) {
+	assert( m_mouse_curr.cursor != nullptr );
+	m_mouse_curr.cursor[0] = x;
+	m_mouse_curr.cursor[1] = y;
+}
+
+double Input::cursorDeltaX() const {
+	return m_mouse_last.cursor[0] - m_mouse_curr.cursor[0];
+}
+
+double Input::cursorDeltaY() const {
+	return m_mouse_last.cursor[1] - m_mouse_curr.cursor[1];
+}
+
+const double* Input::mousePos() const {
+	return m_mouse_curr.scroll.data();
+}
+
+void Input::mouseScroll(double deltaX, double deltaY) {
+	m_mouse_curr.scroll[0] = deltaX;
+	m_mouse_curr.scroll[1] = deltaY;
+}
+
+const double* Input::mouseScroll() const {
+	return m_mouse_curr.scroll.data();
+}
+
+double Input::scrollDeltaX() const {
+	return m_mouse_curr.scroll[0];
+}
+
+double Input::scrollDeltaY() const {
+	return m_mouse_curr.scroll[1];
+}
+
+void Input::mouseBtn(const MouseButton btn, bool state) {
+	if ( state ) {
+		m_mouse_curr.buttons |= btn;
+	} else {
+		m_mouse_curr.buttons &= ~btn;
+	}
+}
+
+bool Input::mouseDown(const MouseButton btn) const {
+	return m_mouse_curr.buttons & btn;
+}
+
+bool Input::mousePressed(const MouseButton btn) const {
+	return (m_mouse_curr.buttons & btn) && !(m_mouse_last.buttons & btn);
+}
+
+bool Input::mouseReleased(const MouseButton btn) const {
+	return !(m_mouse_curr.buttons & btn) && (m_mouse_last.buttons & btn);
+}
+
+void Input::joystickConnect(int id, Joystick &data){
+	std::cout << "JOYSTICK TIME: " << data.name << std::endl;
+	m_joysticks[id] = true;
+	m_joydata[id] = data;
+}
+
+void Input::joystickDisconnect( int id ){
+	// reset to empty joystick
+	m_joysticks[id] = false;
+	m_joydata[id] = Joystick();
+}
+
+bool Input::joystick(int id) const {
+	return m_joysticks[id];
+}
diff --git a/fggl/gfx/input.hpp b/fggl/gfx/input.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..492284848eabce98e016648a6bab6e46fd2481fc
--- /dev/null
+++ b/fggl/gfx/input.hpp
@@ -0,0 +1,99 @@
+#ifndef FGGL_GFX_INPUT_H
+#define FGGL_GFX_INPUT_H
+
+#include <array>
+#include <bitset>
+
+namespace fggl::gfx {
+
+	enum MouseButton {
+		MOUSE_1 = 0b00000001,
+		MOUSE_2 = 0b00000010,
+		MOUSE_3 = 0b00000100,
+		MOUSE_4 = 0b00001000,
+		MOUSE_5 = 0b00010000,
+		MOUSE_6 = 0b00100000,
+		MOUSE_7 = 0b01000000,
+		MOUSE_8 = 0b10000000,
+	};
+
+	struct MouseInputState {
+		std::array<double, 2> cursor;
+		std::array<double, 2> scroll;
+		char buttons;
+	};
+
+	struct Joystick {
+		const char* name = nullptr;
+		const float* axes = nullptr;
+		const unsigned char* buttons = nullptr;
+		const unsigned char* hats = nullptr;
+		int hatCount = 0;
+		int axisCount = 0;
+		int buttonCount = 0;
+	};
+
+	class Input {
+		public:
+			// this is a neccerry evil due to glfw's design :'(
+			static Input& instance() {
+				static Input *instance = new Input();
+				return *instance;
+			}
+
+			void clear();
+			void frame();
+
+			// state motification
+			void mousePos(double x, double y);
+			void mouseScroll(double deltaX, double deltaY);
+			void mouseBtn(const MouseButton btn, bool state);
+
+			// mouse position
+			const double* mousePos() const;
+			double cursorDeltaX() const;
+			double cursorDeltaY() const;
+
+			// mouse scroll
+			const double* mouseScroll() const;
+			double scrollDeltaX() const;
+			double scrollDeltaY() const;
+
+			/**
+			 * is the mouse button down this frame?
+			 *
+			 * True is the mouse button is down
+			 */
+			bool mouseDown(const MouseButton btn) const;
+
+			/**
+			 * Was the mouse button pressed this frame?
+			 *
+			 * True if the mouse button is down, but was up last frame
+			 */
+			bool mousePressed(const MouseButton btn) const;
+
+			/**
+			 * Was the mouse button released this frame?
+			 *
+			 * True if the mouse button is up, but was down last frame
+			 */
+			bool mouseReleased(const MouseButton btn) const;
+
+			// joysticks
+			void joystickConnect(int id, Joystick& data);
+			void joystickDisconnect(int id);
+
+			bool joystick(int id) const;
+
+		private:
+			Input();
+			MouseInputState m_mouse_curr;
+			MouseInputState m_mouse_last;
+			std::bitset<16> m_joysticks;
+			std::array<Joystick, 16> m_joydata;
+	};
+
+};
+
+#endif
diff --git a/fggl/gfx/window.cpp b/fggl/gfx/window.cpp
index f160f039769c087c06dbdb5f71c7437372197280..9cde13b79954041e69d0b6b43abbca9ff2912a46 100644
--- a/fggl/gfx/window.cpp
+++ b/fggl/gfx/window.cpp
@@ -21,7 +21,41 @@ static void framebuffer_resize(GLFWwindow* window, int width, int height) {
 	fgglWindow->framesize( width, height );
 }
 
+static void fggl_input_cursor(GLFWwindow* window, double x, double y) {
+	Input& input = Input::instance();
+	input.mousePos(x, y);
+}
+
+static void fggl_input_scroll(GLFWwindow* window, double x, double y) {
+	Input& input = Input::instance();
+	input.mouseScroll(x, y);
+}
+
+static void fggl_input_mouse_btn(GLFWwindow* window, int btn, int action, int mods) {
+	// as we need the singleton for joysticks, might as well use it for these as well...
+	Input& input = Input::instance();
+	fggl::gfx::MouseButton buttonBit = (fggl::gfx::MouseButton)(1 << btn);
+	input.mouseBtn( buttonBit, action == GLFW_PRESS );
+}
+
+static void fggl_joystick(int jid, int state) {
+	Input& input = Input::instance();
+	if ( state == GLFW_CONNECTED ) {
+		fggl::gfx::Joystick data;
+		data.name = glfwGetJoystickName(jid);
+		data.axes = glfwGetJoystickAxes(jid, &data.axisCount);
+		data.buttons = glfwGetJoystickButtons(jid, &data.buttonCount);
+		data.hats = glfwGetJoystickHats(jid, &data.hatCount);
+
+		input.joystickConnect(jid, data);
+	} else if ( state == GLFW_DISCONNECTED ) {
+		input.joystickDisconnect(jid);
+	}
+}
+
 Context::Context() {
+	glfwInitHint(GLFW_JOYSTICK_HAT_BUTTONS, GLFW_FALSE);
+
 	int state = glfwInit();
 	if ( state == GLFW_FALSE ) {
 		const char** error;
@@ -38,7 +72,7 @@ void Context::pollEvents() {
 	glfwPollEvents();
 }
 
-Window::Window() : m_window(nullptr), m_input(nullptr) {
+Window::Window(Input& input) : m_window(nullptr), m_input(input) {
 	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
 	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
 	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
@@ -52,6 +86,12 @@ Window::Window() : m_window(nullptr), m_input(nullptr) {
 	m_framesize = glm::vec2(1920, 1080);
 	glfwSetWindowUserPointer(m_window, this);
 	glfwSetFramebufferSizeCallback( m_window, framebuffer_resize );
+
+	// input functions
+	glfwSetScrollCallback(m_window, fggl_input_scroll);
+	glfwSetCursorPosCallback(m_window, fggl_input_cursor);
+	glfwSetMouseButtonCallback(m_window, fggl_input_mouse_btn);
+	glfwSetJoystickCallback(fggl_joystick);
 }
 
 Window::~Window() {
diff --git a/fggl/gfx/window.hpp b/fggl/gfx/window.hpp
index 3045983f31f28e101d85eed99f9f8caf241ee19e..eaff91042567c14716556f0769a34d487af0382b 100644
--- a/fggl/gfx/window.hpp
+++ b/fggl/gfx/window.hpp
@@ -6,6 +6,7 @@
 
 #include <fggl/math/types.hpp>
 #include <fggl/gfx/rendering.hpp>
+#include <fggl/gfx/input.hpp>
 
 namespace fggl::gfx {
 
@@ -17,10 +18,6 @@ namespace fggl::gfx {
 			void pollEvents();
 	};
 
-	class Input {
-		private:
-	};
-
 	enum MutWindowHint {
 		Decorated = GLFW_DECORATED,
 		Resizable = GLFW_RESIZABLE,
@@ -42,7 +39,7 @@ namespace fggl::gfx {
 	};
 	class Window {
 		public:
-			Window();
+			Window(Input& input);
 			~Window();
 
 			// window <-> opengl stuff
@@ -123,9 +120,17 @@ namespace fggl::gfx {
 				return m_window;
 			}
 
+			inline Input& input() {
+				return m_input;
+			}
+
+/*			inline const Input* input() const {
+				return m_input;
+			}*/
+
 		private:
 			GLFWwindow* m_window;
-			Input* m_input;
+			Input& m_input;
 			math::vec2 m_framesize;
 
 			inline void set_hint(int hint, bool state) const {