Refactor Rml document handling to use new ContextId system (prompts currently unimplemented)

This commit is contained in:
Mr-Wiseguy 2025-01-19 21:00:05 -05:00
parent 0312439dda
commit a087731f96
11 changed files with 976 additions and 1222 deletions

View file

@ -164,6 +164,7 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/game/rom_decompression.cpp ${CMAKE_SOURCE_DIR}/src/game/rom_decompression.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_renderer.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_launcher.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_config.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config_sub_menu.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_config_sub_menu.cpp

View file

@ -33,7 +33,7 @@
</head> </head>
<body class="window"> <body class="window">
<!-- <handle move_target="#document"> --> <!-- <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" onclick="close_config_menu_backdrop">
<div class="centered-page__modal"> <div class="centered-page__modal">
<tabset class="tabs" id="config_tabset"> <tabset class="tabs" id="config_tabset">
@ -118,7 +118,7 @@
<label><span style="font-family:promptfont;">&#x21A7;</span> Accept</label> --> <label><span style="font-family:promptfont;">&#x21A7;</span> Accept</label> -->
</div> </div>
</div> </div>
<div <!-- <div
id="prompt-root" id="prompt-root"
data-model="prompt_model" data-model="prompt_model"
data-if="prompt__open" data-if="prompt__open"
@ -130,7 +130,7 @@
data-event-click="prompt__on_click" data-event-click="prompt__on_click"
> >
<template src="prompt"/> <template src="prompt"/>
</div> </div> -->
</div> </div>
<!-- </handle> --> <!-- </handle> -->
<!-- <handle size_target="#document" style="position: absolute; width: 16dp; height: 16dp; bottom: 0px; right: 0px; cursor: resize;"></handle> --> <!-- <handle size_target="#document" style="position: absolute; width: 16dp; height: 16dp; bottom: 0px; right: 0px; cursor: resize;"></handle> -->

View file

@ -3,6 +3,9 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <string_view>
// TODO move this file into src/ui
#include "SDL.h" #include "SDL.h"
#include "RmlUi/Core.h" #include "RmlUi/Core.h"
@ -10,6 +13,8 @@
#include "../src/ui/util/hsv.h" #include "../src/ui/util/hsv.h"
#include "../src/ui/util/bem.h" #include "../src/ui/util/bem.h"
#include "../src/ui/core/ui_context.h"
namespace Rml { namespace Rml {
class ElementDocument; class ElementDocument;
class EventListenerInstancer; class EventListenerInstancer;
@ -20,6 +25,7 @@ namespace Rml {
namespace recompui { namespace recompui {
class UiEventListenerInstancer; class UiEventListenerInstancer;
// TODO remove this once the UI has been ported over to the new system.
class MenuController { class MenuController {
public: public:
virtual ~MenuController() {} virtual ~MenuController() {}
@ -39,24 +45,16 @@ namespace recompui {
std::unique_ptr<UiEventListenerInstancer> make_event_listener_instancer(); std::unique_ptr<UiEventListenerInstancer> make_event_listener_instancer();
void register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler); void register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler);
enum class Menu { void show_context(ContextId context, std::string_view param);
Launcher, void hide_context(ContextId context);
Config, void hide_all_contexts();
None bool is_context_open(ContextId context);
}; bool is_context_taking_input();
bool is_any_context_open();
void set_current_menu(Menu menu); ContextId get_launcher_context_id();
Menu get_current_menu(); ContextId get_config_context_id();
ContextId get_close_prompt_context_id();
enum class ConfigSubmenu {
General,
Controls,
Graphics,
Audio,
Mods,
Debug,
Count
};
enum class ButtonVariant { enum class ButtonVariant {
Primary, Primary,
@ -68,38 +66,6 @@ namespace recompui {
NumVariants, NumVariants,
}; };
void set_config_submenu(ConfigSubmenu submenu);
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();
extern const std::unordered_map<ButtonVariant, std::string> button_variants;
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;
std::string returnElementId = "";
bool open = false;
bool shouldFocus = false;
bool focusOnCancel = true;
PromptContext() = default;
void close_prompt();
void open_prompt( void open_prompt(
const std::string& headerText, const std::string& headerText,
const std::string& contentText, const std::string& contentText,
@ -112,12 +78,13 @@ namespace recompui {
bool _focusOnCancel = true, bool _focusOnCancel = true,
const std::string& _returnElementId = "" const std::string& _returnElementId = ""
); );
void on_confirm(void); bool is_prompt_open();
void on_cancel(void);
void on_click(Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs);
};
PromptContext *get_prompt_context(void); 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();
bool get_cont_active(void); bool get_cont_active(void);
void set_cont_active(bool active); void set_cont_active(bool active);

View file

@ -103,7 +103,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
SDL_KeyboardEvent* keyevent = &event->key; SDL_KeyboardEvent* keyevent = &event->key;
// Skip repeated events when not in the menu // 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) { event->key.repeat) {
break; break;
} }
@ -156,8 +156,9 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
return true; return true;
} }
if (recompui::get_current_menu() != recompui::Menu::Config) { recompui::ContextId config_context_id = recompui::get_config_context_id();
recompui::set_current_menu(recompui::Menu::Config); if (!recompui::is_context_open(config_context_id)) {
recompui::show_context(config_context_id, "");
} }
zelda64::open_quit_game_prompt(); zelda64::open_quit_game_prompt();
@ -711,8 +712,8 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
} }
bool recomp::game_input_disabled() { bool recomp::game_input_disabled() {
// Disable input if any menu is open. // Disable input if any menu that blocks input is open.
return recompui::get_current_menu() != recompui::Menu::None; return recompui::is_any_context_open();
} }
bool recomp::all_input_disabled() { bool recomp::all_input_disabled() {

View file

@ -5,6 +5,7 @@
#include "slot_map.h" #include "slot_map.h"
#include "ultramodern/error_handling.hpp" #include "ultramodern/error_handling.hpp"
#include "recomp_ui.h"
#include "ui_context.h" #include "ui_context.h"
#include "../elements/ui_element.h" #include "../elements/ui_element.h"
@ -62,8 +63,7 @@ enum class ContextErrorType {
DestroyResourceWithoutOpen, DestroyResourceWithoutOpen,
DestroyResourceInWrongContext, DestroyResourceInWrongContext,
DestroyResourceNotFound, DestroyResourceNotFound,
GetDocumentWithoutOpen, GetDocumentInvalidContext,
GetDocumentInWrongContext,
}; };
enum class SlotTag : uint8_t { enum class SlotTag : uint8_t {
@ -116,11 +116,8 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
case ContextErrorType::DestroyResourceNotFound: case ContextErrorType::DestroyResourceNotFound:
error_message = "Attempted to destroy a UI resource that doesn't exist in the current context"; error_message = "Attempted to destroy a UI resource that doesn't exist in the current context";
break; break;
case ContextErrorType::GetDocumentWithoutOpen: case ContextErrorType::GetDocumentInvalidContext:
error_message = "Attempted to get the current UI context's document with no open UI context"; error_message = "Attempted to get the document of an invalid UI context";
break;
case ContextErrorType::GetDocumentInWrongContext:
error_message = "Attempted to get the document of a UI context that's not open";
break; break;
default: default:
error_message = "Unknown UI context error"; 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 // 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. // 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); assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__); 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) { recompui::ContextId recompui::create_context(Rml::Context* rml_context, const std::filesystem::path& path) {
ContextId new_context = create_context_impl(nullptr); ContextId new_context = create_context_impl(nullptr);
auto workingdir = std::filesystem::current_path();
new_context.open(); new_context.open();
Rml::ElementDocument* doc = rml_context->LoadDocument(path.string()); Rml::ElementDocument* doc = rml_context->LoadDocument(path.string());
opened_context->document = doc; opened_context->document = doc;
@ -376,31 +375,25 @@ void recompui::ContextId::clear_children() {
} }
Rml::ElementDocument* recompui::ContextId::get_document() { Rml::ElementDocument* recompui::ContextId::get_document() {
// Ensure a context is currently opened by this thread. std::lock_guard lock{ context_state.all_contexts_lock };
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::GetDocumentWithoutOpen); 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. return ctx->document;
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::GetDocumentInWrongContext);
}
return opened_context->document;
} }
recompui::Element* recompui::ContextId::get_root_element() { recompui::Element* recompui::ContextId::get_root_element() {
// Ensure a context is currently opened by this thread. std::lock_guard lock{ context_state.all_contexts_lock };
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::GetDocumentWithoutOpen); 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. return &ctx->root_element;
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::GetDocumentInWrongContext);
}
return &opened_context->root_element;
} }
recompui::ContextId recompui::get_current_context() { recompui::ContextId recompui::get_current_context() {

View file

@ -43,6 +43,9 @@ namespace recompui {
void close(); void close();
static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; } 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); ContextId create_context(Rml::Context*, const std::filesystem::path& path);

View file

@ -19,87 +19,6 @@ Rml::DataModelHandle controls_model_handle;
Rml::DataModelHandle graphics_model_handle; Rml::DataModelHandle graphics_model_handle;
Rml::DataModelHandle sound_options_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 // True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
bool configuring_controller = false; bool configuring_controller = false;
@ -213,11 +132,10 @@ void recomp::config_menu_set_cont_or_kb(bool cont_interacted) {
void close_config_menu_impl() { void close_config_menu_impl() {
zelda64::save_config(); zelda64::save_config();
if (ultramodern::is_game_started()) { recompui::hide_context(recompui::get_config_context_id());
recompui::set_current_menu(recompui::Menu::None);
} if (!ultramodern::is_game_started()) {
else { recompui::show_context(recompui::get_launcher_context_id(), "");
recompui::set_current_menu(recompui::Menu::Launcher);
} }
} }
@ -239,7 +157,7 @@ void apply_graphics_config(void) {
void close_config_menu() { void close_config_menu() {
if (ultramodern::renderer::get_graphics_config() != new_options) { if (ultramodern::renderer::get_graphics_config() != new_options) {
prompt_context.open_prompt( recompui::open_prompt(
"Graphics options have changed", "Graphics options have changed",
"Would you like to apply or discard the changes?", "Would you like to apply or discard the changes?",
"Apply", "Apply",
@ -266,7 +184,7 @@ void close_config_menu() {
} }
void zelda64::open_quit_game_prompt() { void zelda64::open_quit_game_prompt() {
prompt_context.open_prompt( recompui::open_prompt(
"Are you sure you want to quit?", "Are you sure you want to quit?",
"Any progress since your last save will be lost.", "Any progress since your last save will be lost.",
"Quit", "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; DebugContext debug_context;
recompui::ContextId config_context;
recompui::ContextId recompui::get_config_context_id() {
return config_context;
}
class ConfigMenu : public recompui::MenuController { class ConfigMenu : public recompui::MenuController {
private:
recompui::ContextId config_context;
public: public:
ConfigMenu() { ConfigMenu() {
@ -525,9 +436,7 @@ public:
} }
Rml::ElementDocument* load_document(Rml::Context* context) override { Rml::ElementDocument* load_document(Rml::Context* context) override {
config_context = recompui::create_context(context, zelda64::get_asset_path("config_menu.rml")); config_context = recompui::create_context(context, zelda64::get_asset_path("config_menu.rml"));
config_context.open();
Rml::ElementDocument* ret = config_context.get_document(); Rml::ElementDocument* ret = config_context.get_document();
config_context.close();
return ret; return ret;
} }
void register_events(recompui::UiEventListenerInstancer& listener) override { void register_events(recompui::UiEventListenerInstancer& listener) override {
@ -538,7 +447,7 @@ public:
}); });
recompui::register_event(listener, "config_keydown", recompui::register_event(listener, "config_keydown",
[](const std::string& param, Rml::Event& event) { [](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); auto key = event.GetParameter<Rml::Input::KeyIdentifier>("key_identifier", Rml::Input::KeyIdentifier::KI_UNKNOWN);
switch (key) { switch (key) {
case Rml::Input::KeyIdentifier::KI_ESCAPE: case Rml::Input::KeyIdentifier::KI_ESCAPE:
@ -997,34 +906,15 @@ public:
debug_context.model_handle = constructor.GetModelHandle(); 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 { void make_bindings(Rml::Context* context) override {
// initially set cont state for ui help // 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_nav_help_bindings(context);
make_general_bindings(context); make_general_bindings(context);
make_controls_bindings(context); make_controls_bindings(context);
make_graphics_bindings(context); make_graphics_bindings(context);
make_sound_options_bindings(context); make_sound_options_bindings(context);
make_debug_bindings(context); make_debug_bindings(context);
make_prompt_bindings(context);
} }
}; };
@ -1059,3 +949,24 @@ void recompui::toggle_fullscreen() {
apply_graphics_config(); apply_graphics_config();
graphics_model_handle.DirtyVariable("wm_option"); 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;
}

View file

@ -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 { class LauncherMenu : public recompui::MenuController {
public: public:
LauncherMenu() { LauncherMenu() {
@ -57,8 +63,9 @@ public:
} }
Rml::ElementDocument* load_document(Rml::Context* context) override { Rml::ElementDocument* load_document(Rml::Context* context) override {
const std::filesystem::path asset = zelda64::get_asset_path("launcher.rml"); launcher_context = recompui::create_context(context, zelda64::get_asset_path("launcher.rml"));
return context->LoadDocument(asset.string()); Rml::ElementDocument* ret = launcher_context.get_document();
return ret;
} }
void register_events(recompui::UiEventListenerInstancer& listener) override { void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom", recompui::register_event(listener, "select_rom",
@ -75,25 +82,25 @@ public:
recompui::register_event(listener, "start_game", recompui::register_event(listener, "start_game",
[](const std::string& param, Rml::Event& event) { [](const std::string& param, Rml::Event& event) {
recomp::start_game(supported_games[0].game_id); 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", recompui::register_event(listener, "open_controls",
[](const std::string& param, Rml::Event& event) { [](const std::string& param, Rml::Event& event) {
recompui::set_current_menu(recompui::Menu::Config); recompui::hide_all_contexts();
recompui::set_config_submenu(recompui::ConfigSubmenu::Controls); recompui::show_context(recompui::get_config_context_id(), "controls");
} }
); );
recompui::register_event(listener, "open_settings", recompui::register_event(listener, "open_settings",
[](const std::string& param, Rml::Event& event) { [](const std::string& param, Rml::Event& event) {
recompui::set_current_menu(recompui::Menu::Config); recompui::hide_all_contexts();
recompui::set_config_submenu(recompui::ConfigSubmenu::General); recompui::show_context(recompui::get_config_context_id(), "general");
} }
); );
recompui::register_event(listener, "open_mods", recompui::register_event(listener, "open_mods",
[](const std::string &param, Rml::Event &event) { [](const std::string &param, Rml::Event &event) {
recompui::set_current_menu(recompui::Menu::Config); recompui::hide_all_contexts();
recompui::set_config_submenu(recompui::ConfigSubmenu::Mods); recompui::show_context(recompui::get_config_context_id(), "mods");
} }
); );
recompui::register_event(listener, "exit_game", recompui::register_event(listener, "exit_game",

File diff suppressed because it is too large Load diff

35
src/ui/ui_renderer.h Normal file
View file

@ -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

745
src/ui/ui_state.cpp Normal file
View file

@ -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();
}