From a087731f96d8882bad1ac4550d1b27612e50396a Mon Sep 17 00:00:00 2001
From: Mr-Wiseguy <mrwiseguyromhacking@gmail.com>
Date: Sun, 19 Jan 2025 21:00:05 -0500
Subject: [PATCH] Refactor Rml document handling to use new ContextId system
 (prompts currently unimplemented)

---
 CMakeLists.txt             |    1 +
 assets/config_menu.rml     |    6 +-
 include/recomp_ui.h        |  167 +++---
 src/game/input.cpp         |   11 +-
 src/ui/core/ui_context.cpp |   45 +-
 src/ui/core/ui_context.h   |    3 +
 src/ui/ui_config.cpp       |  159 ++----
 src/ui/ui_launcher.cpp     |   25 +-
 src/ui/ui_renderer.cpp     | 1001 ++----------------------------------
 src/ui/ui_renderer.h       |   35 ++
 src/ui/ui_state.cpp        |  745 +++++++++++++++++++++++++++
 11 files changed, 976 insertions(+), 1222 deletions(-)
 create mode 100644 src/ui/ui_renderer.h
 create mode 100644 src/ui/ui_state.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 56cae31..d87be15 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -164,6 +164,7 @@ set (SOURCES
     ${CMAKE_SOURCE_DIR}/src/game/rom_decompression.cpp
 
     ${CMAKE_SOURCE_DIR}/src/ui/ui_renderer.cpp
+    ${CMAKE_SOURCE_DIR}/src/ui/ui_state.cpp
     ${CMAKE_SOURCE_DIR}/src/ui/ui_launcher.cpp
     ${CMAKE_SOURCE_DIR}/src/ui/ui_config.cpp
     ${CMAKE_SOURCE_DIR}/src/ui/ui_config_sub_menu.cpp
diff --git a/assets/config_menu.rml b/assets/config_menu.rml
index 70b799e..0f53941 100644
--- a/assets/config_menu.rml
+++ b/assets/config_menu.rml
@@ -33,7 +33,7 @@
 </head>
 <body class="window">
 	<!-- <handle move_target="#document"> -->
-		<div id="window" class="rmlui-window rmlui-window--hidden" style="display:flex; flex-flow: column; background-color:rgba(0,0,0,0)" onkeydown="config_keydown">
+		<div id="window" class="rmlui-window" style="display:flex; flex-flow: column; background-color:rgba(0,0,0,0)" onkeydown="config_keydown">
 			<div class="centered-page" onclick="close_config_menu_backdrop">
 				<div class="centered-page__modal">
 					<tabset class="tabs" id="config_tabset">
@@ -118,7 +118,7 @@
 					<label><span style="font-family:promptfont;">&#x21A7;</span> Accept</label> -->
 				</div>
 			</div>
-			<div
+			<!-- <div
 				id="prompt-root"
 				data-model="prompt_model"
 				data-if="prompt__open"
@@ -130,7 +130,7 @@
 				data-event-click="prompt__on_click"
 			>
 				<template src="prompt"/>
-			</div>
+			</div> -->
 		</div>
 	<!-- </handle> -->
 	<!-- <handle size_target="#document" style="position: absolute; width: 16dp; height: 16dp; bottom: 0px; right: 0px; cursor: resize;"></handle> -->
diff --git a/include/recomp_ui.h b/include/recomp_ui.h
index dac3a9c..32c07d0 100644
--- a/include/recomp_ui.h
+++ b/include/recomp_ui.h
@@ -3,6 +3,9 @@
 
 #include <memory>
 #include <string>
+#include <string_view>
+
+// TODO move this file into src/ui
 
 #include "SDL.h"
 #include "RmlUi/Core.h"
@@ -10,124 +13,88 @@
 #include "../src/ui/util/hsv.h"
 #include "../src/ui/util/bem.h"
 
+#include "../src/ui/core/ui_context.h"
+
 namespace Rml {
-	class ElementDocument;
-	class EventListenerInstancer;
-	class Context;
-	class Event;
+    class ElementDocument;
+    class EventListenerInstancer;
+    class Context;
+    class Event;
 }
 
 namespace recompui {
-	class UiEventListenerInstancer;
+    class UiEventListenerInstancer;
 
-	class MenuController {
-	public:
-		virtual ~MenuController() {}
-		virtual Rml::ElementDocument* load_document(Rml::Context* context) = 0;
-		virtual void register_events(UiEventListenerInstancer& listener) = 0;
-		virtual void make_bindings(Rml::Context* context) = 0;
-	};
+    // TODO remove this once the UI has been ported over to the new system.
+    class MenuController {
+    public:
+        virtual ~MenuController() {}
+        virtual Rml::ElementDocument* load_document(Rml::Context* context) = 0;
+        virtual void register_events(UiEventListenerInstancer& listener) = 0;
+        virtual void make_bindings(Rml::Context* context) = 0;
+    };
 
-	std::unique_ptr<MenuController> create_launcher_menu();
-	std::unique_ptr<MenuController> create_config_menu();
+    std::unique_ptr<MenuController> create_launcher_menu();
+    std::unique_ptr<MenuController> create_config_menu();
 
-	using event_handler_t = void(const std::string& param, Rml::Event&);
+    using event_handler_t = void(const std::string& param, Rml::Event&);
 
-	void queue_event(const SDL_Event& event);
-	bool try_deque_event(SDL_Event& out);
+    void queue_event(const SDL_Event& event);
+    bool try_deque_event(SDL_Event& out);
 
-	std::unique_ptr<UiEventListenerInstancer> make_event_listener_instancer();
-	void register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler);
+    std::unique_ptr<UiEventListenerInstancer> make_event_listener_instancer();
+    void register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler);
 
-	enum class Menu {
-		Launcher,
-		Config,
-		None
-	};
+    void show_context(ContextId context, std::string_view param);
+    void hide_context(ContextId context);
+    void hide_all_contexts();
+    bool is_context_open(ContextId context);
+    bool is_context_taking_input();
+    bool is_any_context_open();
 
-	void set_current_menu(Menu menu);
-	Menu get_current_menu();
+    ContextId get_launcher_context_id();
+    ContextId get_config_context_id();
+    ContextId get_close_prompt_context_id();
 
-	enum class ConfigSubmenu {
-		General,
-		Controls,
-		Graphics,
-		Audio,
-        Mods,
-		Debug,
-		Count
-	};
+    enum class ButtonVariant {
+        Primary,
+        Secondary,
+        Tertiary,
+        Success,
+        Error,
+        Warning,
+        NumVariants,
+    };
 
-	enum class ButtonVariant {
-		Primary,
-		Secondary,
-		Tertiary,
-		Success,
-		Error,
-		Warning,
-		NumVariants,
-	};
+    void open_prompt(
+        const std::string& headerText,
+        const std::string& contentText,
+        const std::string& confirmLabelText,
+        const std::string& cancelLabelText,
+        std::function<void()> confirmCb,
+        std::function<void()> cancelCb,
+        ButtonVariant _confirmVariant = ButtonVariant::Success,
+        ButtonVariant _cancelVariant = ButtonVariant::Error,
+        bool _focusOnCancel = true,
+        const std::string& _returnElementId = ""
+    );
+    bool is_prompt_open();
 
-	void set_config_submenu(ConfigSubmenu submenu);
+    void apply_color_hack();
+    void get_window_size(int& width, int& height);
+    void set_cursor_visible(bool visible);
+    void update_supported_options();
+    void toggle_fullscreen();
 
-	void destroy_ui();
-	void apply_color_hack();
-	void get_window_size(int& width, int& height);
-	void set_cursor_visible(bool visible);
-	void update_supported_options();
-	void toggle_fullscreen();
-	void update_rml_display_refresh_rate();
+    bool get_cont_active(void);
+    void set_cont_active(bool active);
+    void activate_mouse();
 
-	extern const std::unordered_map<ButtonVariant, std::string> button_variants;
+    void message_box(const char* msg);
 
-	struct PromptContext {
-		Rml::DataModelHandle model_handle;
-		std::string header = "";
-		std::string content = "";
-		std::string confirmLabel = "Confirm";
-		std::string cancelLabel = "Cancel";
-		ButtonVariant confirmVariant = ButtonVariant::Success;
-		ButtonVariant cancelVariant = ButtonVariant::Error;
-		std::function<void()> onConfirm;
-		std::function<void()> onCancel;
+    void set_render_hooks();
 
-		std::string returnElementId = "";
-
-		bool open = false;
-		bool shouldFocus = false;
-		bool focusOnCancel = true;
-
-		PromptContext() = default;
-
-		void close_prompt();
-		void open_prompt(
-			const std::string& headerText,
-			const std::string& contentText,
-			const std::string& confirmLabelText,
-			const std::string& cancelLabelText,
-			std::function<void()> confirmCb,
-			std::function<void()> cancelCb,
-			ButtonVariant _confirmVariant = ButtonVariant::Success,
-			ButtonVariant _cancelVariant = ButtonVariant::Error,
-			bool _focusOnCancel = true,
-			const std::string& _returnElementId = ""
-		);
-		void on_confirm(void);
-		void on_cancel(void);
-		void on_click(Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs);
-	};
-
-	PromptContext *get_prompt_context(void);
-
-	bool get_cont_active(void);
-	void set_cont_active(bool active);
-	void activate_mouse();
-
-	void message_box(const char* msg);
-
-	void set_render_hooks();
-
-	Rml::ElementPtr create_custom_element(Rml::Element* parent, std::string tag);
+    Rml::ElementPtr create_custom_element(Rml::Element* parent, std::string tag);
 }
 
 #endif
diff --git a/src/game/input.cpp b/src/game/input.cpp
index fc707ee..4f94771 100644
--- a/src/game/input.cpp
+++ b/src/game/input.cpp
@@ -103,7 +103,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
             SDL_KeyboardEvent* keyevent = &event->key;
 
             // Skip repeated events when not in the menu
-            if (recompui::get_current_menu() == recompui::Menu::None &&
+            if (!recompui::is_context_taking_input() &&
                 event->key.repeat) {
                 break;
             }
@@ -156,8 +156,9 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
             return true;
         }
 
