diff --git a/demo/main.cpp b/demo/main.cpp
index 857a63d35247ae8dc672e17a088cba4462f9758b..5ea7c1f3cc18f743c455b7a29b468cc3b0a1f79d 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -78,7 +78,7 @@ void process_camera(fggl::ecs3::World& ecs, const InputManager& input) {
 		motion -= (forward * delta);
 	camTransform->origin( camTransform->origin() + motion );
 
-	if ( cam_mode == cam_arcball || input->mouse.button( fggl::input::MouseButton::MIDDLE ) ) { 
+	if ( cam_mode == cam_arcball || input->mouse.down( fggl::input::MouseButton::MIDDLE ) ) { 
 		fggl::input::process_arcball(ecs, *input, cam);
 	} else if ( cam_mode == cam_free ) {
 		fggl::input::process_freecam(ecs, *input, cam);
@@ -125,7 +125,7 @@ public:
             return;
         }
 
-        bool leftMouse = m_inputs->mouse.button(fggl::input::MouseButton::LEFT);
+        bool leftMouse = m_inputs->mouse.down(fggl::input::MouseButton::LEFT);
         if (leftMouse) {
             auto scenes = fggl::util::ServiceLocator::instance().providePtr<fggl::scenes::SceneManager>();
             scenes->activate("game");
@@ -382,6 +382,7 @@ int main(int argc, const char* argv[]) {
 
     // and now our states
     auto menu = app.add_state<fggl::scenes::BasicMenu>("menu");
+    menu->add("start", [&app]() { app.change_state("game"); });
 
     // game state
     app.add_state<GameScene>("game");
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index 20a631910e3a52191e8db22e71278ee7f104f7a1..5cefcc84e32cfb878cf60a226addec327e622612 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -20,6 +20,8 @@ target_sources(${PROJECT_NAME}
     scenes/menu.cpp
     ecs3/module/module.cpp
     input/camera_input.cpp
+    input/input.cpp
+    input/mouse.cpp
     data/heightmap.cpp
 )
 
diff --git a/fggl/app.cpp b/fggl/app.cpp
index 11124cd096e7a25a60bae8b9c99afdff4107eba7..ee3c294474b9ebe0abd489bee5a752439ad0ced0 100644
--- a/fggl/app.cpp
+++ b/fggl/app.cpp
@@ -30,7 +30,8 @@ namespace fggl {
 
         while ( m_running ) {
             auto& state = m_states.active();
-            
+
+            m_modules->onUpdate();
             state.update();
 
             // window rendering to frame buffer
diff --git a/fggl/ecs3/module/module.cpp b/fggl/ecs3/module/module.cpp
index d1fd59c33a49a37fb135116b1ed5c47aab3b6d2c..85ce2e5e8e67a1ea1150bc6d2693bd0b5863ebce 100644
--- a/fggl/ecs3/module/module.cpp
+++ b/fggl/ecs3/module/module.cpp
@@ -6,9 +6,8 @@
 
 namespace fggl::ecs3 {
 
-    void Module::onFrameStart() {
-    }
-
-    void Module::onFrameEnd() {
-    }
+    // default empty implementions
+    void Module::onUpdate() {}
+    void Module::onFrameStart() {}
+    void Module::onFrameEnd() {}
 }
diff --git a/fggl/gfx/window.cpp b/fggl/gfx/window.cpp
index 199ec90eeaeb5fdb8d1be7ca3238ba91c468e634..4c6b21c9e4bf5c0a19b3fda055ea04408d56c429 100644
--- a/fggl/gfx/window.cpp
+++ b/fggl/gfx/window.cpp
@@ -159,7 +159,7 @@ static void fggl_joystick(int jid, int state) {
 GlfwContext::GlfwContext( std::shared_ptr<fggl::input::Input> input ) {
     spdlog::debug("[glfw] context creation stated");
 	auto& glfwCallbacks = GlfwInputManager::instance();
-	glfwCallbacks.setup(std::move(input) );
+	glfwCallbacks.setup( std::move(input) );
 
 	glfwInitHint(GLFW_JOYSTICK_HAT_BUTTONS, GLFW_FALSE);
     glfwSetErrorCallback(glfw_error);
@@ -183,6 +183,9 @@ GlfwContext::~GlfwContext() {
 }
 
 void GlfwContext::pollEvents() {
+	auto& glfwCallbacks = GlfwInputManager::instance();
+    glfwCallbacks.frame();
+
 	glfwPollEvents();
 	fggl_joystick_poll();
 }
diff --git a/fggl/input/input.cpp b/fggl/input/input.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ab6e2f5e3ebc98ef93e10ea5113ac95295aa08b0
--- /dev/null
+++ b/fggl/input/input.cpp
@@ -0,0 +1,11 @@
+#include <fggl/input/input.hpp>
+
+namespace fggl::input {
+
+	void Input::frame(float dt) {
+		keyboard.frame(dt);
+		mouse.frame(dt);
+		gamepads.frame(dt);
+	}
+
+} // namespace fggl::input
diff --git a/fggl/input/mouse.cpp b/fggl/input/mouse.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..114aa5ac6e79165cffd3382b0c5362001143541a
--- /dev/null
+++ b/fggl/input/mouse.cpp
@@ -0,0 +1,13 @@
+#include <fggl/input/mouse.hpp>
+
+namespace fggl::input {
+
+    void MouseState::operator=(const MouseState& rhs) {
+        for ( int i=0; i<4; i++ ) {
+            axis[i] = rhs.axis[i];
+        }
+        buttons = rhs.buttons;
+    }
+
+
+} // namespace fggl::input
diff --git a/fggl/scenes/Scene.h b/fggl/scenes/Scene.h
index d4c9daf1aa98d6ed6ad40e2788a5ed8ef0cea01a..b749d91a9d73c9970b175fafe18fedc1cac352f7 100644
--- a/fggl/scenes/Scene.h
+++ b/fggl/scenes/Scene.h
@@ -14,6 +14,10 @@ namespace fggl::scenes {
     class Scene {
         public:
             virtual ~Scene() = default;
+            
+            // no copying
+            Scene(const Scene&) = delete;
+            Scene& operator=(const Scene&) = delete;
 
             virtual void setup() = 0;
             virtual void cleanup() = 0;
diff --git a/fggl/scenes/menu.cpp b/fggl/scenes/menu.cpp
index d99dc3c0fb3921e97ec390b5c6ef5d425d3be6d1..8d9936ed2bb3599740d6d50ce2b5ae10cce7f835 100644
--- a/fggl/scenes/menu.cpp
+++ b/fggl/scenes/menu.cpp
@@ -1,7 +1,14 @@
 #include <fggl/scenes/menu.hpp>
+#include <fggl/util/service.h>
+
+#include <spdlog/spdlog.h>
 
 namespace fggl::scenes {
 
+    using fggl::input::MouseButton;
+    using fggl::input::MouseAxis;
+
+
     static void buttonBorder( gfx::Path2D& path, glm::vec2 pos, glm::vec2 size ) {
         // outer box
         path.colour( {1.0f, 0.0f, 0.0f} );
@@ -143,17 +150,23 @@ namespace fggl::scenes {
         makeBox( path, innerTop, innerBottom );
     }
 
-    BasicMenu::BasicMenu(fggl::App& app) : AppState(app) {
-
+    BasicMenu::BasicMenu(fggl::App& app) : AppState(app), m_inputs(nullptr), m_active() {
+        auto& locator = fggl::util::ServiceLocator::instance();
+        m_inputs = locator.get<input::Input>();
     }
 
     void BasicMenu::update() {
+        if ( m_inputs != nullptr ) {
+            m_cursorPos.x = m_inputs->mouse.axis( MouseAxis::X );
+            m_cursorPos.y = m_inputs->mouse.axis( MouseAxis::Y );
 
+            if ( m_inputs->mouse.pressed( MouseButton::LEFT ) ) {
+                spdlog::info("clicky clicky: ({}, {})", m_cursorPos.x, m_cursorPos.y);
+            }
+        }
     }
 
     void BasicMenu::render(gfx::Paint& paint) {
-
-
         const math::vec2 btnSize{ 150.0f, 30.0f };
         const float spacing = 5;
 
@@ -161,9 +174,9 @@ namespace fggl::scenes {
         const float padY = 50.0f;
 
         math::vec2 pos { 1920.0f - ( padX + btnSize.x ), padY };
-        for (int i=0; i<10; i++) {
+        for ( const auto& item : m_items ) {
             gfx::Path2D btn( pos );
-            makeButton( btn, pos, btnSize, i==2, i==1 );
+            makeButton( btn, pos, btnSize, m_active == item.first, false );
             paint.fill( btn );
             pos.y += (btnSize.y + spacing);
         }
@@ -196,4 +209,8 @@ namespace fggl::scenes {
 
     }
 
+    void BasicMenu::add(const std::string& name, callback cb) {
+        m_items[name] = cb;
+    }
+
 };
diff --git a/include/fggl/app.hpp b/include/fggl/app.hpp
index 34145abd67605a4f0d933b406684d3f50c318711..160a58ea82f3ecaf7af18a0cf5f0dee0da432f7f 100644
--- a/include/fggl/app.hpp
+++ b/include/fggl/app.hpp
@@ -102,7 +102,7 @@ namespace fggl {
             int run(int argc, const char** argv);
 
             template<typename T>
-            T& add_state(const Identifer& name) {
+            T* add_state(const Identifer& name) {
                 static_assert( std::is_base_of<AppState,T>::value, "States must be AppStates");
                 return m_states.put<T>(name, *this);
             }
diff --git a/include/fggl/ecs3/module/module.h b/include/fggl/ecs3/module/module.h
index b38d1e3441695e8d1b3b060e8dc8704a1594ad24..a71ae981868dacf3658391f3c5393e155d047b39 100644
--- a/include/fggl/ecs3/module/module.h
+++ b/include/fggl/ecs3/module/module.h
@@ -21,6 +21,7 @@ namespace fggl::ecs3 {
 
             virtual void onLoad(ModuleManager& manager, TypeRegistry& tr) {};
 
+            virtual void onUpdate();
             virtual void onFrameStart();
             virtual void onFrameEnd();
     };
@@ -45,6 +46,12 @@ namespace fggl::ecs3 {
                 m_types.callbackAdd( Component<C>::typeID(), cb);
             }
 
+            void onUpdate() {
+                for ( auto& [id,ptr] : m_modules ) {
+                    ptr->onUpdate();
+                }
+            }
+
             void onFrameStart() {
                 for ( auto& [id,ptr] : m_modules ) {
                     ptr->onFrameStart();
diff --git a/include/fggl/gfx/compat.hpp b/include/fggl/gfx/compat.hpp
index d1fcfa04329cec66d5ae38547abf65f097944851..3a1b370824e5b25cd58177e7557a2094cc050009 100644
--- a/include/fggl/gfx/compat.hpp
+++ b/include/fggl/gfx/compat.hpp
@@ -35,7 +35,7 @@ namespace fggl::gfx {
             return window;
         }
 
-        void onFrameStart() override {
+        void onUpdate() override {
             context.pollEvents();
         }
 
diff --git a/include/fggl/gfx/window.hpp b/include/fggl/gfx/window.hpp
index fbb7900896d58d2fd75269f9f6296a9f62fbddf2..3a4c6f2f32e432403afa148ebf279307d8e786f5 100644
--- a/include/fggl/gfx/window.hpp
+++ b/include/fggl/gfx/window.hpp
@@ -28,7 +28,8 @@ namespace fggl::gfx {
 		AutoIconify = GLFW_AUTO_ICONIFY,
 		FocusOnShow = GLFW_FOCUS_ON_SHOW
 	};
-	enum WindowHint {
+
+    enum WindowHint {
 		Focused = GLFW_FOCUSED,
 		Iconified = GLFW_ICONIFIED,
 		Maximised = GLFW_MAXIMIZED,
@@ -40,7 +41,8 @@ namespace fggl::gfx {
 		GlDebugContext = GLFW_OPENGL_DEBUG_CONTEXT,
 		NoError = GLFW_CONTEXT_NO_ERROR
 	};
-	class GlfwWindow : public Window {
+
+    class GlfwWindow : public Window {
 		public:
 			GlfwWindow();
 			~GlfwWindow();
diff --git a/include/fggl/gfx/window_input.hpp b/include/fggl/gfx/window_input.hpp
index 6583efd28428beeca81b6027cb6411c894705a84..153fcd9d554946a77e7452db3ad163ba15f39eef 100644
--- a/include/fggl/gfx/window_input.hpp
+++ b/include/fggl/gfx/window_input.hpp
@@ -21,6 +21,10 @@ namespace fggl::gfx {
 				m_inputs = input;
 			}
 
+            inline void frame() {
+                m_inputs->frame(0.0f);
+            }
+
 			inline bool alive() {
 				return m_inputs != nullptr;
 			}
diff --git a/include/fggl/input/input.hpp b/include/fggl/input/input.hpp
index d57033937c703b96cb3942f844f63a974cb473f4..a2f03074e2b0194911b910498073a2bade054c70 100644
--- a/include/fggl/input/input.hpp
+++ b/include/fggl/input/input.hpp
@@ -10,15 +10,9 @@ namespace fggl::input {
 
 	class Input {
 		public:
-			inline Input() : keyboard(), mouse(), gamepads() {
-			}
-
-			inline void frame(float dt) {
-				keyboard.frame(dt);
-				mouse.frame(dt);
-				gamepads.frame(dt);
-			}
-
+			Input() = default;
+			void frame(float dt);
+			
 			KeyboardInput keyboard;
 			MouseInput mouse;
 			GamepadInput gamepads;
diff --git a/include/fggl/input/keyboard.hpp b/include/fggl/input/keyboard.hpp
index f4f6e828f1f0c3be361732d5c8a20f2773363e01..853e652d647b4c6754134b30330750a3240d0c2c 100644
--- a/include/fggl/input/keyboard.hpp
+++ b/include/fggl/input/keyboard.hpp
@@ -3,33 +3,37 @@
 
 #include <array>
 #include <vector>
-#include <set>
+#include <unordered_set>
 #include <iostream>
 
 namespace fggl::input {
 	using scancode_t = int;
 
-	struct KeyboardState {
-		std::set<scancode_t> m_keys;
+	class KeyboardState {
+        public:
+            KeyboardState() = default;
 
-		inline void clear() {
-			m_keys.clear();
-		}
+            inline void clear() {
+                m_keys.clear();
+            }
 
-		inline void set(scancode_t code, bool state) {
-			if ( state ) {
-				m_keys.insert( code );
-			} else {
-				m_keys.erase( code );
-			}
-		}
+            inline void set(scancode_t code, bool state) {
+                if ( state ) {
+                    m_keys.insert( code );
+                } else {
+                    m_keys.erase( code );
+                }
+            }
 
-		inline bool down(scancode_t scancode) const {
-			if ( m_keys.empty() )
-				return false;
+            inline bool down(scancode_t scancode) const {
+                if ( m_keys.empty() )
+                    return false;
 
-			return m_keys.find(scancode) != m_keys.end();
-		}
+                return m_keys.count(scancode) > 0;
+            }
+		
+		private:
+			std::unordered_set<scancode_t> m_keys;
 	};
 
 	class KeyboardInput {
diff --git a/include/fggl/input/mouse.hpp b/include/fggl/input/mouse.hpp
index d99160b9b15e61072eeede3102bbf6c4802d032f..09af8e93bd81a8ea549e1787cad85228e4262376 100644
--- a/include/fggl/input/mouse.hpp
+++ b/include/fggl/input/mouse.hpp
@@ -27,12 +27,12 @@ namespace fggl::input {
 
 	struct MouseButtonRecord {
 		MouseButton id;
-		char name[15];
+		const char name[15];
 	};
 
 	struct MouseAxisRecord {
 		MouseAxis id;
-		char name[15];
+		const char name[15];
 	};
 
 	constexpr std::array<MouseButtonRecord, 8> MouseButtons = {{
@@ -56,10 +56,16 @@ namespace fggl::input {
 	struct MouseState {
 		float axis[MouseAxes.size()];
 		std::bitset<MouseButtons.size()> buttons;
+
+        void operator=(const MouseState& rhs);
 	};
 
 	class MouseInput {
 		public:
+            MouseInput() = default;
+            MouseInput(const MouseInput& rhs) = delete;
+            void operator=(const MouseInput& rhs) = delete;
+
 			inline void frame(float dt) {
 				m_prev = m_curr;
 			}
@@ -80,10 +86,14 @@ namespace fggl::input {
 				m_curr.buttons[(int)btn] = state;
 			}
 
-			inline bool button(MouseButton btn) const {
+			inline bool down(MouseButton btn) const {
 				return m_curr.buttons[(int)btn];
 			}
 
+			inline bool downPrev(MouseButton btn) const {
+				return m_prev.buttons[(int)btn];
+			}
+
 			inline bool pressed(MouseButton btn) const {
 				return m_curr.buttons[(int)btn] && !m_prev.buttons[(int)btn];
 			}
diff --git a/include/fggl/scenes/menu.hpp b/include/fggl/scenes/menu.hpp
index c36040195c0d8229a748c515095daf4dfba8d226..14cd63eda114f9db7f0e81bfbc67b5cc764fc424 100644
--- a/include/fggl/scenes/menu.hpp
+++ b/include/fggl/scenes/menu.hpp
@@ -1,11 +1,18 @@
 #ifndef FGGL_SCENES_MENU_H
 #define FGGL_SCENES_MENU_H
 
+#include <functional>
+#include <map>
+#include <memory>
+
 #include <fggl/app.hpp>
+#include <fggl/math/types.hpp>
 #include <fggl/input/input.hpp>
 
 namespace fggl::scenes {
 
+    using callback = std::function<void(void)>;
+
     class BasicMenu : public AppState {
         public:
             BasicMenu(App& owner);
@@ -16,8 +23,15 @@ namespace fggl::scenes {
             void activate() override;
             void deactivate() override;
 
+            void add(const std::string& label, callback cb);
+
         private:
-            input::Input* m_inputs;
+            std::shared_ptr<input::Input> m_inputs;
+            std::map<const std::string, callback> m_items;
+
+            // menu state
+            std::string m_active;
+            math::vec2 m_cursorPos;
     };
 
 } // namepace fggl::scenes
diff --git a/include/fggl/util/states.hpp b/include/fggl/util/states.hpp
index a72fe314a483b4512046f67514f3db7dd4e1a736..29067a7790e7213a6bf1fa87c0a98739d825711e 100644
--- a/include/fggl/util/states.hpp
+++ b/include/fggl/util/states.hpp
@@ -42,7 +42,7 @@ namespace fggl::util {
             StateMachine& operator=(StateMachine other) = delete;
 
             template<typename T, typename... Args>
-            T& put(const Identifer& name, Args&&... args) {
+            T* put(const Identifer& name, Args&&... args) {
                 static_assert( std::is_base_of<StateType,T>::value, "States must be AppStates");
                 m_states[name] = std::make_unique<T>( std::forward<Args...>(args...) );
 
@@ -51,7 +51,7 @@ namespace fggl::util {
                     m_active = name;
                 }
 
-                return *(T*)(m_states[name].get());
+                return (T*)(m_states[name].get());
             }
 
             void change(const Identifer& name) {