diff --git a/demo/main.cpp b/demo/main.cpp
index 820e84875d8787f9be4dd44637e8adc2a313afd3..64d444ed02c2f9c059ffa39740cd551ccff0dfa5 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -152,8 +152,9 @@ public:
         auto& locator = fggl::util::ServiceLocator::instance();
         m_inputs = locator.providePtr<fggl::input::Input>();
 
-        auto types = locator.providePtr<fggl::ecs3::TypeRegistry>();
-        m_world = std::make_unique<fggl::ecs3::World>(*types);
+		// setup the world using the global type registry
+		// FIXME: type registry probably doesn't need to be application-global - will make savegame/mod support complicated
+        m_world = std::make_unique<fggl::ecs3::World>( *m_owner.registry() );
 
         m_sceneTime = std::make_unique<fggl::util::Timer>();
         m_sceneTime->frequency( glfwGetTimerFrequency() );
@@ -173,15 +174,19 @@ public:
             m_world->add(prototype, types.find(fggl::input::FreeCamKeys::name));
 
             auto camTf = m_world->get<fggl::math::Transform>(prototype);
-            camTf->origin( glm::vec3(10.0f, 3.0f, 10.0f) );
+			if ( camTf != nullptr) {
+				camTf->origin(glm::vec3(10.0f, 3.0f, 10.0f));
+			}
 
             auto cameraKeys = m_world->get<fggl::input::FreeCamKeys>(prototype);
-            cameraKeys->forward = glfwGetKeyScancode(GLFW_KEY_W);
-            cameraKeys->backward = glfwGetKeyScancode(GLFW_KEY_S);
-            cameraKeys->left = glfwGetKeyScancode(GLFW_KEY_A);
-            cameraKeys->right = glfwGetKeyScancode(GLFW_KEY_D);
-            cameraKeys->rotate_cw = glfwGetKeyScancode(GLFW_KEY_Q);
-            cameraKeys->rotate_ccw = glfwGetKeyScancode(GLFW_KEY_E);
+			if ( cameraKeys != nullptr ) {
+				cameraKeys->forward = glfwGetKeyScancode(GLFW_KEY_W);
+				cameraKeys->backward = glfwGetKeyScancode(GLFW_KEY_S);
+				cameraKeys->left = glfwGetKeyScancode(GLFW_KEY_A);
+				cameraKeys->right = glfwGetKeyScancode(GLFW_KEY_D);
+				cameraKeys->rotate_cw = glfwGetKeyScancode(GLFW_KEY_Q);
+				cameraKeys->rotate_ccw = glfwGetKeyScancode(GLFW_KEY_E);
+			}
         }
 
         fggl::ecs3::entity_t terrain;
@@ -286,7 +291,7 @@ public:
     }
 
     void render(fggl::gfx::Paint& paint) override {
-        debugInspector();
+        //debugInspector();
         fggl::gfx::renderMeshes(glModule, *m_world, m_sceneTime->delta());
     }
 
@@ -368,6 +373,7 @@ int main(int argc, const char* argv[]) {
     auto& locator = fggl::util::ServiceLocator::instance();
 	auto inputs = locator.supply<fggl::input::Input>(std::make_shared<fggl::input::Input>());
 	auto storage = locator.supply<fggl::data::Storage>(std::make_shared<fggl::data::Storage>());
+	locator.supply<fggl::ecs3::TypeRegistry>(std::make_shared<fggl::ecs3::TypeRegistry>());
 
     // Would be nice to not take args like this, it messes with lifetimes
     auto& windowing = app.use<fggl::gfx::ecsGlfwModule>(inputs);
@@ -378,15 +384,22 @@ int main(int argc, const char* argv[]) {
     window->fullscreen( true );
     app.setWindow( std::move(window) );
 
+	// load a bunch of modules to provide game functionality
+	app.use<fggl::ecs3::ecsTypes>();
+	app.use<fggl::gfx::ecsOpenGLModule>(storage);
+
 	// atlas testing
 	std::vector< fggl::gfx::ImageAtlas<char>::SubImage > images;
 	auto *atlas = fggl::gfx::ImageAtlas<char>::pack(images);
 
 	// and now our states
     auto *menu = app.add_state<fggl::scenes::BasicMenu>("menu");
+
     menu->add("start", [&app]() { app.change_state("game"); });
+	menu->add("options", [&app]() { app.change_state("game"); });
+	menu->add("quit", [&app]() { app.change_state("game"); });
 
-    // game state
+	// game state
     app.add_state<GameScene>("game");
 
 	return app.run(argc, argv);
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index 244f3bf49760ca97e5cb1579e377fc9c6ca215cf..0ff4692f0cd80b5a8b782c34a9f24e5d8c9bf87e 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -22,6 +22,9 @@ target_sources(${PROJECT_NAME}
     input/input.cpp
     input/mouse.cpp
     data/heightmap.cpp
+	gui/widget.cpp
+	gui/widgets.cpp
+	gui/containers.cpp
 )
 
 # spdlog for cleaner logging
diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index b4d18e8a3c37a0a10dbdc7d28e546de1a975613a..baf0a00f95315a7fd9442bb07ed83d0ece87e1f7 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -299,11 +299,10 @@ namespace fggl::gfx {
 		return token;
 	}
 
-// TODO(webpigeon): this shouldn't be hard-coded
+	// TODO(webpigeon): this shouldn't be hard-coded
 	constexpr glm::vec3 DEFAULT_LIGHTPOS = glm::vec3(20.0F, 20.0F, 15.0F);
 
-	void MeshRenderer::render(fggl::ecs3::World &ecs, ecs3::entity_t camera,
-							  float dt) {
+	void MeshRenderer::render(fggl::ecs3::World &ecs, ecs3::entity_t camera, float dt) {
 		if (camera == ecs::NULL_ENTITY) {
 			spdlog::warn("tried to render a scene, but no camera exists!");
 			return;
@@ -334,8 +333,7 @@ namespace fggl::gfx {
 		glm::vec3 lightPos = DEFAULT_LIGHTPOS;
 
 		// TODO(webpigeon): better performance if grouped by vao first
-		// TODO(webpigeon): the nvidia performance presentation said I shouldn't use
-		// uniforms for large data
+		// TODO(webpigeon): the nvidia performance presentation said I shouldn't use uniforms for large data
 		for (auto &entity : entities) {
 			const auto &transform = ecs.get<fggl::math::Transform>(entity);
 			const auto &mesh = ecs.get<GlRenderToken>(entity);
diff --git a/fggl/gui/containers.cpp b/fggl/gui/containers.cpp
index 33f83e24160e12e9108154949d1721d3f0a4dffa..ed36f1ad1c7b7cb03ac34fcc4485358af53282f2 100644
--- a/fggl/gui/containers.cpp
+++ b/fggl/gui/containers.cpp
@@ -2,6 +2,32 @@
 
 namespace fggl::gui {
 
+	Widget *Container::getChildAt(const math::vec2 &point) {
+		for ( auto& child : m_children ){
+			if ( child->contains(point) ){
+				return child->getChildAt(point);
+			}
+		}
+
+		return nullptr;
+	}
+
+	bool Container::contains(const math::vec2 &point) {
+		return true;
+	}
+
+	void Container::render(gfx::Paint &paint) {
+		for( auto& child : m_children ){
+			child->render( paint );
+		}
+	}
+
+	void Container::add(std::unique_ptr<Widget> widget) {
+		m_children.push_back( std::move(widget) );
+		m_dirty = true;
+	}
+
+	/*
 	Box::Box( LayoutAxis axis ) : m_axis( axis ) {}
 	
 	void Box::layout() {
@@ -24,6 +50,6 @@ namespace fggl::gui {
 			setSize(lineSum, biggestCross);
 		else
 			setSize(biggestCross, lineSum);
-	}
+	}*/
 
 };
diff --git a/fggl/gui/widget.cpp b/fggl/gui/widget.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..837a42c63683ad88fea86e5eddb72619844dd90c
--- /dev/null
+++ b/fggl/gui/widget.cpp
@@ -0,0 +1,146 @@
+//
+// Created by webpigeon on 17/04/22.
+//
+
+#include <fggl/gui/widget.hpp>
+
+namespace fggl::gui {
+
+	void buttonBorder( gfx::Path2D& path, glm::vec2 pos, glm::vec2 size ) {
+		// outer box
+		path.colour( {1.0f, 0.0f, 0.0f} );
+		path.pathTo( { pos.x + size.x, pos.y } );
+		path.pathTo( { pos.x + size.x, pos.y + size.y } );
+		path.pathTo( { pos.x, pos.y + size.y } );
+		path.close();
+
+		// inner box
+		math::vec2 innerTop { pos.x + 5, pos.y + 5 };
+		math::vec2 innerBottom { pos.x + size.x - 5, pos.y + size.y - 5 };
+
+		path.colour( {1.0f, 1.0f, 0.0f} );
+		path.moveTo( { innerTop.x, innerTop.y } );
+		path.pathTo( { innerBottom.x, innerTop.y } );
+		path.pathTo( { innerBottom.x, innerBottom.y } );
+		path.pathTo( { innerTop.x, innerBottom.y } );
+		path.pathTo( { innerTop.x, innerTop.y } );
+	}
+
+	void makeBox( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 bottomRight ) {
+		path.moveTo( { topLeft.x, topLeft.y } );
+		path.pathTo( { bottomRight.x, topLeft.y } );
+		path.pathTo( { bottomRight.x, bottomRight.y } );
+		path.pathTo( { topLeft.x, bottomRight.y } );
+		path.pathTo( { topLeft.x, topLeft.y } );
+	}
+
+	void draw_progress( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 size, float value ) {
+		const auto bottomRight { topLeft + size };
+
+		// background
+		path.colour( {0.5f, 0.5f, 0.5f} );
+		makeBox( path, topLeft, bottomRight );
+
+		// fill
+		math::vec2 innerTop { topLeft.x + 5, topLeft.y + 5 };
+		math::vec2 innerBottom { bottomRight.x - 5, bottomRight.y - 5 };
+
+		// figure out how wide the bar should be
+		float barWidth = (innerBottom.x - innerTop.x) * value;
+		float trueBottom = innerBottom.x;
+		innerBottom.x = innerTop.x + barWidth;
+
+		// draw the bar
+		path.colour( {0.8f, 0.0f, 0.0f} );
+		makeBox( path, innerTop, innerBottom );
+
+		// part of the bar that's not filled in
+		math::vec2 emptyTop { innerBottom.x, innerTop.y };
+		math::vec2 emptyBottom { trueBottom, innerBottom.y };
+		path.colour( {0.4f, 0.0f, 0.0f} );
+		makeBox( path, emptyTop, emptyBottom );
+
+	}
+
+	void draw_slider( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 size, float value ) {
+		draw_progress( path, topLeft, size, value );
+
+		// dimensions
+		const auto bottomRight { topLeft + size };
+		const math::vec2 innerTop { topLeft.x + 5, topLeft.y + 5 };
+		const math::vec2 innerBottom { bottomRight.x - 5, bottomRight.y - 5 };
+
+		// selector bar
+		float trackWidth = innerBottom.x - innerTop.x;
+		float selectorValue = trackWidth * value;
+		float selectorWidth = 6;
+
+		math::vec2 selectorTop { innerTop.x + selectorValue - ( selectorWidth/2), topLeft.y };
+		math::vec2 selectorBottom { selectorTop.x + selectorWidth, bottomRight.y };
+		path.colour( {1.0f, 1.0f, 1.0f} );
+		makeBox( path, selectorTop, selectorBottom );
+	}
+
+	void draw_button( gfx::Path2D& path, glm::vec2 pos, glm::vec2 size, bool active, bool pressed) {
+		// locations
+		math::vec2 outerTop { pos };
+		math::vec2 outerBottom { pos + size };
+		math::vec2 innerTop { pos.x + 5, pos.y + 5 };
+		math::vec2 innerBottom { pos.x + size.x - 5, pos.y + size.y - 5 };
+
+		math::vec3 baseColour{ 0.5f, 0.5f, 0.5f };
+
+		if ( active ) {
+			baseColour *= 1.2f;
+		}
+
+		if ( pressed ) {
+			baseColour *= 0.8f;
+		}
+
+		math::vec3 lightColour{ baseColour * 1.2f };
+		math::vec3 darkColour{ baseColour * 0.8f };
+		if ( pressed ) {
+			// flip light and dark for selected buttons
+			auto tmp = darkColour;
+			darkColour = lightColour;
+			lightColour = tmp;
+		}
+
+		// bottom side
+		path.colour( darkColour );
+		path.moveTo( outerTop );
+		path.pathTo( innerTop );
+		path.pathTo( { innerBottom.x, innerTop.y } );
+		path.pathTo( { outerBottom.x, outerTop.y } );
+		path.pathTo( outerTop );
+
+		// left side
+		path.colour( darkColour );
+		path.moveTo( outerTop );
+		path.pathTo( innerTop );
+		path.pathTo( { innerTop.x, innerBottom.y } );
+		path.pathTo( { outerTop.x, outerBottom.y } );
+		path.pathTo( outerTop );
+
+		// top side
+		path.colour( lightColour );
+		path.moveTo( { outerTop.x, outerBottom.y} );
+		path.pathTo( { innerTop.x, innerBottom.y} );
+		path.pathTo( innerBottom );
+		path.pathTo( outerBottom );
+		path.pathTo( { outerTop.x, outerBottom.y}  );
+
+		// right side
+		path.colour( lightColour );
+		path.moveTo( outerBottom );
+		path.pathTo( innerBottom );
+		path.pathTo( { innerBottom.x, innerTop.y } );
+		path.pathTo( { outerBottom.x, outerTop.y } );
+		path.pathTo( outerBottom  );
+
+		// inner box
+		path.colour( baseColour );
+		makeBox( path, innerTop, innerBottom );
+	}
+}
\ No newline at end of file
diff --git a/fggl/gui/widgets.cpp b/fggl/gui/widgets.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c10d07d75d51b9c8ae25fe02bdaece0b4d547908
--- /dev/null
+++ b/fggl/gui/widgets.cpp
@@ -0,0 +1,40 @@
+//
+// Created by webpigeon on 17/04/22.
+//
+
+#include <fggl/gui/widget.hpp>
+#include <fggl/gui/widgets.hpp>
+
+#include <spdlog/spdlog.h>
+
+namespace fggl::gui {
+
+	Button::Button( math::vec2 pos, math::vec2 size) : Widget(pos, size), m_hover(false), m_active(false) {}
+
+	void Button::render(gfx::Paint &paint) {
+		gfx::Path2D path{ topLeft() };
+		draw_button(path, topLeft(), size(), m_hover, m_active);
+		paint.fill(path);
+	}
+
+	void Button::activate() {
+		m_active = !m_active;
+		if ( m_active ) {
+			for( auto& callback : m_callbacks ) {
+				callback();
+			}
+		}
+	}
+
+	void Button::onEnter() {
+		m_hover = true;
+	}
+
+	void Button::onExit() {
+		m_hover = false;
+	}
+
+	void Button::addCallback(Callback cb) {
+		m_callbacks.push_back( cb );
+	}
+} // namespace fggl::gui
\ No newline at end of file
diff --git a/fggl/scenes/menu.cpp b/fggl/scenes/menu.cpp
index 8d9936ed2bb3599740d6d50ce2b5ae10cce7f835..386dbf67feab1a9be8eb7a0d0516e12f9204c9d4 100644
--- a/fggl/scenes/menu.cpp
+++ b/fggl/scenes/menu.cpp
@@ -1,5 +1,6 @@
 #include <fggl/scenes/menu.hpp>
 #include <fggl/util/service.h>
+#include <fggl/gui/gui.hpp>
 
 #include <spdlog/spdlog.h>
 
@@ -9,150 +10,14 @@ namespace fggl::scenes {
     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} );
-        path.pathTo( { pos.x + size.x, pos.y } );
-        path.pathTo( { pos.x + size.x, pos.y + size.y } );
-        path.pathTo( { pos.x, pos.y + size.y } );
-        path.close();
-
-        // inner box
-        math::vec2 innerTop { pos.x + 5, pos.y + 5 };
-        math::vec2 innerBottom { pos.x + size.x - 5, pos.y + size.y - 5 };
-
-        path.colour( {1.0f, 1.0f, 0.0f} );
-        path.moveTo( { innerTop.x, innerTop.y } );
-        path.pathTo( { innerBottom.x, innerTop.y } );
-        path.pathTo( { innerBottom.x, innerBottom.y } );
-        path.pathTo( { innerTop.x, innerBottom.y } );
-        path.pathTo( { innerTop.x, innerTop.y } );
-    }
-
-    static void makeBox( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 bottomRight ) {
-        path.moveTo( { topLeft.x, topLeft.y } );
-        path.pathTo( { bottomRight.x, topLeft.y } );
-        path.pathTo( { bottomRight.x, bottomRight.y } );
-        path.pathTo( { topLeft.x, bottomRight.y } );
-        path.pathTo( { topLeft.x, topLeft.y } );
-    }
-
-    static void makeProgress( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 size, float value ) {
-        const auto bottomRight { topLeft + size };
-
-        // background
-        path.colour( {0.5f, 0.5f, 0.5f} );
-        makeBox( path, topLeft, bottomRight );
-
-        // fill
-        math::vec2 innerTop { topLeft.x + 5, topLeft.y + 5 };
-        math::vec2 innerBottom { bottomRight.x - 5, bottomRight.y - 5 };
-
-        // figure out how wide the bar should be
-        float barWidth = (innerBottom.x - innerTop.x) * value;
-        float trueBottom = innerBottom.x;
-        innerBottom.x = innerTop.x + barWidth;
-
-        // draw the bar
-        path.colour( {0.8f, 0.0f, 0.0f} );
-        makeBox( path, innerTop, innerBottom );
-
-        // part of the bar that's not filled in
-        math::vec2 emptyTop { innerBottom.x, innerTop.y };
-        math::vec2 emptyBottom { trueBottom, innerBottom.y };
-        path.colour( {0.4f, 0.0f, 0.0f} );
-        makeBox( path, emptyTop, emptyBottom );
-
-    }
-
-    static void makeSlider( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 size, float value ) {
-
-        makeProgress( path, topLeft, size, value );
-
-        // dimensions
-        const auto bottomRight { topLeft + size };
-        const math::vec2 innerTop { topLeft.x + 5, topLeft.y + 5 };
-        const math::vec2 innerBottom { bottomRight.x - 5, bottomRight.y - 5 };
-
-        // selector bar
-        float trackWidth = innerBottom.x - innerTop.x;
-        float selectorValue = trackWidth * value;
-        float selectorWidth = 6;
-
-        math::vec2 selectorTop { innerTop.x + selectorValue - ( selectorWidth/2), topLeft.y };
-        math::vec2 selectorBottom { selectorTop.x + selectorWidth, bottomRight.y };
-        path.colour( {1.0f, 1.0f, 1.0f} );
-        makeBox( path, selectorTop, selectorBottom );
-    }
-
-
-
-    static void makeButton( gfx::Path2D& path, glm::vec2 pos, glm::vec2 size, bool active, bool pressed) {
-        // locations
-        math::vec2 outerTop { pos };
-        math::vec2 outerBottom { pos + size };
-        math::vec2 innerTop { pos.x + 5, pos.y + 5 };
-        math::vec2 innerBottom { pos.x + size.x - 5, pos.y + size.y - 5 };
 
-        math::vec3 baseColour{ 0.5f, 0.5f, 0.5f };
-
-        if ( active ) {
-            baseColour *= 1.2f;
-        }
-
-        if ( pressed ) {
-            baseColour *= 0.8f;
-        }
-
-        math::vec3 lightColour{ baseColour * 1.2f };
-        math::vec3 darkColour{ baseColour * 0.8f };
-        if ( pressed ) {
-            // flip light and dark for selected buttons
-            auto tmp = darkColour;
-            darkColour = lightColour;
-            lightColour = tmp;
-        }
-
-        // bottom side
-        path.colour( darkColour );
-        path.moveTo( outerTop );
-        path.pathTo( innerTop );
-        path.pathTo( { innerBottom.x, innerTop.y } );
-        path.pathTo( { outerBottom.x, outerTop.y } );
-        path.pathTo( outerTop );
-
-        // left side
-        path.colour( darkColour );
-        path.moveTo( outerTop );
-        path.pathTo( innerTop );
-        path.pathTo( { innerTop.x, innerBottom.y } );
-        path.pathTo( { outerTop.x, outerBottom.y } );
-        path.pathTo( outerTop );
-
-        // top side
-        path.colour( lightColour );
-        path.moveTo( { outerTop.x, outerBottom.y} );
-        path.pathTo( { innerTop.x, innerBottom.y} );
-        path.pathTo( innerBottom );
-        path.pathTo( outerBottom );
-        path.pathTo( { outerTop.x, outerBottom.y}  );
-
-        // right side
-        path.colour( lightColour );
-        path.moveTo( outerBottom );
-        path.pathTo( innerBottom );
-        path.pathTo( { innerBottom.x, innerTop.y } );
-        path.pathTo( { outerBottom.x, outerTop.y } );
-        path.pathTo( outerBottom  );
-
-        // inner box
-        path.colour( baseColour );
-        makeBox( path, innerTop, innerBottom );
-    }
-
-    BasicMenu::BasicMenu(fggl::App& app) : AppState(app), m_inputs(nullptr), m_active() {
+    BasicMenu::BasicMenu(fggl::App& app) : AppState(app), m_inputs(nullptr), m_active(), m_hover(nullptr) {
         auto& locator = fggl::util::ServiceLocator::instance();
         m_inputs = locator.get<input::Input>();
+
+		math::vec2 pos{ 500.F, 500.F };
+		math::vec2 size{ 32.5F, 35.F };
+		m_canvas.add(std::make_unique<gui::Button>(pos, size));
     }
 
     void BasicMenu::update() {
@@ -160,45 +25,36 @@ namespace fggl::scenes {
             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);
+			// in canvas space
+			math::vec2 projected;
+			projected.x = math::rescale_ndc(m_cursorPos.x, 0, 1920.f);
+			projected.y = math::rescale_ndc(m_cursorPos.y, 1080.0f, 0);
+
+			auto* hoverWidget = m_canvas.getChildAt(projected);
+			if ( hoverWidget != m_hover ){
+				if ( m_hover != nullptr ) {
+					m_hover->onExit();
+				}
+				m_hover = hoverWidget;
+				if ( m_hover != nullptr ){
+					m_hover->onEnter();
+				}
+			}
+
+			if ( m_inputs->mouse.pressed( MouseButton::LEFT ) ) {
+				spdlog::info("clicky clicky: ({}, {})", projected.x, projected.y);
+
+				auto widget = m_canvas.getChildAt(projected);
+				if (widget != nullptr) {
+					widget->activate();
+					spdlog::info("ooo! there is a thing there!");
+				}
             }
         }
     }
 
     void BasicMenu::render(gfx::Paint& paint) {
-        const math::vec2 btnSize{ 150.0f, 30.0f };
-        const float spacing = 5;
-
-        const float padX = 50.0f;
-        const float padY = 50.0f;
-
-        math::vec2 pos { 1920.0f - ( padX + btnSize.x ), padY };
-        for ( const auto& item : m_items ) {
-            gfx::Path2D btn( pos );
-            makeButton( btn, pos, btnSize, m_active == item.first, false );
-            paint.fill( btn );
-            pos.y += (btnSize.y + spacing);
-        }
-
-        pos.x = padX;
-        pos.y = padY;
-        for ( int i = 0; i <= 10; i++ ) {
-            gfx::Path2D btn( pos );
-            makeProgress( btn, pos, btnSize, i / 10.f );
-            paint.fill( btn );
-
-            pos.y += (btnSize.y + spacing);
-        }
-
-        for ( int i = 0; i <= 10; i++ ) {
-            gfx::Path2D btn( pos );
-            makeSlider( btn, pos, btnSize, i / 10.0f );
-            paint.fill( btn );
-
-            pos.y += (btnSize.y + spacing);
-        }
-
+		m_canvas.render( paint );
     }
 
     void BasicMenu::activate() {
@@ -211,6 +67,22 @@ namespace fggl::scenes {
 
     void BasicMenu::add(const std::string& name, callback cb) {
         m_items[name] = cb;
+
+		const math::vec2 btnSize{ 150.0f, 30.0f };
+		const float spacing = 5;
+		const float padX = 50.0f;
+		const float padY = 50.0f;
+
+		// figure out the position based off the old logic
+		// FIXME should be the container's job
+		math::vec2 pos { 1920.0f - ( padX + btnSize.x ), padY };
+		auto btnIdx = m_items.size() - 1;
+		pos.y += (btnIdx * (btnSize.y + spacing));
+
+		// build the button
+		auto btn = std::make_unique<gui::Button>(pos, btnSize);
+		btn->addCallback(cb);
+		m_canvas.add(std::move(btn));
     }
 
 };
diff --git a/include/fggl/app.hpp b/include/fggl/app.hpp
index 14fb9582c079d3ddc81222e2291277de4a853af7..b3f15053612ede7dfee60ad6a8fb67e7f064ed2f 100644
--- a/include/fggl/app.hpp
+++ b/include/fggl/app.hpp
@@ -129,6 +129,10 @@ namespace fggl {
 				return m_states.active();
 			}
 
+			inline ecs3::TypeRegistry* registry() {
+				return m_types.get();
+			}
+
 			inline bool running() const {
 				return m_running;
 			}
diff --git a/include/fggl/ecs3/ecs.hpp b/include/fggl/ecs3/ecs.hpp
index bdd1fe06041712c8d3b9748aba394a6609f78531..0619eda3951aa56a2926be3e965ae2c12d2bed2c 100644
--- a/include/fggl/ecs3/ecs.hpp
+++ b/include/fggl/ecs3/ecs.hpp
@@ -3,11 +3,24 @@
 
 #include <fggl/ecs3/module/module.h>
 #include <fggl/ecs3/prototype/world.h>
+#include <fggl/math/types.hpp>
 
 namespace fggl::ecs3 {
 
 	using World = prototype::World;
 
+	class ecsTypes : public Module {
+
+		public:
+			inline std::string name() const override {
+				return "ecs::core";
+			}
+
+			inline void onLoad(ModuleManager& manager, TypeRegistry& types) override {
+				types.make<math::Transform>();
+			}
+	};
+
 }
 
 #endif
\ No newline at end of file
diff --git a/include/fggl/ecs3/prototype/world.h b/include/fggl/ecs3/prototype/world.h
index 314668d197e3617aed65f132186251f712e07b91..97e2d10d2d96812c5f8519677c11c553c9387837 100644
--- a/include/fggl/ecs3/prototype/world.h
+++ b/include/fggl/ecs3/prototype/world.h
@@ -201,8 +201,18 @@ namespace fggl::ecs3::prototype {
 
 			template<typename C>
 			C *get(entity_t entity_id) {
-				auto &entity = m_entities.at(entity_id);
-				return entity.get<C>();
+				try {
+					auto &entity = m_entities.at(entity_id);
+					try {
+						return entity.get<C>();
+					} catch ( std::out_of_range& e ) {
+						std::cerr << "entity " << entity_id << " does not have component "<< C::name << std::endl;
+						return nullptr;
+					}
+				} catch ( std::out_of_range& e) {
+					std::cerr << "someone requested an component that didn't exist, entity was: " << entity_id << std::endl;
+					return nullptr;
+				}
 			}
 
 			void *get(entity_t entity_id, component_type_t t) {
diff --git a/include/fggl/ecs3/types.hpp b/include/fggl/ecs3/types.hpp
index f6fe4e527bef5481a7bb08504be5bc879a1edd26..868447213b81c0095101bf604f6a21dac1878123 100644
--- a/include/fggl/ecs3/types.hpp
+++ b/include/fggl/ecs3/types.hpp
@@ -184,6 +184,8 @@ namespace fggl::ecs3 {
 						return type;
 					}
 				}
+
+				std::cerr << "asked for unknown component type: " << name << std::endl;
 				return 0;
 			}
 
diff --git a/include/fggl/gfx/ogl/backend.hpp b/include/fggl/gfx/ogl/backend.hpp
index 047d713f3fa1da1513cfd1646a468907a257f3f7..8250f5f0e08ae1c24a0b04a2d8587e253e028336 100644
--- a/include/fggl/gfx/ogl/backend.hpp
+++ b/include/fggl/gfx/ogl/backend.hpp
@@ -72,6 +72,7 @@ namespace fggl::gfx {
 		private:
 			int m_handle;
 	};
+
 }
 
 #endif
diff --git a/include/fggl/gfx/ogl/compat.hpp b/include/fggl/gfx/ogl/compat.hpp
index 187f46a854b958e3ff36eed25c290d951b658418..8592b40fd868cdc3eef94ed31ed1fba98bdfb543 100644
--- a/include/fggl/gfx/ogl/compat.hpp
+++ b/include/fggl/gfx/ogl/compat.hpp
@@ -33,7 +33,7 @@ namespace fggl::gfx {
 		fggl::gfx::MeshRenderer renderer;
 		fggl::gfx::ShaderCache cache;
 
-		ecsOpenGLModule(Window &window, std::shared_ptr<fggl::data::Storage> storage) :
+		explicit ecsOpenGLModule(std::shared_ptr<fggl::data::Storage> storage) :
 			renderer(),
 			cache(std::move(storage)) {}
 
@@ -42,7 +42,7 @@ namespace fggl::gfx {
 		}
 
 		void uploadMesh(ecs3::World *world, ecs::entity_t entity) {
-			auto meshData = world->get<gfx::StaticMesh>(entity);
+			auto *meshData = world->get<gfx::StaticMesh>(entity);
 
 			auto pipeline = cache.get(meshData->pipeline);
 			auto glMesh = renderer.upload(meshData->mesh);
@@ -52,7 +52,7 @@ namespace fggl::gfx {
 		}
 
 		void uploadHeightmap(ecs3::World *world, ecs::entity_t entity) {
-			const auto heightmap = world->get<data::HeightMap>(entity);
+			auto *const heightmap = world->get<data::HeightMap>(entity);
 
 			data::Mesh tmpMesh{};
 			data::generateHeightMesh(heightmap, tmpMesh);
diff --git a/include/fggl/gfx/ogl/models.hpp b/include/fggl/gfx/ogl/models.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..afcaff86abd52a8a1b5168417565eb7b9cb39fcb
--- /dev/null
+++ b/include/fggl/gfx/ogl/models.hpp
@@ -0,0 +1,48 @@
+//
+// Created by webpigeon on 17/04/22.
+//
+
+#ifndef FGGL_GFX_OGL_MODELS_HPP
+#define FGGL_GFX_OGL_MODELS_HPP
+
+#include <string>
+#include <unordered_map>
+
+#include <fggl/gfx/ogl/backend.hpp>
+#include <fggl/ecs3/ecs.hpp>
+
+namespace fgg::gfx::ogl {
+
+	namespace {
+		using fggl::ecs3::World;
+		using fggl::gfx::Shader;
+	}
+
+	class StaticModelRenderer {
+		public:
+			StaticModelRenderer();
+			~StaticModelRenderer() = default;
+
+			void render(World& world) {
+				resolveModels();
+				renderModels();
+			}
+
+		private:
+			/**
+			 * Attach any missing render tokens to models.
+			 */
+			void resolveModels();
+
+			/**
+			 * Render all visible objects according to their render tokens.
+			 */
+			void renderModels();
+
+		private:
+			std::unordered_map<std::string, std::shared_ptr<Shader>> m_shaders;
+	};
+
+}
+
+#endif //FGGL_INCLUDE_FGGL_GFX_OGL_MODELS_HPP
diff --git a/include/fggl/gfx/paint.hpp b/include/fggl/gfx/paint.hpp
index 6082674daf911d43ac4bca90c0032ed4a8e69b13..01a63cf2daa3adea35189cab7b169e465863f57e 100644
--- a/include/fggl/gfx/paint.hpp
+++ b/include/fggl/gfx/paint.hpp
@@ -1,6 +1,7 @@
 #ifndef FGGL_GFX_PAINT_H
 #define FGGL_GFX_PAINT_H
 
+#include <string>
 #include <vector>
 #include <fggl/math/types.hpp>
 
diff --git a/include/fggl/gui/containers.hpp b/include/fggl/gui/containers.hpp
index 813e450908381d40d2e4cce2b4540a4cd5818ff1..d27d59d8a8678ad2e64de0c1f028535b7efc1375 100644
--- a/include/fggl/gui/containers.hpp
+++ b/include/fggl/gui/containers.hpp
@@ -5,33 +5,26 @@
 
 namespace fggl::gui {
 
-	class Widget {
-		public:
-			Widget() = default;
-
-			virtual math::vec2 baseSize();
-
-			void size(math::vec2 size);
-			math::vec2 size() const;
-
-			virtual bool contains(const math::vec2 &point);
-			virtual Widget *getChildAt(const math::vec2 &point);
-
-			virtual void render(gfx::Paint &paint) = 0;
-	};
-
 	class Container : public Widget {
 		public:
 			Container() = default;
+			virtual ~Container() = default;
 
-			void clear();
-			Widget *getChildAt(const std::math &point) override;
+			inline void clear() {
+				m_children.clear();
+			}
 
-			void add(Widget &&widget);
-			virtual void layout() = 0;
+			void add(std::unique_ptr<Widget> widget);
+			virtual inline void layout() {}
+
+			bool contains(const math::vec2 &point) override;
+			Widget *getChildAt(const math::vec2 &point) override;
+			void render(gfx::Paint &paint) override;
 
 		private:
-			bool requireLayout;
+			bool m_dirty;
+
+		protected:
 			std::vector<std::unique_ptr<Widget>> m_children;
 	};
 
@@ -50,13 +43,13 @@ namespace fggl::gui {
 	 *   * QT's QBoxLayout
 	 *   * Android's XML box things
 	 */
-	class Box : public Container {
+	/*class Box : public Container {
 		public:
 			Box(LayoutAxis axis);
 			void layout() override;
 		private:
 			const LayoutAxis m_axis;
-	}
+	};*/
 
 }; //namespace fggl::gui
 
diff --git a/include/fggl/gui/gui.hpp b/include/fggl/gui/gui.hpp
index e31d655c5b915d80543394012edf3a9a47547c61..a79255258231f042dd6a035815546e429eda0511 100644
--- a/include/fggl/gui/gui.hpp
+++ b/include/fggl/gui/gui.hpp
@@ -5,5 +5,7 @@ namespace fggl::gui {
 }; //namespace fggl::gui
 
 #include <fggl/gui/widget.hpp>
+#include <fggl/gui/containers.hpp>
+#include <fggl/gui/widgets.hpp>
 
 #endif
diff --git a/include/fggl/gui/widget.hpp b/include/fggl/gui/widget.hpp
index 50a092d0e8ce2fe4b3c447bafd62b8cfc402b2b9..e74e496f11eda4b429f146e36af84b8d74b44cf3 100644
--- a/include/fggl/gui/widget.hpp
+++ b/include/fggl/gui/widget.hpp
@@ -4,32 +4,72 @@
 #include <fggl/math/types.hpp>
 #include <fggl/gfx/paint.hpp>
 
+#include <memory>
+
 namespace fggl::gui {
 
+	void draw_box( gfx::Path2D& path, math::vec2 topLeft, math::vec2 bottomRight);
+	void draw_progress( gfx::Path2D& path, math::vec2 topLeft, math::vec2 size, float value);
+	void draw_slider( gfx::Path2D& path, math::vec2 topLeft, math::vec2 size, float value);
+	void draw_button( gfx::Path2D& path, math::vec2 topLeft, math::vec2 size, bool active, bool pressed);
+
+	struct Bounds2D {
+		math::vec2 topLeft;
+		math::vec2 size;
+
+		Bounds2D() = default;
+		inline Bounds2D(math::vec2 pos, math::vec2 a_size) : topLeft(pos), size(a_size) {}
+
+		inline bool contains(math::vec2 point) {
+			return ! ( (point.x > topLeft.x + size.x) ||
+			           (point.x < topLeft.x ) ||
+					   (point.y > topLeft.y + size.y) ||
+				       (point.y < topLeft.y)
+					   );
+		}
+	};
+
 	class Widget {
 		public:
 			Widget() = default;
+			inline Widget(math::vec2 pos, math::vec2 size) : m_bounds(pos, size) {}
 
-			virtual bool contains(const math::vec2 &point);
-			virtual Widget *getChildAt(const math::vec2 &point);
+			virtual ~Widget() = default;
 
-			virtual void render(gfx::Paint &paint) = 0;
-	};
+			inline math::vec2 topLeft() {
+				return m_bounds.topLeft;
+			};
 
-	class Container : public Widget {
-		public:
-			Container() = default;
+			inline math::vec2 bottomRight() {
+				return m_bounds.topLeft + m_bounds.size;
+			}
 
-			void clear();
-			Widget *getChildAt(const std::math &point) override;
+			inline math::vec2 size() {
+				return m_bounds.size;
+			}
 
-			void add(Widget &&widget);
+			virtual inline bool contains(const math::vec2 &point){
+				return m_bounds.contains(point);
+			};
+
+			virtual inline Widget *getChildAt(const math::vec2 &point) {
+				if ( !contains(point) ) {
+					return nullptr;
+				}
+				return this;
+			}
+
+			virtual void render(gfx::Paint &paint) = 0;
+			inline virtual void activate() {};
+
+			inline virtual void onEnter() {}
+			inline virtual void onExit() {}
 
 		private:
-			bool requireLayout;
-			std::vector<std::unique_ptr<Widget>> m_children;
+			Bounds2D m_bounds;
 	};
 
+
 }; //namespace fggl::gui
 
 #endif
diff --git a/include/fggl/gui/widgets.hpp b/include/fggl/gui/widgets.hpp
index 7fff2377e82b9656d3720a8544f537104f95a300..27a620723d7753ad6f78e6acb288eb193e668987 100644
--- a/include/fggl/gui/widgets.hpp
+++ b/include/fggl/gui/widgets.hpp
@@ -1,16 +1,30 @@
 #ifndef FGGL_GUI_WIDGETS_H
 #define FGGL_GUI_WIDGETS_H
 
+#include <functional>
 #include <fggl/gui/widget.hpp>
 
 namespace fggl::gui {
 
+	using Callback = std::function<void(void)>;
+
 	class Button : public Widget {
 		public:
-			Button();
+			Button() = default;
+			Button(math::vec2 pos, math::vec2 size);
+
 			void render(gfx::Paint &paint) override;
+
+			void activate() override;
+			void onEnter() override;
+			void onExit() override;
+
+			void addCallback(Callback cb);
 		private:
 			const std::string m_value;
+			std::vector<Callback> m_callbacks;
+			bool m_hover;
+			bool m_active;
 	};
 
 	class Label : public Widget {
diff --git a/include/fggl/math/types.hpp b/include/fggl/math/types.hpp
index 4730553f51a0a3948fcbabdb072f31e9d725a50b..9c035fe1177d24c93dcca6d1ce3e7a8f57f85b3d 100644
--- a/include/fggl/math/types.hpp
+++ b/include/fggl/math/types.hpp
@@ -30,6 +30,27 @@ namespace fggl::math {
 		return x < xi ? xi - 1 : xi;
 	}
 
+	inline float rescale_norm(float value, float min, float max) {
+		return (value - min) / (max - min);
+	}
+
+	inline float rescale_norm(float value, float min, float max, float newMin, float newMax) {
+		return newMin + ((value - min) * (newMax - newMin)) / (max - min);
+	}
+
+	inline float rescale_ndc(float value, float newMin, float newMax){
+		return rescale_norm(value, -1, 1, newMin, newMax);
+	}
+
+	inline float rescale_01(float value, float newMin, float newMax){
+		return rescale_norm(value, 0, 1, newMin, newMax);
+	}
+
+	inline float recale_mean(float value, float avg, float max, float min) {
+		return (value - avg) / (max - min);
+	}
+
+
 	// reference vectors
 	constexpr vec3f UP{0.0f, 1.0f, 0.0f};
 	constexpr vec3f FORWARD{1.0f, 0.0f, 0.0f};
diff --git a/include/fggl/scenes/menu.hpp b/include/fggl/scenes/menu.hpp
index 6e364890f391fc0b1a91824412af131e21073d94..8bb8c2be3a4d7c36e2cfa10ef26f368f8df4b202 100644
--- a/include/fggl/scenes/menu.hpp
+++ b/include/fggl/scenes/menu.hpp
@@ -8,6 +8,7 @@
 #include <fggl/app.hpp>
 #include <fggl/math/types.hpp>
 #include <fggl/input/input.hpp>
+#include <fggl/gui/gui.hpp>
 
 namespace fggl::scenes {
 
@@ -32,6 +33,8 @@ namespace fggl::scenes {
 			// menu state
 			std::string m_active;
 			math::vec2 m_cursorPos;
+			gui::Container m_canvas;
+			gui::Widget* m_hover;
 	};
 
 } // namepace fggl::scenes
diff --git a/include/fggl/util/service.h b/include/fggl/util/service.h
index d80b3bd38be6728f416d0cad88a6a78a472813f8..9bd9f59dc324892dcb8a47e055833f5e50e92f05 100644
--- a/include/fggl/util/service.h
+++ b/include/fggl/util/service.h
@@ -8,6 +8,8 @@
 #include <memory>
 #include <typeindex>
 #include <unordered_map>
+#include <stdexcept>
+#include <iostream>
 
 namespace fggl::util {
 
@@ -37,14 +39,18 @@ namespace fggl::util {
 
 			template<typename T>
 			std::shared_ptr<T> get() {
-				auto info = std::type_index(typeid(T));
-				return std::static_pointer_cast<T>(m_services.at(info));
+				try {
+					auto info = std::type_index(typeid(T));
+					return std::static_pointer_cast<T>(m_services.at(info));
+				} catch ( std::out_of_range& e ){
+					std::cerr << "someone requested a service that doesn't exist!" << std::endl;
+					return nullptr;
+				}
 			}
 
 			template<typename T>
 			std::shared_ptr<T> providePtr() {
-				auto info = std::type_index(typeid(T));
-				return std::static_pointer_cast<T>(m_services.at(info));
+				return get<T>();
 			}
 
 	};