-        if (recompui::get_current_menu() != recompui::Menu::Config) {
-            recompui::set_current_menu(recompui::Menu::Config);
+        recompui::ContextId config_context_id = recompui::get_config_context_id();
+        if (!recompui::is_context_open(config_context_id)) {
+            recompui::show_context(config_context_id, "");
         }
 
         zelda64::open_quit_game_prompt();
@@ -711,8 +712,8 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
 }
 
 bool recomp::game_input_disabled() {
-    // Disable input if any menu is open.
-    return recompui::get_current_menu() != recompui::Menu::None;
+    // Disable input if any menu that blocks input is open.
+    return recompui::is_any_context_open();
 }
 
 bool recomp::all_input_disabled() {
diff --git a/src/ui/core/ui_context.cpp b/src/ui/core/ui_context.cpp
index 5271eb6..98d0059 100644
--- a/src/ui/core/ui_context.cpp
+++ b/src/ui/core/ui_context.cpp
@@ -5,6 +5,7 @@
 #include "slot_map.h"
 
 #include "ultramodern/error_handling.hpp"
+#include "recomp_ui.h"
 #include "ui_context.h"
 #include "../elements/ui_element.h"
 
@@ -62,8 +63,7 @@ enum class ContextErrorType {
     DestroyResourceWithoutOpen,
     DestroyResourceInWrongContext,
     DestroyResourceNotFound,
-    GetDocumentWithoutOpen,
-    GetDocumentInWrongContext,
+    GetDocumentInvalidContext,
 };
 
 enum class SlotTag : uint8_t {
@@ -116,11 +116,8 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
         case ContextErrorType::DestroyResourceNotFound:
             error_message = "Attempted to destroy a UI resource that doesn't exist in the current context";
             break;
-        case ContextErrorType::GetDocumentWithoutOpen:
-            error_message = "Attempted to get the current UI context's document with no open UI context";
-            break;
-        case ContextErrorType::GetDocumentInWrongContext:
-            error_message = "Attempted to get the document of a UI context that's not open";
+        case ContextErrorType::GetDocumentInvalidContext:
+            error_message = "Attempted to get the document of an invalid UI context";
             break;
         default:
             error_message = "Unknown UI context error";
@@ -129,7 +126,7 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
 
     // This assumes the error is coming from a mod, as it's unlikely that an end user will see a UI context error
     // in the base recomp.
-    ultramodern::error_handling::message_box((std::string{"Fatal error in mod - "} + error_message + ".").c_str());
+    recompui::message_box((std::string{"Fatal error in mod - "} + error_message + ".").c_str());
     assert(false);
     ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
 }
@@ -159,6 +156,8 @@ recompui::ContextId create_context_impl(Rml::ElementDocument* document) {
 recompui::ContextId recompui::create_context(Rml::Context* rml_context, const std::filesystem::path& path) {
     ContextId new_context = create_context_impl(nullptr);
 
+    auto workingdir = std::filesystem::current_path();
+
     new_context.open();
     Rml::ElementDocument* doc = rml_context->LoadDocument(path.string());
     opened_context->document = doc;
@@ -376,31 +375,25 @@ void recompui::ContextId::clear_children() {
 }
 
 Rml::ElementDocument* recompui::ContextId::get_document() {
-    // Ensure a context is currently opened by this thread.
-    if (opened_context_id == ContextId::null()) {
-        context_error(*this, ContextErrorType::GetDocumentWithoutOpen);
+    std::lock_guard lock{ context_state.all_contexts_lock };
+
+    Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
+    if (ctx == nullptr) {
+        context_error(*this, ContextErrorType::GetDocumentInvalidContext);
     }
 
-    // Check that the context that was specified is the same one that's currently open.
-    if (*this != opened_context_id) {
-        context_error(*this, ContextErrorType::GetDocumentInWrongContext);
-    }
-
-    return opened_context->document;
+    return ctx->document;
 }
 
 recompui::Element* recompui::ContextId::get_root_element() {
-    // Ensure a context is currently opened by this thread.
-    if (opened_context_id == ContextId::null()) {
-        context_error(*this, ContextErrorType::GetDocumentWithoutOpen);
+    std::lock_guard lock{ context_state.all_contexts_lock };
+
+    Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
+    if (ctx == nullptr) {
+        context_error(*this, ContextErrorType::GetDocumentInvalidContext);
     }
 
-    // Check that the context that was specified is the same one that's currently open.
-    if (*this != opened_context_id) {
-        context_error(*this, ContextErrorType::GetDocumentInWrongContext);
-    }
-
-    return &opened_context->root_element;
+    return &ctx->root_element;
 }
 
 recompui::ContextId recompui::get_current_context() {
diff --git a/src/ui/core/ui_context.h b/src/ui/core/ui_context.h
index f03cb00..72ef338 100644
--- a/src/ui/core/ui_context.h
+++ b/src/ui/core/ui_context.h
@@ -43,6 +43,9 @@ namespace recompui {
         void close();
 
         static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; }
+
+        // TODO
+        bool takes_input() { return true; }
     };
 
     ContextId create_context(Rml::Context*, const std::filesystem::path& path);
diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp
index 312b4e7..9be4427 100644
--- a/src/ui/ui_config.cpp
+++ b/src/ui/ui_config.cpp
@@ -19,87 +19,6 @@ Rml::DataModelHandle controls_model_handle;
 Rml::DataModelHandle graphics_model_handle;
 Rml::DataModelHandle sound_options_model_handle;
 
-recompui::PromptContext prompt_context;
-
-namespace recompui {
-    const std::unordered_map<ButtonVariant, std::string> button_variants {
-        {ButtonVariant::Primary, "primary"},
-        {ButtonVariant::Secondary, "secondary"},
-        {ButtonVariant::Tertiary, "tertiary"},
-        {ButtonVariant::Success, "success"},
-        {ButtonVariant::Error, "error"},
-        {ButtonVariant::Warning, "warning"}
-    };
-
-    void PromptContext::close_prompt() {
-        open = false;
-        model_handle.DirtyVariable("prompt__open");
-    }
-
-    void PromptContext::open_prompt(
-        const std::string& headerText,
-        const std::string& contentText,
-        const std::string& confirmLabelText,
-        const std::string& cancelLabelText,
-        std::function<void()> confirmCb,
-        std::function<void()> cancelCb,
-        ButtonVariant _confirmVariant,
-        ButtonVariant _cancelVariant,
-        bool _focusOnCancel,
-        const std::string& _returnElementId
-    ) {
-        open = true;
-        header = headerText;
-        content = contentText;
-        confirmLabel = confirmLabelText;
-        cancelLabel = cancelLabelText;
-        onConfirm = confirmCb;
-        onCancel = cancelCb;
-        confirmVariant = _confirmVariant;
-        cancelVariant = _cancelVariant;
-        focusOnCancel = _focusOnCancel;
-        returnElementId = _returnElementId;
-
-        model_handle.DirtyVariable("prompt__open");
-        model_handle.DirtyVariable("prompt__header");
-        model_handle.DirtyVariable("prompt__content");
-        model_handle.DirtyVariable("prompt__confirmLabel");
-        model_handle.DirtyVariable("prompt__cancelLabel");
-        shouldFocus = true;
-    }
-
-    void PromptContext::on_confirm(void) {
-        onConfirm();
-        open = false;
-        model_handle.DirtyVariable("prompt__open");
-    }
-
-    void PromptContext::on_cancel(void) {
-        onCancel();
-        open = false;
-        model_handle.DirtyVariable("prompt__open");
-    }
-
-    void PromptContext::on_click(Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
-        Rml::Element *target = event.GetTargetElement();
-        auto id = target->GetId();
-        if (id == "prompt__confirm-button" || id == "prompt__confirm-button-label") {
-            on_confirm();
-            event.StopPropagation();
-        } else if (id == "prompt__cancel-button" || id == "prompt__cancel-button-label") {
-            on_cancel();
-            event.StopPropagation();
-        }
-        if (event.GetCurrentElement()->GetId() == "prompt-root") {
-            event.StopPropagation();
-        }
-    }
-
-    PromptContext *get_prompt_context() {
-        return &prompt_context;
-    }
-};
-
 // True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
 bool configuring_controller = false;
 
@@ -213,11 +132,10 @@ void recomp::config_menu_set_cont_or_kb(bool cont_interacted) {
 void close_config_menu_impl() {
     zelda64::save_config();
 
-    if (ultramodern::is_game_started()) {
-        recompui::set_current_menu(recompui::Menu::None);
-    }
-    else {
-        recompui::set_current_menu(recompui::Menu::Launcher);
+	recompui::hide_context(recompui::get_config_context_id());
+
+    if (!ultramodern::is_game_started()) {
+        recompui::show_context(recompui::get_launcher_context_id(), "");
     }
 }
 
@@ -239,7 +157,7 @@ void apply_graphics_config(void) {
 
 void close_config_menu() {
     if (ultramodern::renderer::get_graphics_config() != new_options) {
-        prompt_context.open_prompt(
+        recompui::open_prompt(
             "Graphics options have changed",
             "Would you like to apply or discard the changes?",
             "Apply",
@@ -266,7 +184,7 @@ void close_config_menu() {
 }
 
 void zelda64::open_quit_game_prompt() {
-    prompt_context.open_prompt(
+    recompui::open_prompt(
         "Are you sure you want to quit?",
         "Any progress since your last save will be lost.",
         "Quit",
@@ -500,22 +418,15 @@ struct DebugContext {
     }
 };
 
-void recompui::update_rml_display_refresh_rate() {
-    static uint32_t lastRate = 0;
-    if (!graphics_model_handle) return;
-
-    uint32_t curRate = ultramodern::get_display_refresh_rate();
-    if (curRate != lastRate) {
-        graphics_model_handle.DirtyVariable("display_refresh_rate");
-    }
-    lastRate = curRate;
-}
-
 DebugContext debug_context;
 
+recompui::ContextId config_context;
+
+recompui::ContextId recompui::get_config_context_id() {
+	return config_context;
+}
+
 class ConfigMenu : public recompui::MenuController {
-private:
-	recompui::ContextId config_context;
 public:
     ConfigMenu() {
 
@@ -525,9 +436,7 @@ public:
     }
     Rml::ElementDocument* load_document(Rml::Context* context) override {
 		config_context = recompui::create_context(context, zelda64::get_asset_path("config_menu.rml"));
-        config_context.open();
         Rml::ElementDocument* ret = config_context.get_document();
-        config_context.close();
 		return ret;
     }
     void register_events(recompui::UiEventListenerInstancer& listener) override {
@@ -538,7 +447,7 @@ public:
             });
         recompui::register_event(listener, "config_keydown",
             [](const std::string& param, Rml::Event& event) {
-                if (!prompt_context.open && event.GetId() == Rml::EventId::Keydown) {
+                if (!recompui::is_prompt_open() && event.GetId() == Rml::EventId::Keydown) {
                     auto key = event.GetParameter<Rml::Input::KeyIdentifier>("key_identifier", Rml::Input::KeyIdentifier::KI_UNKNOWN);
                     switch (key) {
                         case Rml::Input::KeyIdentifier::KI_ESCAPE:
@@ -997,34 +906,15 @@ public:
         debug_context.model_handle = constructor.GetModelHandle();
     }
 
-    void make_prompt_bindings(Rml::Context* context) {
-        Rml::DataModelConstructor constructor = context->CreateDataModel("prompt_model");
-        if (!constructor) {
-            throw std::runtime_error("Failed to make RmlUi data model for the prompt");
-        }
-
-        // Bind the debug mode enabled flag.
-        constructor.Bind("prompt__open", &prompt_context.open);
-        constructor.Bind("prompt__header", &prompt_context.header);
-        constructor.Bind("prompt__content", &prompt_context.content);
-        constructor.Bind("prompt__confirmLabel", &prompt_context.confirmLabel);
-        constructor.Bind("prompt__cancelLabel", &prompt_context.cancelLabel);
-
-        constructor.BindEventCallback("prompt__on_click", &recompui::PromptContext::on_click, &prompt_context);
-
-        prompt_context.model_handle = constructor.GetModelHandle();
-    }
-
     void make_bindings(Rml::Context* context) override {
         // initially set cont state for ui help
-        recomp::config_menu_set_cont_or_kb(recompui::get_cont_active());
+        //recomp::config_menu_set_cont_or_kb(recompui::get_cont_active());
         make_nav_help_bindings(context);
         make_general_bindings(context);
         make_controls_bindings(context);
         make_graphics_bindings(context);
         make_sound_options_bindings(context);
         make_debug_bindings(context);
-        make_prompt_bindings(context);
     }
 };
 
@@ -1059,3 +949,24 @@ void recompui::toggle_fullscreen() {
     apply_graphics_config();
     graphics_model_handle.DirtyVariable("wm_option");
 }
+
+void recompui::open_prompt(
+	const std::string& headerText,
+	const std::string& contentText,
+	const std::string& confirmLabelText,
+	const std::string& cancelLabelText,
+	std::function<void()> confirmCb,
+	std::function<void()> cancelCb,
+	ButtonVariant _confirmVariant,
+	ButtonVariant _cancelVariant,
+	bool _focusOnCancel,
+	const std::string& _returnElementId
+) {
+	printf("Prompt opened\n    %s (%s): %s %s\n", contentText.c_str(), headerText.c_str(), confirmLabelText.c_str(), cancelLabelText.c_str());
+	printf("    Autoselected %s\n", confirmLabelText.c_str());
+	confirmCb();
+}
+
+bool recompui::is_prompt_open() {
+	return false;
+}
diff --git a/src/ui/ui_launcher.cpp b/src/ui/ui_launcher.cpp
index 7a6b6ba..8167a53 100644
--- a/src/ui/ui_launcher.cpp
+++ b/src/ui/ui_launcher.cpp
@@ -48,6 +48,12 @@ void select_rom() {
     });
 }
 
+recompui::ContextId launcher_context;
+
+recompui::ContextId recompui::get_launcher_context_id() {
+	return launcher_context;
+}
+
 class LauncherMenu : public recompui::MenuController {
 public:
     LauncherMenu() {
@@ -57,8 +63,9 @@ public:
 
     }
     Rml::ElementDocument* load_document(Rml::Context* context) override {
-        const std::filesystem::path asset = zelda64::get_asset_path("launcher.rml");
-        return context->LoadDocument(asset.string());
+		launcher_context = recompui::create_context(context, zelda64::get_asset_path("launcher.rml"));
+        Rml::ElementDocument* ret = launcher_context.get_document();
+		return ret;
     }
     void register_events(recompui::UiEventListenerInstancer& listener) override {
         recompui::register_event(listener, "select_rom",
@@ -75,25 +82,25 @@ public:
         recompui::register_event(listener, "start_game",
             [](const std::string& param, Rml::Event& event) {
                 recomp::start_game(supported_games[0].game_id);
-                recompui::set_current_menu(recompui::Menu::None);
+                recompui::hide_all_contexts();
             }
         );
         recompui::register_event(listener, "open_controls",
             [](const std::string& param, Rml::Event& event) {
-                recompui::set_current_menu(recompui::Menu::Config);
-                recompui::set_config_submenu(recompui::ConfigSubmenu::Controls);
+                recompui::hide_all_contexts();
+                recompui::show_context(recompui::get_config_context_id(), "controls");
             }
         );
         recompui::register_event(listener, "open_settings",
             [](const std::string& param, Rml::Event& event) {
-                recompui::set_current_menu(recompui::Menu::Config);
-                recompui::set_config_submenu(recompui::ConfigSubmenu::General);
+                recompui::hide_all_contexts();
+                recompui::show_context(recompui::get_config_context_id(), "general");
             }
         );
         recompui::register_event(listener, "open_mods",
             [](const std::string &param, Rml::Event &event) {
-                recompui::set_current_menu(recompui::Menu::Config);
-                recompui::set_config_submenu(recompui::ConfigSubmenu::Mods);
+                recompui::hide_all_contexts();
+                recompui::show_context(recompui::get_config_context_id(), "mods");
             }
         );
         recompui::register_event(listener, "exit_game",
diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp
index 8dd6c7e..6bfea72 100644
--- a/src/ui/ui_renderer.cpp
+++ b/src/ui/ui_renderer.cpp
@@ -1,37 +1,17 @@
 #ifdef _WIN32
 #define _CRT_SECURE_NO_WARNINGS
+#define WIN32_LEAN_AND_MEAN
 #endif
 
 #include <fstream>
 #include <filesystem>
-#ifdef _WIN32
-#include <SDL_video.h>
-#else
-#include <SDL2/SDL_video.h>
-#endif
-
-
-#include "recomp_ui.h"
-#include "recomp_input.h"
-#include "librecomp/game.hpp"
-#include "zelda_config.h"
-#include "zelda_support.h"
-#include "ui_rml_hacks.hpp"
-
-#include "concurrentqueue.h"
 
 #include "rt64_render_hooks.h"
 #include "rt64_render_interface_builders.h"
 
-#include "RmlUi/Core.h"
-#include "RmlUi/Debugger.h"
 #include "RmlUi/Core/RenderInterfaceCompatibility.h"
-#include "RmlUi/../../Source/Core/Elements/ElementLabel.h"
-#include "RmlUi_Platform_SDL.h"
 
-#include "ui_elements.h"
-#include "ui_mod_menu.h"
-#include "librecomp/config.hpp"
+#include "ui_renderer.h"
 
 #include "InterfaceVS.hlsl.spirv.h"
 #include "InterfacePS.hlsl.spirv.h"
@@ -65,12 +45,6 @@
         ((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : 0)
 #endif
 
-struct UIRenderContext {
-    RT64::RenderInterface* interface;
-    RT64::RenderDevice* device;
-    Rml::ElementDocument* document;
-};
-
 // TODO deduplicate from rt64_common.h
 void CalculateTextureRowWidthPadding(uint32_t rowPitch, uint32_t &rowWidth, uint32_t &rowPadding) {
     const int RowMultiple = 256;
@@ -114,9 +88,8 @@ T from_bytes_le(const char* input) {
     return *reinterpret_cast<const T*>(input);
 }
 
-void load_document();
-
-class RmlRenderInterface_RT64 : public Rml::RenderInterfaceCompatibility {
+namespace recompui {
+class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
     struct DynamicBuffer {
         std::unique_ptr<RT64::RenderBuffer> buffer_{};
         uint32_t size_ = 0;
@@ -136,7 +109,8 @@ class RmlRenderInterface_RT64 : public Rml::RenderInterfaceCompatibility {
     static constexpr RT64::RenderFormat SwapChainFormat = RT64::RenderFormat::B8G8R8A8_UNORM;
     static constexpr uint32_t RmlTextureFormatBytesPerPixel = RenderFormatSize(RmlTextureFormat);
     static_assert(RenderFormatSize(RmlTextureFormatBgra) == RmlTextureFormatBytesPerPixel);
-    struct UIRenderContext* render_context_;
+    RT64::RenderInterface* interface_;
+    RT64::RenderDevice* device_;
     int scissor_x_ = 0;
     int scissor_y_ = 0;
     int scissor_width_ = 0;
@@ -173,12 +147,13 @@ class RmlRenderInterface_RT64 : public Rml::RenderInterfaceCompatibility {
     bool scissor_enabled_ = false;
     std::vector<std::unique_ptr<RT64::RenderBuffer>> stale_buffers_{};
 public:
-    RmlRenderInterface_RT64(struct UIRenderContext* render_context) {
-        render_context_ = render_context;
+    RmlRenderInterface_RT64_impl(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
+        interface_ = interface;
+        device_ = device;
 
         // Enable 4X MSAA if supported by the device.
         const RT64::RenderSampleCounts desired_sample_count = RT64::RenderSampleCount::COUNT_8;
-        if (render_context->device->getSampleCountsSupported(SwapChainFormat) & desired_sample_count) {
+        if (device_->getSampleCountsSupported(SwapChainFormat) & desired_sample_count) {
             multisampling_.sampleCount = desired_sample_count;
         }
 
@@ -203,17 +178,17 @@ public:
         samplerDesc.addressU = RT64::RenderTextureAddressMode::CLAMP;
         samplerDesc.addressV = RT64::RenderTextureAddressMode::CLAMP;
         samplerDesc.addressW = RT64::RenderTextureAddressMode::CLAMP;
-        nearestSampler_ = render_context->device->createSampler(samplerDesc);
+        nearestSampler_ = device_->createSampler(samplerDesc);
 
         samplerDesc.minFilter = RT64::RenderFilter::LINEAR;
         samplerDesc.magFilter = RT64::RenderFilter::LINEAR;
-        linearSampler_ = render_context->device->createSampler(samplerDesc);
+        linearSampler_ = device_->createSampler(samplerDesc);
 
         // Create the shaders
-        RT64::RenderShaderFormat shaderFormat = render_context->interface->getCapabilities().shaderFormat;
+        RT64::RenderShaderFormat shaderFormat = interface_->getCapabilities().shaderFormat;
 
-        vertex_shader_ = render_context->device->createShader(GET_SHADER_BLOB(InterfaceVS, shaderFormat), GET_SHADER_SIZE(InterfaceVS, shaderFormat), "VSMain", shaderFormat);
-        pixel_shader_ = render_context->device->createShader(GET_SHADER_BLOB(InterfacePS, shaderFormat), GET_SHADER_SIZE(InterfacePS, shaderFormat), "PSMain", shaderFormat);
+        vertex_shader_ = device_->createShader(GET_SHADER_BLOB(InterfaceVS, shaderFormat), GET_SHADER_SIZE(InterfaceVS, shaderFormat), "VSMain", shaderFormat);
+        pixel_shader_ = device_->createShader(GET_SHADER_BLOB(InterfacePS, shaderFormat), GET_SHADER_SIZE(InterfacePS, shaderFormat), "PSMain", shaderFormat);
 
 
         // Create the descriptor set that contains the sampler
@@ -222,7 +197,7 @@ public:
         sampler_set_builder.addImmutableSampler(1, linearSampler_.get());
         sampler_set_builder.addConstantBuffer(3, 1); // Workaround D3D12 crash due to an empty RT64 descriptor set
         sampler_set_builder.end();
-        sampler_set_ = sampler_set_builder.create(render_context->device);
+        sampler_set_ = sampler_set_builder.create(device_);
 
         // Create a builder for the descriptor sets that will contain textures
         texture_set_builder_ = std::make_unique<RT64::RenderDescriptorSetBuilder>();
@@ -239,7 +214,7 @@ public:
         // Add the descriptor set for descriptors changed once per draw.
         layout_builder.addDescriptorSet(*texture_set_builder_);
         layout_builder.end();
-        layout_ = layout_builder.create(render_context->device);
+        layout_ = layout_builder.create(device_);
 
         // Create the pipeline description
         RT64::RenderGraphicsPipelineDesc pipeline_desc{};
@@ -256,19 +231,19 @@ public:
         pipeline_desc.vertexShader = vertex_shader_.get();
         pipeline_desc.pixelShader = pixel_shader_.get();
 
-        pipeline_ = render_context->device->createGraphicsPipeline(pipeline_desc);
+        pipeline_ = device_->createGraphicsPipeline(pipeline_desc);
 
         if (multisampling_.sampleCount > 1) {
             pipeline_desc.multisampling = multisampling_;
-            pipeline_ms_ = render_context->device->createGraphicsPipeline(pipeline_desc);
+            pipeline_ms_ = device_->createGraphicsPipeline(pipeline_desc);
 
             // Create the descriptor set for the screen drawer.
             RT64::RenderDescriptorRange screen_descriptor_range(RT64::RenderDescriptorRangeType::TEXTURE, 2, 1);
-            screen_descriptor_set_ = render_context->device->createDescriptorSet(RT64::RenderDescriptorSetDesc(&screen_descriptor_range, 1));
+            screen_descriptor_set_ = device_->createDescriptorSet(RT64::RenderDescriptorSetDesc(&screen_descriptor_range, 1));
 
             // Create vertex buffer for the screen drawer (full-screen triangle).
             screen_vertex_buffer_size_ = sizeof(Rml::Vertex) * 3;
-            screen_vertex_buffer_ = render_context->device->createBuffer(RT64::RenderBufferDesc::VertexBuffer(screen_vertex_buffer_size_, RT64::RenderHeapType::UPLOAD));
+            screen_vertex_buffer_ = device_->createBuffer(RT64::RenderBufferDesc::VertexBuffer(screen_vertex_buffer_size_, RT64::RenderHeapType::UPLOAD));
             Rml::Vertex *vertices = (Rml::Vertex *)(screen_vertex_buffer_->map());
             const Rml::ColourbPremultiplied white(255, 255, 255, 255);
             vertices[0] = Rml::Vertex{ Rml::Vector2f(-1.0f, 1.0f), white, Rml::Vector2f(0.0f, 0.0f) };
@@ -302,7 +277,7 @@ public:
         }
 
         // Create the new buffer, update the size and map it.
-        dynamic_buffer.buffer_ = render_context_->device->createBuffer(RT64::RenderBufferDesc::UploadBuffer(new_size, dynamic_buffer.flags_));
+        dynamic_buffer.buffer_ = device_->createBuffer(RT64::RenderBufferDesc::UploadBuffer(new_size, dynamic_buffer.flags_));
         dynamic_buffer.size_ = new_size;
         dynamic_buffer.bytes_used_ = 0;
 
@@ -489,7 +464,7 @@ public:
 
     bool create_texture(Rml::TextureHandle texture_handle, const Rml::byte* source, const Rml::Vector2i& source_dimensions, bool flip_y = false, bool bgra = false) {
         std::unique_ptr<RT64::RenderTexture> texture =
-            render_context_->device->createTexture(RT64::RenderTextureDesc::Texture2D(source_dimensions.x, source_dimensions.y, 1, bgra ? RmlTextureFormatBgra : RmlTextureFormat));
+            device_->createTexture(RT64::RenderTextureDesc::Texture2D(source_dimensions.x, source_dimensions.y, 1, bgra ? RmlTextureFormatBgra : RmlTextureFormat));
 
         if (texture != nullptr) {
             uint32_t image_size_bytes = source_dimensions.x * source_dimensions.y * RmlTextureFormatBytesPerPixel;
@@ -545,7 +520,7 @@ public:
             list_->barriers(RT64::RenderBarrierStage::GRAPHICS, RT64::RenderTextureBarrier(texture.get(), RT64::RenderTextureLayout::SHADER_READ));
 
             // Create a descriptor set with this texture in it.
-            std::unique_ptr<RT64::RenderDescriptorSet> set = texture_set_builder_->create(render_context_->device);
+            std::unique_ptr<RT64::RenderDescriptorSet> set = texture_set_builder_->create(device_);
 
             set->setTexture(gTexture_descriptor_index, texture.get(), RT64::RenderTextureLayout::SHADER_READ);
 
@@ -576,10 +551,10 @@ public:
         if (multisampling_.sampleCount > 1) {
             if (window_width_ != image_width || window_height_ != image_height) {
                 screen_framebuffer_.reset();
-                screen_texture_ = render_context_->device->createTexture(RT64::RenderTextureDesc::ColorTarget(image_width, image_height, SwapChainFormat));
-                screen_texture_ms_ = render_context_->device->createTexture(RT64::RenderTextureDesc::ColorTarget(image_width, image_height, SwapChainFormat, multisampling_));
+                screen_texture_ = device_->createTexture(RT64::RenderTextureDesc::ColorTarget(image_width, image_height, SwapChainFormat));
+                screen_texture_ms_ = device_->createTexture(RT64::RenderTextureDesc::ColorTarget(image_width, image_height, SwapChainFormat, multisampling_));
                 const RT64::RenderTexture *color_attachment = screen_texture_ms_.get();
-                screen_framebuffer_ = render_context_->device->createFramebuffer(RT64::RenderFramebufferDesc(&color_attachment, 1));
+                screen_framebuffer_ = device_->createFramebuffer(RT64::RenderFramebufferDesc(&color_attachment, 1));
                 screen_descriptor_set_->setTexture(0, screen_texture_.get(), RT64::RenderTextureLayout::SHADER_READ);
             }
 
@@ -651,918 +626,34 @@ public:
         list_ = nullptr;
     }
 };
+} // namespace recompui
 
-bool can_focus(Rml::Element* element) {
-    return element->GetOwnerDocument() != nullptr && element->GetProperty(Rml::PropertyId::TabIndex)->Get<Rml::Style::TabIndex>() != Rml::Style::TabIndex::None;
+recompui::RmlRenderInterface_RT64::RmlRenderInterface_RT64() = default;
+recompui::RmlRenderInterface_RT64::~RmlRenderInterface_RT64() = default;
+
+void recompui::RmlRenderInterface_RT64::reset() {
+    impl.reset();
 }
 
-//! Copied from lib\RmlUi\Source\Core\Elements\ElementLabel.cpp
-// Get the first descending element whose tag name matches one of tags.
-static Rml::Element* TagMatchRecursive(const Rml::StringList& tags, Rml::Element* element)
-{
-	const int num_children = element->GetNumChildren();
-
-	for (int i = 0; i < num_children; i++)
-	{
-		Rml::Element* child = element->GetChild(i);
-
-		for (const Rml::String& tag : tags)
-		{
-			if (child->GetTagName() == tag)
-				return child;
-		}
-
-		Rml::Element* matching_element = TagMatchRecursive(tags, child);
-		if (matching_element)
-			return matching_element;
-	}
-
-	return nullptr;
+void recompui::RmlRenderInterface_RT64::init(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
+    impl = std::make_unique<RmlRenderInterface_RT64_impl>(interface, device);
 }
 
-Rml::Element* get_target(Rml::ElementDocument* document, Rml::Element* element) {
-    // Labels can have targets, so check if this element is a label.
-    if (element->GetTagName() == "label") {
-        Rml::ElementLabel* labelElement = (Rml::ElementLabel*)element;
-        const Rml::String target_id = labelElement->GetAttribute<Rml::String>("for", "");
-
-        if (target_id.empty())
-        {
-            const Rml::StringList matching_tags = {"button", "input", "textarea", "progress", "progressbar", "select"};
-
-            return TagMatchRecursive(matching_tags, element);
-        }
-        else
-        {
-            Rml::Element* target = labelElement->GetElementById(target_id);
-            if (target != element)
-                return target;
-        }
-
-        return nullptr;
+Rml::RenderInterface* recompui::RmlRenderInterface_RT64::get_rml_interface() {
+    if (impl) {
+        return impl->GetAdaptedInterface();
     }
-    // Return the element directly if no target exists.
-    return element;
+    return nullptr;
 }
 
-namespace recompui {
-    class UiEventListener : public Rml::EventListener {
-        event_handler_t* handler_;
-        Rml::String param_;
-    public:
-        UiEventListener(event_handler_t* handler, Rml::String&& param) : handler_(handler), param_(std::move(param)) {}
-        void ProcessEvent(Rml::Event& event) override {
-            handler_(param_, event);
-        }
-    };
+void recompui::RmlRenderInterface_RT64::start(RT64::RenderCommandList* list, int image_width, int image_height) {
+    assert(static_cast<bool>(impl));
 
-    class UiEventListenerInstancer : public Rml::EventListenerInstancer {
-        std::unordered_map<Rml::String, event_handler_t*> handler_map_;
-        std::unordered_map<Rml::String, UiEventListener> listener_map_;
-    public:
-        Rml::EventListener* InstanceEventListener(const Rml::String& value, Rml::Element* element) override {
-            // Check if a listener has already been made for the full event string and return it if so.
-            auto find_listener_it = listener_map_.find(value);
-            if (find_listener_it != listener_map_.end()) {
-                return &find_listener_it->second;
-            }
-
-            // No existing listener, so check if a handler has been registered for this event type and create a listener for it if so.
-            size_t delimiter_pos = value.find(':');
-            Rml::String event_type = value.substr(0, delimiter_pos);
-            auto find_handler_it = handler_map_.find(event_type);
-            if (find_handler_it != handler_map_.end()) {
-                // A handler was found, create a listener and return it.
-                Rml::String event_param = value.substr(std::min(delimiter_pos, value.size()));
-                return &listener_map_.emplace(value, UiEventListener{ find_handler_it->second, std::move(event_param) }).first->second;
-            }
-
-            return nullptr;
-        }
-
-        void register_event(const Rml::String& value, event_handler_t* handler) {
-            handler_map_.emplace(value, handler);
-        }
-    };
+    impl->start(list, image_width, image_height);
 }
 
-void recompui::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
-    listener.register_event(name, handler);
-}
-
-Rml::Element* find_autofocus_element(Rml::Element* start) {
-    Rml::Element* cur_element = start;
-
-    while (cur_element) {
-        if (cur_element->HasAttribute("autofocus")) {
-            break;
-        }
-        cur_element = RecompRml::FindNextTabElement(cur_element, true);
-    }
-
-    return cur_element;
-}
-
-struct UIContext {
-    struct UIRenderContext render;
-    class Context {
-        std::unordered_map<recompui::Menu, std::unique_ptr<recompui::MenuController>> menus;
-        std::unordered_map<recompui::Menu, Rml::ElementDocument*> documents;
-        Rml::ElementDocument* current_document;
-        Rml::Element* prev_focused;
-        bool mouse_is_active_changed = false;
-    public:
-        bool mouse_is_active_initialized = false;
-        bool mouse_is_active = false;
-        bool cont_is_active = false;
-        bool submenu_is_active = false;
-        bool await_stick_return_x = false;
-        bool await_stick_return_y = false;
-        int last_active_mouse_position[2] = {0, 0};
-        std::unique_ptr<SystemInterface_SDL> system_interface;
-        std::unique_ptr<RmlRenderInterface_RT64> render_interface;
-        Rml::Context* context;
-        recompui::UiEventListenerInstancer event_listener_instancer;
-
-        void unload() {
-            render_interface.reset();
-        }
-
-        void swap_document(recompui::Menu menu) {
-            if (menu != recompui::Menu::Config) {
-                quit_sub_menu();
-            }
-
-            if (current_document != nullptr) {
-                Rml::Element* window_el = current_document->GetElementById("window");
-                if (window_el != nullptr) {
-                    window_el->SetClassNames("rmlui-window rmlui-window--hidden");
-                }
-                current_document->Hide();
-            }
-
-            auto find_it = documents.find(menu);
-            if (find_it != documents.end()) {
-                assert(find_it->second && "Document for menu not loaded!");
-                current_document = find_it->second;
-                Rml::Element* window_el = current_document->GetElementById("window");
-                if (window_el != nullptr) {
-                    window_el->SetClassNames("rmlui-window rmlui-window--hidden");
-                }
-                current_document->Show();
-                if (window_el != nullptr) {
-                    window_el->SetClassNames("rmlui-window");
-                }
-            }
-            else {
-                current_document = nullptr;
-            }
-            prev_focused = nullptr;
-            mouse_is_active = false;
-            mouse_is_active_changed = false;
-            mouse_is_active_initialized = false;
-
-            if (menu == recompui::Menu::Config) {
-                recompui::ElementModMenu *mods_menu = get_mods_menu();
-                recompui::ElementConfigSubMenu *config_sub_menu = get_config_sub_menu();
-                if (mods_menu != nullptr && config_sub_menu != nullptr) {
-                    mods_menu->set_config_sub_menu(config_sub_menu->get_config_sub_menu_element());
-                    config_sub_menu->set_enter_sub_menu_callback(std::bind(&Context::enter_sub_menu, this));
-                    config_sub_menu->set_quit_sub_menu_callback(std::bind(&Context::quit_sub_menu, this));
-                }
-            }
-        }
-
-        Rml::ElementTabSet *get_config_tabset() {
-            if (current_document != nullptr) {
-                Rml::Element *config_tabset_base = current_document->GetElementById("config_tabset");
-                if (config_tabset_base != nullptr) {
-                    return rmlui_dynamic_cast<Rml::ElementTabSet *>(config_tabset_base);
-                }
-            }
-
-            return nullptr;
-        }
-
-        recompui::ElementModMenu *get_mods_menu() {
-            if (current_document != nullptr) {
-                Rml::Element *menu_mods_base = current_document->GetElementById("menu_mods");
-                if (menu_mods_base != nullptr) {
-                    return rmlui_dynamic_cast<recompui::ElementModMenu *>(menu_mods_base);
-                }
-            }
-
-            return nullptr;
-        }
-
-        recompui::ElementConfigSubMenu *get_config_sub_menu() {
-            if (current_document != nullptr) {
-                Rml::Element *config_sub_menu_base = current_document->GetElementById("config_sub_menu");
-                if (config_sub_menu_base != nullptr) {
-                    return rmlui_dynamic_cast<recompui::ElementConfigSubMenu *>(config_sub_menu_base);
-                }
-            }
-
-            return nullptr;
-        }
-
-        void swap_config_menu(recompui::ConfigSubmenu submenu) {
-            Rml::ElementTabSet* config_tabset = get_config_tabset();
-            if (config_tabset != nullptr) {
-                config_tabset->SetActiveTab(static_cast<int>(submenu));
-                prev_focused = nullptr;
-                mouse_is_active = false;
-                mouse_is_active_changed = false;
-                mouse_is_active_initialized = false;
-            }
-        }
-
-        void enter_sub_menu() {
-            Rml::ElementTabSet *config_tabset = get_config_tabset();
-            recompui::ElementConfigSubMenu *config_sub_menu = get_config_sub_menu();
-            if (config_tabset != nullptr && config_sub_menu != nullptr) {
-                config_tabset->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None);
-                config_sub_menu->set_display(true);
-            }
-
-            submenu_is_active = true;
-        }
-
-        void quit_sub_menu() {
-            Rml::ElementTabSet *config_tabset = get_config_tabset();
-            recompui::ElementConfigSubMenu *config_sub_menu = get_config_sub_menu();
-            if (config_tabset != nullptr && config_sub_menu != nullptr) {
-                config_tabset->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Flex);
-                config_sub_menu->set_display(false);
-            }
-
-            submenu_is_active = false;
-        }
-
-        void load_documents() {
-            if (!documents.empty()) {
-                Rml::Factory::RegisterEventListenerInstancer(nullptr);
-                for (auto doc : documents) {
-                    doc.second->ReloadStyleSheet();
-                }
-
-                Rml::ReleaseTextures();
-                Rml::ReleaseMemoryPools();
-
-                if (current_document != nullptr) {
-                    current_document->Close();
-                }
-
-                current_document = nullptr;
-
-                documents.clear();
-                Rml::Factory::RegisterEventListenerInstancer(&event_listener_instancer);
-            }
-
-            for (auto& [menu, controller]: menus) {
-                documents.emplace(menu, controller->load_document(context));
-            }
-
-            prev_focused = nullptr;
-            mouse_is_active = false;
-            mouse_is_active_changed = false;
-            mouse_is_active_initialized = false;
-        }
-
-        void make_event_listeners() {
-            for (auto& [menu, controller]: menus) {
-                controller->register_events(event_listener_instancer);
-            }
-        }
-
-        void make_bindings() {
-            for (auto& [menu, controller]: menus) {
-                controller->make_bindings(context);
-            }
-        }
-
-        void update_primary_input(bool mouse_moved, bool non_mouse_interacted) {
-            mouse_is_active_changed = false;
-            if (non_mouse_interacted) {
-                // controller newly interacted with
-                if (mouse_is_active) {
-                    mouse_is_active = false;
-                    mouse_is_active_changed = true;
-                }
-            }
-            else if (mouse_moved) {
-                // mouse newly interacted with
-                if (!mouse_is_active) {
-                    mouse_is_active = true;
-                    mouse_is_active_changed = true;
-                }
-            }
-
-            if (mouse_moved || non_mouse_interacted) {
-                mouse_is_active_initialized = true;
-            }
-
-            if (mouse_is_active_initialized) {
-                recompui::set_cursor_visible(mouse_is_active);
-            }
-
-            if (current_document == nullptr) {
-                return;
-            }
-
-            Rml::Element* window_el = current_document->GetElementById("window");
-            if (window_el != nullptr) {
-                if (mouse_is_active) {
-                    if (!window_el->HasAttribute("mouse-active")) {
-                        window_el->SetAttribute("mouse-active", true);
-                    }
-                }
-                else if (window_el->HasAttribute("mouse-active")) {
-                    window_el->RemoveAttribute("mouse-active");
-                }
-            }
-        }
-
-        void update_focus(bool mouse_moved, bool non_mouse_interacted) {
-            if (current_document == nullptr) {
-                return;
-            }
-
-            if (cont_is_active || non_mouse_interacted) {
-                if (non_mouse_interacted && !submenu_is_active) {
-                    auto focusedEl = current_document->GetFocusLeafNode();
-                    if (focusedEl == nullptr || RecompRml::CanFocusElement(focusedEl) != RecompRml::CanFocus::Yes) {
-                        Rml::Element* element = find_autofocus_element(current_document);
-                        if (element != nullptr) {
-                            element->Focus();
-                        }
-                    }
-                }
-                return;
-            }
-
-            // If there was mouse motion, get the current hovered element (or its target if it points to one) and focus that if applicable.
-            if (mouse_is_active) {
-                if (mouse_is_active_changed) {
-                    Rml::Element* focused = current_document->GetFocusLeafNode();
-                    if (focused) focused->Blur();
-                } else if (mouse_moved) {
-                    Rml::Element* hovered = context->GetHoverElement();
-                    if (hovered) {
-                        Rml::Element* hover_target = get_target(current_document, hovered);
-                        if (hover_target && can_focus(hover_target)) {
-                            prev_focused = hover_target;
-                        }
-                    }
-                }
-            }
-
-            if (!mouse_is_active) {
-                if (!prev_focused || !can_focus(prev_focused)) {
-                    // Find the autofocus element in the tab chain
-                    Rml::Element* element = find_autofocus_element(current_document);
-                    if (element && can_focus(element)) {
-                        prev_focused = element;
-                    }
-                }
-
-                if (mouse_is_active_changed && prev_focused && can_focus(prev_focused)) {
-                    prev_focused->Focus();
-                }
-            }
-        }
-
-        void add_menu(recompui::Menu menu, std::unique_ptr<recompui::MenuController>&& controller) {
-            menus.emplace(menu, std::move(controller));
-        }
-        
-        void update_config_menu_loop(bool menu_changed) {
-            static int prevTab = -1;
-            if (menu_changed) prevTab = -1;
-            recompui::update_rml_display_refresh_rate();
-
-            Rml::ElementTabSet *tabset = (Rml::ElementTabSet *)current_document->GetElementById("config_tabset");
-            if (tabset == nullptr) return;
-
-            int curTab = tabset->GetActiveTab();
-            if (curTab == prevTab) return;
-            prevTab = curTab;
-
-            Rml::ElementList panels;
-            current_document->GetElementsByTagName(panels, "panel");
-            
-            Rml::Element *firstFocus = nullptr;
-            for (const auto& panel : panels) {
-                if (panel->IsVisible()) {
-                    firstFocus = RecompRml::FindNextTabElement(panel, true);
-                    break;
-                }
-            }
-
-            if (!firstFocus) return;
-            Rml::String id = firstFocus->GetId();
-            if (id.empty()) return;
-
-            Rml::ElementList tabs;
-            current_document->GetElementsByTagName(tabs, "tab");
-            for (const auto& tab : tabs) {
-                tab->SetProperty("nav-down", "#" + id);
-            }
-        }
-        
-        void update_prompt_loop(void) {
-            static bool wasShowingPrompt = false;
-
-            recompui::PromptContext *ctx = recompui::get_prompt_context();
-            if (!ctx->open && wasShowingPrompt) {
-                Rml::Element* focused = current_document->GetFocusLeafNode();
-                if (focused) focused->Blur();
-
-                bool didFocus = false;
-
-                if (ctx->returnElementId.size() > 0) {
-                    Rml::Element *retEl = current_document->GetElementById(ctx->returnElementId);
-                    if (retEl != nullptr && retEl->IsVisible()) {
-                        retEl->Focus(true);
-                        didFocus = true;
-                    }
-                }
-
-                if (!didFocus) {
-                    Rml::ElementList tabs;
-                    current_document->GetElementsByTagName(tabs, "tab");
-                    for (const auto& tab : tabs) {
-                        if (tab->IsVisible()) {
-                            tab->Focus(true);
-                            break;
-                        }
-                    }
-                }
-            }
-            wasShowingPrompt = ctx->open;
-
-            if (!ctx->open) {
-                return;
-            }
-
-            Rml::Element* focused = current_document->GetFocusLeafNode();
-            // Check if unfocused or current focus isn't either prompt button
-            if (mouse_is_active == false) {
-                if (
-                    focused == nullptr || (
-                        focused != current_document->GetElementById("prompt__cancel-button") &&
-                        focused != current_document->GetElementById("prompt__confirm-button")
-                    )
-                ) {
-                    ctx->shouldFocus = true;
-                }
-            }
-
-            if (!ctx->shouldFocus) {
-                return;
-            }
-
-            if (focused != nullptr) {
-                focused->Blur();
-            }
-
-            Rml::Element *targetButton = current_document->GetElementById(
-                ctx->focusOnCancel ? "prompt__cancel-button" :  "prompt__confirm-button");
-
-            if (targetButton == nullptr) {
-                return;
-            }
-
-            targetButton->Focus(true);
-
-            ctx->shouldFocus = false;
-
-            Rml::Element *confirmButton = current_document->GetElementById("prompt__confirm-button");
-            Rml::Element *cancelButton  = current_document->GetElementById("prompt__cancel-button");
-            if (confirmButton != nullptr) confirmButton->SetClassNames("button button--" + recompui::button_variants.at(ctx->confirmVariant));
-            if (cancelButton  != nullptr) cancelButton->SetClassNames( "button button--" + recompui::button_variants.at(ctx->cancelVariant));
-        }
-    } rml;
-};
-
-std::unique_ptr<UIContext> ui_context;
-std::mutex ui_context_mutex{};
-
-// TODO make this not be global
-extern SDL_Window* window;
-
-void recompui::get_window_size(int& width, int& height) {
-    SDL_GetWindowSizeInPixels(window, &width, &height);
-}
-
-inline const std::string read_file_to_string(std::filesystem::path path) {
-    std::ifstream stream = std::ifstream{path};
-    std::ostringstream ss;
-    ss << stream.rdbuf();
-    return ss.str(); 
-}
-
-void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
-#if defined(__linux__)
-    std::locale::global(std::locale::classic());
-#endif
-    ui_context = std::make_unique<UIContext>();
-
-    ui_context->rml.add_menu(recompui::Menu::Config, recompui::create_config_menu());
-    ui_context->rml.add_menu(recompui::Menu::Launcher, recompui::create_launcher_menu());
-
-    ui_context->render.interface = interface;
-    ui_context->render.device = device;
-
-    // Setup RML
-    ui_context->rml.system_interface = std::make_unique<SystemInterface_SDL>();
-    ui_context->rml.system_interface->SetWindow(window);
-    ui_context->rml.render_interface = std::make_unique<RmlRenderInterface_RT64>(&ui_context->render);
-    ui_context->rml.make_event_listeners();
-
-    Rml::SetSystemInterface(ui_context->rml.system_interface.get());
-    Rml::SetRenderInterface(ui_context->rml.render_interface.get()->GetAdaptedInterface());
-    Rml::Factory::RegisterEventListenerInstancer(&ui_context->rml.event_listener_instancer);
-
-    recompui::register_custom_elements();
-
-    Rml::Initialise();
-
-    // Apply the hack to replace RmlUi's default color parser with one that conforms to HTML5 alpha parsing for SASS compatibility
-    recompui::apply_color_hack();
-
-    int width, height;
-    SDL_GetWindowSizeInPixels(window, &width, &height);
-    
-    ui_context->rml.context = Rml::CreateContext("main", Rml::Vector2i(width, height));
-    ui_context->rml.make_bindings();
-
-    Rml::Debugger::Initialise(ui_context->rml.context);
-
-    {
-        struct FontFace {
-            const char* filename;
-            bool fallback_face;
-        };
-        FontFace font_faces[] = {
-            {"LatoLatin-Regular.ttf", false},
-            {"ChiaroNormal.otf", false},
-            {"ChiaroBold.otf", false},
-            {"LatoLatin-Italic.ttf", false},
-            {"LatoLatin-Bold.ttf", false},
-            {"LatoLatin-BoldItalic.ttf", false},
-            {"NotoEmoji-Regular.ttf", true},
-            {"promptfont/promptfont.ttf", false},
-        };
-
-        for (const FontFace& face : font_faces) {
-            auto font = zelda64::get_asset_path(face.filename);
-            Rml::LoadFontFace(font.string(), face.fallback_face);
-        }
-    }
-
-    ui_context->rml.load_documents();
-}
-
-moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
-
-void recompui::queue_event(const SDL_Event& event) {
-    ui_event_queue.enqueue(event);
-}
-
-bool recompui::try_deque_event(SDL_Event& out) {
-    return ui_event_queue.try_dequeue(out);
-}
-
-std::atomic<recompui::Menu> open_menu = recompui::Menu::Launcher;
-std::atomic<recompui::ConfigSubmenu> open_config_submenu = recompui::ConfigSubmenu::Count;
-
-int cont_button_to_key(SDL_ControllerButtonEvent& button) {
-    // Configurable accept button in menu
-    auto menuAcceptBinding0 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller);
-    auto menuAcceptBinding1 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller);
-    // note - magic number: 0 is InputType::None
-    if ((menuAcceptBinding0.input_type != 0 && button.button == menuAcceptBinding0.input_id) ||
-        (menuAcceptBinding1.input_type != 0 && button.button == menuAcceptBinding1.input_id)) {
-        return SDLK_RETURN;
-    }
-
-    // Configurable apply button in menu
-    auto menuApplyBinding0 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller);
-    auto menuApplyBinding1 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller);
-    // note - magic number: 0 is InputType::None
-    if ((menuApplyBinding0.input_type != 0 && button.button == menuApplyBinding0.input_id) ||
-        (menuApplyBinding1.input_type != 0 && button.button == menuApplyBinding1.input_id)) {
-        return SDLK_f;
-    } 
-
-    // Allows closing the menu
-    auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
-    auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
-    // note - magic number: 0 is InputType::None
-    if ((menuToggleBinding0.input_type != 0 && button.button == menuToggleBinding0.input_id) ||
-        (menuToggleBinding1.input_type != 0 && button.button == menuToggleBinding1.input_id)) {
-        return SDLK_ESCAPE;
-    }
-
-    switch (button.button) {
-        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP:
-            return SDLK_UP;
-        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN:
-            return SDLK_DOWN;
-        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT:
-            return SDLK_LEFT;
-        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
-            return SDLK_RIGHT;
-    }
-
-    return 0;
-}
-
-
-int cont_axis_to_key(SDL_ControllerAxisEvent& axis, float value) {
-    switch (axis.axis) {
-    case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY:
-        if (value < 0) return SDLK_UP;
-        return SDLK_DOWN;
-    case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX:
-        if (value >= 0) return SDLK_RIGHT;
-        return SDLK_LEFT;
-    }
-    return 0;
-}
-
-void apply_background_input_mode() {
-    static recomp::BackgroundInputMode last_input_mode = recomp::BackgroundInputMode::OptionCount;
-
-    recomp::BackgroundInputMode cur_input_mode = recomp::get_background_input_mode();
-
-    if (last_input_mode != cur_input_mode) {
-        SDL_SetHint(
-            SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
-            cur_input_mode == recomp::BackgroundInputMode::On
-                ? "1"
-                : "0"
-        );
-    }
-    last_input_mode = cur_input_mode;
-}
-
-bool recompui::get_cont_active() {
-    return ui_context->rml.cont_is_active;
-}
-
-void recompui::set_cont_active(bool active) {
-    ui_context->rml.cont_is_active = active;
-}
-
-void recompui::activate_mouse() {
-    ui_context->rml.update_primary_input(true, false);
-    ui_context->rml.update_focus(true, false);
-}
-
-void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* swap_chain_framebuffer) {
-    std::lock_guard lock {ui_context_mutex};
-
-    apply_background_input_mode();
-
-    // Return early if the ui context has been destroyed already.
-    if (!ui_context) {
-        return;
-    }
-
-    int num_keys;
-    const Uint8* key_state = SDL_GetKeyboardState(&num_keys);
-
-    static bool was_reload_held = false;
-    bool is_reload_held = key_state[SDL_SCANCODE_F10] != 0;
-    bool reload_sheets = is_reload_held && !was_reload_held;
-    was_reload_held = is_reload_held;
-
-    static recompui::Menu prev_menu = recompui::Menu::None;
-    recompui::Menu cur_menu = open_menu.load();
-
-    // Return to the launcher if no menu is open and the game isn't started.
-    if (cur_menu == recompui::Menu::None && !ultramodern::is_game_started()) {
-        cur_menu = recompui::Menu::Launcher;
-        recompui::set_current_menu(cur_menu);
-    }
-
-    if (reload_sheets) {
-        ui_context->rml.load_documents();
-        prev_menu = recompui::Menu::None;
-    }
-
-    bool menu_changed = cur_menu != prev_menu;
-    if (menu_changed) {
-        ui_context->rml.swap_document(cur_menu);
-    }
-
-    recompui::ConfigSubmenu config_submenu = open_config_submenu.load();
-    if (config_submenu != recompui::ConfigSubmenu::Count) {
-        ui_context->rml.swap_config_menu(config_submenu);
-        open_config_submenu.store(recompui::ConfigSubmenu::Count);
-    }
-
-    prev_menu = cur_menu;
-
-    SDL_Event cur_event{};
-
-    bool mouse_moved = false;
-    bool mouse_clicked = false;
-    bool non_mouse_interacted = false;
-    bool cont_interacted = false;
-    bool kb_interacted = false;
-
-    if (cur_menu == recompui::Menu::Config) {
-        ui_context->rml.update_config_menu_loop(menu_changed);
-    }
-    if (cur_menu != recompui::Menu::None) {
-        ui_context->rml.update_prompt_loop();
-    }
-
-    while (recompui::try_deque_event(cur_event)) {
-        bool menu_is_open = cur_menu != recompui::Menu::None;
-
-        if (!recomp::all_input_disabled()) {
-            // Implement some additional behavior for specific events on top of what RmlUi normally does with them.
-            switch (cur_event.type) {
-            case SDL_EventType::SDL_MOUSEMOTION: {
-                int *last_mouse_pos = ui_context->rml.last_active_mouse_position;
-
-                if (!ui_context->rml.mouse_is_active) {
-                    float xD = cur_event.motion.x - last_mouse_pos[0];
-                    float yD = cur_event.motion.y - last_mouse_pos[1];
-                    if (sqrt(xD * xD + yD * yD) < 100) {
-                        break;
-                    }
-                }
-                last_mouse_pos[0] = cur_event.motion.x;
-                last_mouse_pos[1] = cur_event.motion.y;
-
-                // if controller is the primary input, don't use mouse movement to allow cursor to reactivate
-                if (recompui::get_cont_active()) {
-                    break;
-                }
-            }
-            // fallthrough
-            case SDL_EventType::SDL_MOUSEBUTTONDOWN:
-                mouse_moved = true;
-                mouse_clicked = true;
-                break;
-                
-            case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
-                int rml_key = cont_button_to_key(cur_event.cbutton);
-                if (menu_is_open && rml_key) {
-                    ui_context->rml.context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
-                }
-                non_mouse_interacted = true;
-                cont_interacted = true;
-                break;
-            }
-            case SDL_EventType::SDL_KEYDOWN:
-                non_mouse_interacted = true;
-                kb_interacted = true;
-                break;
-            case SDL_EventType::SDL_USEREVENT:
-                if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
-                    ui_context->rml.await_stick_return_y = true;
-                } else if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
-                    ui_context->rml.await_stick_return_x = true;
-                }
-                break;
-            case SDL_EventType::SDL_CONTROLLERAXISMOTION:
-                SDL_ControllerAxisEvent* axis_event = &cur_event.caxis;
-                if (axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY && axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
-                    break;
-                }
-
-                float axis_value = axis_event->value * (1 / 32768.0f);
-                bool* await_stick_return = axis_event->axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY
-                        ? &ui_context->rml.await_stick_return_y
-                        : &ui_context->rml.await_stick_return_x;
-                if (fabsf(axis_value) > 0.5f) {
-                    if (!*await_stick_return) {
-                        *await_stick_return = true;
-                        non_mouse_interacted = true;
-                        int rml_key = cont_axis_to_key(cur_event.caxis, axis_value);
-                        if (menu_is_open && rml_key) {
-                            ui_context->rml.context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
-                        }
-                    }
-                    non_mouse_interacted = true;
-                    cont_interacted = true;
-                }
-                else if (*await_stick_return && fabsf(axis_value) < 0.15f) {
-                    *await_stick_return = false;
-                }
-                break;
-            }
-
-            if (menu_is_open) {
-                RmlSDL::InputEventHandler(ui_context->rml.context, cur_event);
-            }
-        }
-
-        // If no menu is open and the game has been started and either the escape key or select button are pressed, open the config menu.
-        if (!menu_is_open && ultramodern::is_game_started()) {
-            bool open_config = false;
-
-            switch (cur_event.type) {
-            case SDL_EventType::SDL_KEYDOWN:
-                if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
-                    open_config = true;
-                }
-                break;
-            case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
-                auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
-                auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
-                // note - magic number: 0 is InputType::None
-                if ((menuToggleBinding0.input_type != 0 && cur_event.cbutton.button == menuToggleBinding0.input_id) ||
-                    (menuToggleBinding1.input_type != 0 && cur_event.cbutton.button == menuToggleBinding1.input_id)) {
-                    open_config = true;
-                }
-                break;
-            }
-
-            if (open_config) {
-                cur_menu = recompui::Menu::Config;
-                open_menu.store(recompui::Menu::Config);
-                ui_context->rml.swap_document(cur_menu);
-            }
-        }
-    } // end dequeue event loop
-
-    if (cont_interacted || kb_interacted || mouse_clicked) {
-        recompui::set_cont_active(cont_interacted);
-    }
-    recomp::config_menu_set_cont_or_kb(ui_context->rml.cont_is_active);
-
-    recomp::InputField scanned_field = recomp::get_scanned_input();
-    if (scanned_field != recomp::InputField{}) {
-        recomp::finish_scanning_input(scanned_field);
-    }
-
-    ui_context->rml.update_primary_input(mouse_moved, non_mouse_interacted);
-    ui_context->rml.update_focus(mouse_moved, non_mouse_interacted);
-
-    if (cur_menu != recompui::Menu::None) {
-        int width = swap_chain_framebuffer->getWidth();
-        int height = swap_chain_framebuffer->getHeight();
-
-        // Scale the UI based on the window size with 1080 vertical resolution as the reference point.
-        ui_context->rml.context->SetDensityIndependentPixelRatio((height) / 1080.0f);
-
-        ui_context->rml.render_interface->start(command_list, width, height);
-
-        static int prev_width = 0;
-        static int prev_height = 0;
-
-        if (prev_width != width || prev_height != height) {
-            ui_context->rml.context->SetDimensions({ width, height });
-        }
-        prev_width = width;
-        prev_height = height;
-
-        ui_context->rml.context->Update();
-        ui_context->rml.context->Render();
-        ui_context->rml.render_interface->end(command_list, swap_chain_framebuffer);
-    }
-}
-
-void deinit_hook() {
-    recompui::destroy_all_contexts();
-
-    std::lock_guard lock {ui_context_mutex};
-    Rml::Debugger::Shutdown();
-    Rml::Shutdown();
-    ui_context->rml.unload();
-    ui_context.reset();
-}
-
-void recompui::set_render_hooks() {
-    RT64::SetRenderHooks(init_hook, draw_hook, deinit_hook);
-}
-
-void recompui::set_current_menu(Menu menu) {
-    open_menu.store(menu);
-    if (menu == recompui::Menu::None) {
-        ui_context->rml.system_interface->SetMouseCursor("arrow");
-    }
-}
-
-void recompui::set_config_submenu(recompui::ConfigSubmenu submenu) {
-	open_config_submenu.store(submenu);
-}
-
-void recompui::destroy_ui() {
-}
-
-recompui::Menu recompui::get_current_menu() {
-    return open_menu.load();
-}
-
-void recompui::message_box(const char* msg) {
-    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), msg, nullptr);
-    printf("[ERROR] %s\n", msg);
+void recompui::RmlRenderInterface_RT64::end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer) {
+    assert(static_cast<bool>(impl));
+
+    impl->end(list, framebuffer);
 }
diff --git a/src/ui/ui_renderer.h b/src/ui/ui_renderer.h
new file mode 100644
index 0000000..187c02c
--- /dev/null
+++ b/src/ui/ui_renderer.h
@@ -0,0 +1,35 @@
+#ifndef __UI_RENDERER_H__
+#define __UI_RENDERER_H__
+
+#include <memory>
+
+namespace RT64 {
+    class RenderInterface;
+    class RenderDevice;
+    class RenderCommandList;
+    class RenderFramebuffer;
+};
+
+namespace Rml {
+    class RenderInterface;
+}
+
+namespace recompui {
+    class RmlRenderInterface_RT64_impl;
+
+    class RmlRenderInterface_RT64 {
+    private:
+        std::unique_ptr<RmlRenderInterface_RT64_impl> impl;
+    public:
+        RmlRenderInterface_RT64();
+        ~RmlRenderInterface_RT64();
+        void reset();
+        void init(RT64::RenderInterface* interface, RT64::RenderDevice* device);
+        Rml::RenderInterface* get_rml_interface();
+        
+        void start(RT64::RenderCommandList* list, int image_width, int image_height);
+        void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer);
+    };
+} // namespace recompui
+
+#endif
diff --git a/src/ui/ui_state.cpp b/src/ui/ui_state.cpp
new file mode 100644
index 0000000..e43a169
--- /dev/null
+++ b/src/ui/ui_state.cpp
@@ -0,0 +1,745 @@
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <SDL_video.h>
+#else
+#include <SDL2/SDL_video.h>
+#endif
+
+#include "rt64_render_hooks.h"
+
+#include "concurrentqueue.h"
+
+#include "RmlUi/Core.h"
+#include "RmlUi/Debugger.h"
+#include "RmlUi/Core/RenderInterfaceCompatibility.h"
+#include "RmlUi/../../Source/Core/Elements/ElementLabel.h"
+#include "RmlUi_Platform_SDL.h"
+
+#include "recomp_ui.h"
+#include "recomp_input.h"
+#include "librecomp/game.hpp"
+#include "zelda_config.h"
+#include "ui_rml_hacks.hpp"
+#include "ui_elements.h"
+#include "ui_mod_menu.h"
+#include "ui_renderer.h"
+#include "librecomp/config.hpp"
+
+bool can_focus(Rml::Element* element) {
+    return element->GetOwnerDocument() != nullptr && element->GetProperty(Rml::PropertyId::TabIndex)->Get<Rml::Style::TabIndex>() != Rml::Style::TabIndex::None;
+}
+
+//! Copied from lib\RmlUi\Source\Core\Elements\ElementLabel.cpp
+// Get the first descending element whose tag name matches one of tags.
+static Rml::Element* TagMatchRecursive(const Rml::StringList& tags, Rml::Element* element)
+{
+	const int num_children = element->GetNumChildren();
+
+	for (int i = 0; i < num_children; i++)
+	{
+		Rml::Element* child = element->GetChild(i);
+
+		for (const Rml::String& tag : tags)
+		{
+			if (child->GetTagName() == tag)
+				return child;
+		}
+
+		Rml::Element* matching_element = TagMatchRecursive(tags, child);
+		if (matching_element)
+			return matching_element;
+	}
+
+	return nullptr;
+}
+
+Rml::Element* get_target(Rml::ElementDocument* document, Rml::Element* element) {
+    // Labels can have targets, so check if this element is a label.
+    if (element->GetTagName() == "label") {
+        Rml::ElementLabel* labelElement = (Rml::ElementLabel*)element;
+        const Rml::String target_id = labelElement->GetAttribute<Rml::String>("for", "");
+
+        if (target_id.empty())
+        {
+            const Rml::StringList matching_tags = {"button", "input", "textarea", "progress", "progressbar", "select"};
+
+            return TagMatchRecursive(matching_tags, element);
+        }
+        else
+        {
+            Rml::Element* target = labelElement->GetElementById(target_id);
+            if (target != element)
+                return target;
+        }
+
+        return nullptr;
+    }
+    // Return the element directly if no target exists.
+    return element;
+}
+
+namespace recompui {
+    class UiEventListener : public Rml::EventListener {
+        event_handler_t* handler_;
+        Rml::String param_;
+    public:
+        UiEventListener(event_handler_t* handler, Rml::String&& param) : handler_(handler), param_(std::move(param)) {}
+        void ProcessEvent(Rml::Event& event) override {
+            handler_(param_, event);
+        }
+    };
+
+    class UiEventListenerInstancer : public Rml::EventListenerInstancer {
+        std::unordered_map<Rml::String, event_handler_t*> handler_map_;
+        std::unordered_map<Rml::String, UiEventListener> listener_map_;
+    public:
+        Rml::EventListener* InstanceEventListener(const Rml::String& value, Rml::Element* element) override {
+            // Check if a listener has already been made for the full event string and return it if so.
+            auto find_listener_it = listener_map_.find(value);
+            if (find_listener_it != listener_map_.end()) {
+                return &find_listener_it->second;
+            }
+
+            // No existing listener, so check if a handler has been registered for this event type and create a listener for it if so.
+            size_t delimiter_pos = value.find(':');
+            Rml::String event_type = value.substr(0, delimiter_pos);
+            auto find_handler_it = handler_map_.find(event_type);
+            if (find_handler_it != handler_map_.end()) {
+                // A handler was found, create a listener and return it.
+                Rml::String event_param = value.substr(std::min(delimiter_pos, value.size()));
+                return &listener_map_.emplace(value, UiEventListener{ find_handler_it->second, std::move(event_param) }).first->second;
+            }
+
+            return nullptr;
+        }
+
+        void register_event(const Rml::String& value, event_handler_t* handler) {
+            handler_map_.emplace(value, handler);
+        }
+    };
+}
+
+void recompui::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
+    listener.register_event(name, handler);
+}
+
+Rml::Element* find_autofocus_element(Rml::Element* start) {
+    Rml::Element* cur_element = start;
+
+    while (cur_element) {
+        if (cur_element->HasAttribute("autofocus")) {
+            break;
+        }
+        cur_element = RecompRml::FindNextTabElement(cur_element, true);
+    }
+
+    return cur_element;
+}
+
+struct ContextDetails {
+    recompui::ContextId context;
+    Rml::ElementDocument* document;
+    bool takes_input;
+};
+
+class UIState {
+    Rml::Element* prev_focused = nullptr;
+    bool mouse_is_active_changed = false;
+    std::unique_ptr<recompui::MenuController> launcher_menu_controller{};
+    std::unique_ptr<recompui::MenuController> config_menu_controller{};
+    std::vector<ContextDetails> opened_contexts{};
+public:
+    bool mouse_is_active_initialized = false;
+    bool mouse_is_active = false;
+    bool cont_is_active = false;
+    bool submenu_is_active = false;
+    bool await_stick_return_x = false;
+    bool await_stick_return_y = false;
+    int last_active_mouse_position[2] = {0, 0};
+    std::unique_ptr<recompui::MenuController> config_controller;
+    std::unique_ptr<recompui::MenuController> launcher_controller;
+    std::unique_ptr<SystemInterface_SDL> system_interface;
+    recompui::RmlRenderInterface_RT64 render_interface;
+    Rml::Context* context;
+    recompui::UiEventListenerInstancer event_listener_instancer;
+
+    UIState(const UIState& rhs) = delete;
+    UIState& operator=(const UIState& rhs) = delete;
+    UIState(UIState&& rhs) = delete;
+    UIState& operator=(UIState&& rhs) = delete;
+
+    UIState(SDL_Window* window, RT64::RenderInterface* interface, RT64::RenderDevice* device) {
+        launcher_menu_controller = recompui::create_launcher_menu();
+        config_menu_controller = recompui::create_config_menu();
+
+        system_interface = std::make_unique<SystemInterface_SDL>();
+        system_interface->SetWindow(window);
+        render_interface.init(interface, device);
+
+        launcher_menu_controller->register_events(event_listener_instancer);
+        config_menu_controller->register_events(event_listener_instancer);
+
+        Rml::SetSystemInterface(system_interface.get());
+        Rml::SetRenderInterface(render_interface.get_rml_interface());
+        Rml::Factory::RegisterEventListenerInstancer(&event_listener_instancer);
+
+        recompui::register_custom_elements();
+
+        Rml::Initialise();
+        
+        // Apply the hack to replace RmlUi's default color parser with one that conforms to HTML5 alpha parsing for SASS compatibility
+        recompui::apply_color_hack();
+
+        int width, height;
+        SDL_GetWindowSizeInPixels(window, &width, &height);
+        
+        context = Rml::CreateContext("main", Rml::Vector2i(width, height));
+        launcher_menu_controller->make_bindings(context);
+        config_menu_controller->make_bindings(context);
+
+        Rml::Debugger::Initialise(context);
+        {
+            struct FontFace {
+                const char* filename;
+                bool fallback_face;
+            };
+            FontFace font_faces[] = {
+                {"LatoLatin-Regular.ttf", false},
+                {"ChiaroNormal.otf", false},
+                {"ChiaroBold.otf", false},
+                {"LatoLatin-Italic.ttf", false},
+                {"LatoLatin-Bold.ttf", false},
+                {"LatoLatin-BoldItalic.ttf", false},
+                {"NotoEmoji-Regular.ttf", true},
+                {"promptfont/promptfont.ttf", false},
+            };
+
+            for (const FontFace& face : font_faces) {
+                auto font = zelda64::get_asset_path(face.filename);
+                Rml::LoadFontFace(font.string(), face.fallback_face);
+            }
+        }
+
+        launcher_menu_controller->load_document(context);
+        config_menu_controller->load_document(context);
+    }
+
+    void unload() {
+        render_interface.reset();
+    }
+
+    void update_primary_input(bool mouse_moved, bool non_mouse_interacted) {
+        mouse_is_active_changed = false;
+        if (non_mouse_interacted) {
+            // controller newly interacted with
+            if (mouse_is_active) {
+                mouse_is_active = false;
+                mouse_is_active_changed = true;
+            }
+        }
+        else if (mouse_moved) {
+            // mouse newly interacted with
+            if (!mouse_is_active) {
+                mouse_is_active = true;
+                mouse_is_active_changed = true;
+            }
+        }
+
+        if (mouse_moved || non_mouse_interacted) {
+            mouse_is_active_initialized = true;
+        }
+
+        if (mouse_is_active_initialized) {
+            recompui::set_cursor_visible(mouse_is_active);
+        }
+
+        Rml::ElementDocument* current_document = top_input_document();
+        if (current_document == nullptr) {
+            return;
+        }
+
+        // TODO is this needed?
+        Rml::Element* window_el = current_document->GetElementById("window");
+        if (window_el != nullptr) {
+            if (mouse_is_active) {
+                if (!window_el->HasAttribute("mouse-active")) {
+                    window_el->SetAttribute("mouse-active", true);
+                }
+            }
+            else if (window_el->HasAttribute("mouse-active")) {
+                window_el->RemoveAttribute("mouse-active");
+            }
+        }
+    }
+
+    void update_focus(bool mouse_moved, bool non_mouse_interacted) {
+        Rml::ElementDocument* current_document = top_input_document();
+
+        if (current_document == nullptr) {
+            return;
+        }
+
+        if (cont_is_active || non_mouse_interacted) {
+            if (non_mouse_interacted && !submenu_is_active) {
+                auto focusedEl = current_document->GetFocusLeafNode();
+                if (focusedEl == nullptr || RecompRml::CanFocusElement(focusedEl) != RecompRml::CanFocus::Yes) {
+                    Rml::Element* element = find_autofocus_element(current_document);
+                    if (element != nullptr) {
+                        element->Focus();
+                    }
+                }
+            }
+            return;
+        }
+
+        // If there was mouse motion, get the current hovered element (or its target if it points to one) and focus that if applicable.
+        if (mouse_is_active) {
+            if (mouse_is_active_changed) {
+                Rml::Element* focused = current_document->GetFocusLeafNode();
+                if (focused) focused->Blur();
+            } else if (mouse_moved) {
+                Rml::Element* hovered = context->GetHoverElement();
+                if (hovered) {
+                    Rml::Element* hover_target = get_target(current_document, hovered);
+                    if (hover_target && can_focus(hover_target)) {
+                        prev_focused = hover_target;
+                    }
+                }
+            }
+        }
+
+        if (!mouse_is_active) {
+            if (!prev_focused || !can_focus(prev_focused)) {
+                // Find the autofocus element in the tab chain
+                Rml::Element* element = find_autofocus_element(current_document);
+                if (element && can_focus(element)) {
+                    prev_focused = element;
+                }
+            }
+
+            if (mouse_is_active_changed && prev_focused && can_focus(prev_focused)) {
+                prev_focused->Focus();
+            }
+        }
+    }
+
+    void show_context(recompui::ContextId context) {
+        if (std::find_if(opened_contexts.begin(), opened_contexts.end(), [context](auto& c){ return c.context == context; }) != opened_contexts.end()) {
+            recompui::message_box("Attemped to show the same context twice");
+            assert(false);
+        }
+        bool takes_input = context.takes_input();
+        Rml::ElementDocument* document = context.get_document();
+        opened_contexts.push_back(ContextDetails{
+            .context = context,
+            .document = document,
+            .takes_input = takes_input
+        });
+
+        document->PullToFront();
+        document->Show();
+    }
+
+    void hide_context(recompui::ContextId context) {
+        auto remove_it = std::remove_if(opened_contexts.begin(), opened_contexts.end(), [context](auto& c) { return c.context == context; });
+        if (remove_it == opened_contexts.end()) {
+            recompui::message_box("Attemped to hide a context that isn't shown");
+            assert(false);
+        }
+        opened_contexts.erase(remove_it, opened_contexts.end());
+
+        context.get_document()->Hide();
+    }
+    
+    void hide_all_contexts() {
+        for (auto& context : opened_contexts) {
+            context.document->Hide();
+        }
+
+        opened_contexts.clear();
+    }
+
+    bool is_context_open(recompui::ContextId context) {
+        return std::find_if(opened_contexts.begin(), opened_contexts.end(), [context](auto& c){ return c.context == context; }) != opened_contexts.end();
+    }
+
+    bool is_context_taking_input() {
+        return std::find_if(opened_contexts.begin(), opened_contexts.end(), [](auto& c){ return c.takes_input; }) != opened_contexts.end();
+    }
+
+    bool is_any_context_open() {
+        return !opened_contexts.empty();
+    }
+
+    Rml::ElementDocument* top_input_document() {
+        // Iterate backwards and stop at the first context that takes input.
+        for (auto it = opened_contexts.rbegin(); it != opened_contexts.rend(); it++) {
+            if (it->takes_input) {
+                return it->document;
+            }
+        }
+        return nullptr;
+    }
+};
+
+std::unique_ptr<UIState> ui_state;
+std::recursive_mutex ui_state_mutex{};
+
+// TODO make this not be global
+extern SDL_Window* window;
+
+void recompui::get_window_size(int& width, int& height) {
+    SDL_GetWindowSizeInPixels(window, &width, &height);
+}
+
+inline const std::string read_file_to_string(std::filesystem::path path) {
+    std::ifstream stream = std::ifstream{path};
+    std::ostringstream ss;
+    ss << stream.rdbuf();
+    return ss.str(); 
+}
+
+void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
+#if defined(__linux__)
+    std::locale::global(std::locale::classic());
+#endif
+    ui_state = std::make_unique<UIState>(window, interface, device);
+}
+
+moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
+
+void recompui::queue_event(const SDL_Event& event) {
+    ui_event_queue.enqueue(event);
+}
+
+bool recompui::try_deque_event(SDL_Event& out) {
+    return ui_event_queue.try_dequeue(out);
+}
+
+int cont_button_to_key(SDL_ControllerButtonEvent& button) {
+    // Configurable accept button in menu
+    auto menuAcceptBinding0 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller);
+    auto menuAcceptBinding1 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller);
+    // note - magic number: 0 is InputType::None
+    if ((menuAcceptBinding0.input_type != 0 && button.button == menuAcceptBinding0.input_id) ||
+        (menuAcceptBinding1.input_type != 0 && button.button == menuAcceptBinding1.input_id)) {
+        return SDLK_RETURN;
+    }
+
+    // Configurable apply button in menu
+    auto menuApplyBinding0 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller);
+    auto menuApplyBinding1 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller);
+    // note - magic number: 0 is InputType::None
+    if ((menuApplyBinding0.input_type != 0 && button.button == menuApplyBinding0.input_id) ||
+        (menuApplyBinding1.input_type != 0 && button.button == menuApplyBinding1.input_id)) {
+        return SDLK_f;
+    } 
+
+    // Allows closing the menu
+    auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
+    auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
+    // note - magic number: 0 is InputType::None
+    if ((menuToggleBinding0.input_type != 0 && button.button == menuToggleBinding0.input_id) ||
+        (menuToggleBinding1.input_type != 0 && button.button == menuToggleBinding1.input_id)) {
+        return SDLK_ESCAPE;
+    }
+
+    switch (button.button) {
+        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP:
+            return SDLK_UP;
+        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN:
+            return SDLK_DOWN;
+        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT:
+            return SDLK_LEFT;
+        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
+            return SDLK_RIGHT;
+    }
+
+    return 0;
+}
+
+
+int cont_axis_to_key(SDL_ControllerAxisEvent& axis, float value) {
+    switch (axis.axis) {
+    case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY:
+        if (value < 0) return SDLK_UP;
+        return SDLK_DOWN;
+    case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX:
+        if (value >= 0) return SDLK_RIGHT;
+        return SDLK_LEFT;
+    }
+    return 0;
+}
+
+void apply_background_input_mode() {
+    static recomp::BackgroundInputMode last_input_mode = recomp::BackgroundInputMode::OptionCount;
+
+    recomp::BackgroundInputMode cur_input_mode = recomp::get_background_input_mode();
+
+    if (last_input_mode != cur_input_mode) {
+        SDL_SetHint(
+            SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
+            cur_input_mode == recomp::BackgroundInputMode::On
+                ? "1"
+                : "0"
+        );
+    }
+    last_input_mode = cur_input_mode;
+}
+
+bool recompui::get_cont_active() {
+    return ui_state->cont_is_active;
+}
+
+void recompui::set_cont_active(bool active) {
+    ui_state->cont_is_active = active;
+}
+
+void recompui::activate_mouse() {
+    ui_state->update_primary_input(true, false);
+    ui_state->update_focus(true, false);
+}
+
+void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* swap_chain_framebuffer) {
+
+    apply_background_input_mode();
+
+    // Return early if the ui context has been destroyed already.
+    if (!ui_state) {
+        return;
+    }
+
+    // Return to the launcher if no menu is open and the game isn't started.
+    if (!recompui::is_any_context_open() && !ultramodern::is_game_started()) {
+        recompui::show_context(recompui::get_launcher_context_id(), "");
+    }
+
+    std::lock_guard lock{ ui_state_mutex };
+
+    SDL_Event cur_event{};
+
+    bool mouse_moved = false;
+    bool mouse_clicked = false;
+    bool non_mouse_interacted = false;
+    bool cont_interacted = false;
+    bool kb_interacted = false;
+
+    bool config_was_open = recompui::is_context_open(recompui::get_config_context_id());
+
+    while (recompui::try_deque_event(cur_event)) {
+        bool context_taking_input = recompui::is_context_taking_input();
+        if (!recomp::all_input_disabled()) {
+            // Implement some additional behavior for specific events on top of what RmlUi normally does with them.
+            switch (cur_event.type) {
+            case SDL_EventType::SDL_MOUSEMOTION: {
+                int *last_mouse_pos = ui_state->last_active_mouse_position;
+
+                if (!ui_state->mouse_is_active) {
+                    float xD = cur_event.motion.x - last_mouse_pos[0];
+                    float yD = cur_event.motion.y - last_mouse_pos[1];
+                    if (sqrt(xD * xD + yD * yD) < 100) {
+                        break;
+                    }
+                }
+                last_mouse_pos[0] = cur_event.motion.x;
+                last_mouse_pos[1] = cur_event.motion.y;
+
+                // if controller is the primary input, don't use mouse movement to allow cursor to reactivate
+                if (recompui::get_cont_active()) {
+                    break;
+                }
+            }
+            // fallthrough
+            case SDL_EventType::SDL_MOUSEBUTTONDOWN:
+                mouse_moved = true;
+                mouse_clicked = true;
+                break;
+                
+            case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
+                int rml_key = cont_button_to_key(cur_event.cbutton);
+                if (context_taking_input && rml_key) {
+                    ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
+                }
+                non_mouse_interacted = true;
+                cont_interacted = true;
+                break;
+            }
+            case SDL_EventType::SDL_KEYDOWN:
+                non_mouse_interacted = true;
+                kb_interacted = true;
+                break;
+            case SDL_EventType::SDL_USEREVENT:
+                if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
+                    ui_state->await_stick_return_y = true;
+                } else if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
+                    ui_state->await_stick_return_x = true;
+                }
+                break;
+            case SDL_EventType::SDL_CONTROLLERAXISMOTION:
+                SDL_ControllerAxisEvent* axis_event = &cur_event.caxis;
+                if (axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY && axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
+                    break;
+                }
+
+                float axis_value = axis_event->value * (1 / 32768.0f);
+                bool* await_stick_return = axis_event->axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY
+                        ? &ui_state->await_stick_return_y
+                        : &ui_state->await_stick_return_x;
+                if (fabsf(axis_value) > 0.5f) {
+                    if (!*await_stick_return) {
+                        *await_stick_return = true;
+                        non_mouse_interacted = true;
+                        int rml_key = cont_axis_to_key(cur_event.caxis, axis_value);
+                        if (context_taking_input && rml_key) {
+                            ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
+                        }
+                    }
+                    non_mouse_interacted = true;
+                    cont_interacted = true;
+                }
+                else if (*await_stick_return && fabsf(axis_value) < 0.15f) {
+                    *await_stick_return = false;
+                }
+                break;
+            }
+
+            if (context_taking_input) {
+                RmlSDL::InputEventHandler(ui_state->context, cur_event);
+            }
+        }
+
+        // If the config menu isn't open and the game has been started and either the escape key or select button are pressed, open the config menu.
+        if (!config_was_open && ultramodern::is_game_started()) {
+            bool open_config = false;
+
+            switch (cur_event.type) {
+            case SDL_EventType::SDL_KEYDOWN:
+                if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
+                    open_config = true;
+                }
+                break;
+            case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
+                auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
+                auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
+                // note - magic number: 0 is InputType::None
+                if ((menuToggleBinding0.input_type != 0 && cur_event.cbutton.button == menuToggleBinding0.input_id) ||
+                    (menuToggleBinding1.input_type != 0 && cur_event.cbutton.button == menuToggleBinding1.input_id)) {
+                    open_config = true;
+                }
+                break;
+            }
+
+            recompui::hide_all_contexts();
+            if (open_config) {
+                recompui::show_context(recompui::get_config_context_id(), "");
+            }
+        }
+    } // end dequeue event loop
+
+    if (cont_interacted || kb_interacted || mouse_clicked) {
+        recompui::set_cont_active(cont_interacted);
+    }
+    recomp::config_menu_set_cont_or_kb(ui_state->cont_is_active);
+
+    recomp::InputField scanned_field = recomp::get_scanned_input();
+    if (scanned_field != recomp::InputField{}) {
+        recomp::finish_scanning_input(scanned_field);
+    }
+
+    ui_state->update_primary_input(mouse_moved, non_mouse_interacted);
+    ui_state->update_focus(mouse_moved, non_mouse_interacted);
+
+    if (recompui::is_any_context_open()) {
+        int width = swap_chain_framebuffer->getWidth();
+        int height = swap_chain_framebuffer->getHeight();
+
+        // Scale the UI based on the window size with 1080 vertical resolution as the reference point.
+        ui_state->context->SetDensityIndependentPixelRatio((height) / 1080.0f);
+
+        ui_state->render_interface.start(command_list, width, height);
+
+        static int prev_width = 0;
+        static int prev_height = 0;
+
+        if (prev_width != width || prev_height != height) {
+            ui_state->context->SetDimensions({ width, height });
+        }
+        prev_width = width;
+        prev_height = height;
+
+        ui_state->context->Update();
+        ui_state->context->Render();
+        ui_state->render_interface.end(command_list, swap_chain_framebuffer);
+    }
+}
+
+void deinit_hook() {
+    recompui::destroy_all_contexts();
+
+    std::lock_guard lock {ui_state_mutex};
+    Rml::Debugger::Shutdown();
+    Rml::Shutdown();
+    ui_state->unload();
+    ui_state.reset();
+}
+
+void recompui::set_render_hooks() {
+    RT64::SetRenderHooks(init_hook, draw_hook, deinit_hook);
+}
+
+void recompui::message_box(const char* msg) {
+    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), msg, nullptr);
+    printf("[ERROR] %s\n", msg);
+}
+
+void recompui::show_context(ContextId context, std::string_view param) {
+    std::lock_guard lock{ui_state_mutex};
+
+    // TODO call the context's on_show callback with the param.
+    ui_state->show_context(context);
+}
+
+void recompui::hide_context(ContextId context) {
+    std::lock_guard lock{ui_state_mutex};
+
+    ui_state->hide_context(context);
+}
+
+void recompui::hide_all_contexts() {
+    std::lock_guard lock{ui_state_mutex};
+
+    if (ui_state) {
+        ui_state->hide_all_contexts();
+    }
+}
+
+bool recompui::is_context_open(ContextId context) {
+    std::lock_guard lock{ui_state_mutex};
+
+    if (!ui_state) {
+        return false;
+    }
+
+    return ui_state->is_context_open(context);
+}
+
+bool recompui::is_context_taking_input() {
+    std::lock_guard lock{ui_state_mutex};
+
+    if (!ui_state) {
+        return false;
+    }
+
+    return ui_state->is_context_taking_input();
+}
+
+bool recompui::is_any_context_open() {
+    std::lock_guard lock{ui_state_mutex};
+
+    if (!ui_state) {
+        return false;
+    }
+
+    return ui_state->is_any_context_open();
+}
+