diff --git a/src/ui/core/ui_context.cpp b/src/ui/core/ui_context.cpp index c470d77..5271eb6 100644 --- a/src/ui/core/ui_context.cpp +++ b/src/ui/core/ui_context.cpp @@ -1,3 +1,466 @@ +#include +#include +#include + #include "slot_map.h" -#include "ui_context.hpp" +#include "ultramodern/error_handling.hpp" +#include "ui_context.h" +#include "../elements/ui_element.h" + +// Hash implementations for ContextId and ResourceId. +template <> +struct std::hash { + std::size_t operator()(const recompui::ContextId& id) const { + return std::hash()(id.slot_id); + } +}; + +template <> +struct std::hash { + std::size_t operator()(const recompui::ResourceId& id) const { + return std::hash()(id.slot_id); + } +}; + +using resource_slotmap = dod::slot_map32>; + +namespace recompui { + struct Context { + std::mutex context_lock; + resource_slotmap resources; + Rml::ElementDocument* document; + Element root_element; + std::vector loose_elements; + Context(Rml::ElementDocument* document) : document(document), root_element(document) {} + }; +} // namespace recompui + +using context_slotmap = dod::slot_map32; + +static struct { + std::mutex all_contexts_lock; + context_slotmap all_contexts; + std::unordered_set opened_contexts; + std::unordered_map documents_to_contexts; +} context_state; + +thread_local recompui::Context* opened_context = nullptr; +thread_local recompui::ContextId opened_context_id = recompui::ContextId::null(); + +enum class ContextErrorType { + OpenWithoutClose, + OpenInvalidContext, + CloseWithoutOpen, + CloseWrongContext, + DestroyInvalidContext, + GetContextWithoutOpen, + AddResourceWithoutOpen, + AddResourceToWrongContext, + GetResourceWithoutOpen, + GetResourceFailed, + DestroyResourceWithoutOpen, + DestroyResourceInWrongContext, + DestroyResourceNotFound, + GetDocumentWithoutOpen, + GetDocumentInWrongContext, +}; + +enum class SlotTag : uint8_t { + Style = 0, + Element = 1, +}; + +void context_error(recompui::ContextId id, ContextErrorType type) { + (void)id; + + const char* error_message = ""; + + switch (type) { + case ContextErrorType::OpenWithoutClose: + error_message = "Attempted to open a UI context without closing another UI context"; + break; + case ContextErrorType::OpenInvalidContext: + error_message = "Attempted to open an invalid UI context"; + break; + case ContextErrorType::CloseWithoutOpen: + error_message = "Attempted to close a UI context without one being open"; + break; + case ContextErrorType::CloseWrongContext: + error_message = "Attempted to close a different UI context than the one that's open"; + break; + case ContextErrorType::DestroyInvalidContext: + error_message = "Attempted to destroy an invalid UI element"; + break; + case ContextErrorType::GetContextWithoutOpen: + error_message = "Attempted to get the current UI context with no UI context open"; + break; + case ContextErrorType::AddResourceWithoutOpen: + error_message = "Attempted to create a UI resource with no open UI context"; + break; + case ContextErrorType::AddResourceToWrongContext: + error_message = "Attempted to create a UI resource in a different UI context than the one that's open"; + break; + case ContextErrorType::GetResourceWithoutOpen: + error_message = "Attempted to get a UI resource with no open UI context"; + break; + case ContextErrorType::GetResourceFailed: + error_message = "Failed to get a UI resource from the current open UI context"; + break; + case ContextErrorType::DestroyResourceWithoutOpen: + error_message = "Attempted to destroy a UI resource with no open UI context"; + break; + case ContextErrorType::DestroyResourceInWrongContext: + error_message = "Attempted to destroy a UI resource in a different UI context than the one that's open"; + break; + 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"; + break; + default: + error_message = "Unknown UI context error"; + break; + } + + // 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()); + assert(false); + ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__); +} + +recompui::ContextId create_context_impl(Rml::ElementDocument* document) { + static Rml::ElementDocument dummy_document{""}; + bool add_to_dict = true; + + if (document == nullptr) { + document = &dummy_document; + add_to_dict = false; + } + + recompui::ContextId ret; + { + std::lock_guard lock{ context_state.all_contexts_lock }; + ret = { context_state.all_contexts.emplace(document).raw }; + + if (add_to_dict) { + context_state.documents_to_contexts.emplace(document, ret); + } + } + + return ret; +} + +recompui::ContextId recompui::create_context(Rml::Context* rml_context, const std::filesystem::path& path) { + ContextId new_context = create_context_impl(nullptr); + + new_context.open(); + Rml::ElementDocument* doc = rml_context->LoadDocument(path.string()); + opened_context->document = doc; + opened_context->root_element.base = doc; + new_context.close(); + + { + std::lock_guard lock{ context_state.all_contexts_lock }; + context_state.documents_to_contexts.emplace(doc, new_context); + } + + return new_context; +} + +recompui::ContextId recompui::create_context(Rml::ElementDocument* document) { + assert(document != nullptr); + + return create_context_impl(document); +} + +void recompui::destroy_context(ContextId id) { + bool existed = false; + + // TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping + // from the slotmap? + + // Check if the provided id exists. + { + std::lock_guard lock{ context_state.all_contexts_lock }; + // Check if the target context is currently open. + existed = context_state.all_contexts.has_key(context_slotmap::key{ id.slot_id }); + } + + + // Raise an error if the context didn't exist. + if (!existed) { + context_error(id, ContextErrorType::DestroyInvalidContext); + } + + id.open(); + id.clear_children(); + id.close(); + + // Delete the provided id. + { + std::lock_guard lock{ context_state.all_contexts_lock }; + context_state.all_contexts.erase(context_slotmap::key{ id.slot_id }); + } +} + +void recompui::destroy_all_contexts() { + std::lock_guard lock{ context_state.all_contexts_lock }; + + // TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping + // from the slotmap + + std::vector keys{}; + for (const auto& [key, item] : context_state.all_contexts.items()) { + keys.push_back(key); + } + + for (auto key : keys) { + Context* ctx = context_state.all_contexts.get(key); + + std::lock_guard context_lock{ ctx->context_lock }; + opened_context = ctx; + opened_context_id = ContextId{ key }; + + opened_context_id.clear_children(); + + opened_context = nullptr; + opened_context_id = ContextId::null(); + } + + context_state.all_contexts.reset(); + context_state.documents_to_contexts.clear(); +} + +void recompui::ContextId::open() { + // Ensure no other context is opened by this thread already. + if (opened_context_id != ContextId::null()) { + context_error(*this, ContextErrorType::OpenWithoutClose); + } + + // Get the context with this id. + Context* ctx; + { + std::lock_guard lock{ context_state.all_contexts_lock }; + ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id }); + // If the context was found, add it to the opened contexts. + if (ctx != nullptr) { + context_state.opened_contexts.emplace(*this); + } + } + + // Check if the context exists. + if (ctx == nullptr) { + context_error(*this, ContextErrorType::OpenInvalidContext); + } + + // Take ownership of the target context. + ctx->context_lock.lock(); + opened_context = ctx; + opened_context_id = *this; +} + +void recompui::ContextId::close() { + // Ensure a context is currently opened by this thread. + if (opened_context_id == ContextId::null()) { + context_error(*this, ContextErrorType::CloseWithoutOpen); + } + + // Check that the context that was specified is the same one that's currently open. + if (*this != opened_context_id) { + context_error(*this, ContextErrorType::CloseWrongContext); + } + + // Release ownership of the target context. + opened_context->context_lock.unlock(); + opened_context = nullptr; + opened_context_id = ContextId::null(); + + // Remove this context from the opened contexts. + { + std::lock_guard lock{ context_state.all_contexts_lock }; + context_state.opened_contexts.erase(*this); + } +} + +recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr