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;">↧</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 ¶m, 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(); +} +