diff --git a/CMakeLists.txt b/CMakeLists.txt index ae79ade..f0ef7fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,6 +166,7 @@ set (SOURCES ${CMAKE_SOURCE_DIR}/src/ui/ui_mod_details_panel.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_mod_menu.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_api.cpp + ${CMAKE_SOURCE_DIR}/src/ui/ui_api_events.cpp ${CMAKE_SOURCE_DIR}/src/ui/util/hsv.cpp ${CMAKE_SOURCE_DIR}/src/ui/core/ui_context.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp diff --git a/patches/gamestate_patches.c b/patches/gamestate_patches.c new file mode 100644 index 0000000..d64a632 --- /dev/null +++ b/patches/gamestate_patches.c @@ -0,0 +1,14 @@ +#include "patches.h" +#include "ui_funcs.h" + +// @recomp Patched to run UI callbacks. +RECOMP_PATCH void Graph_UpdateGame(GameState* gameState) { + recomp_run_ui_callbacks(); + + GameState_GetInput(gameState); + GameState_IncrementFrameCount(gameState); + if (SREG(20) < 3) { + Audio_Update(); + } +} + diff --git a/patches/recompui_event_structs.h b/patches/recompui_event_structs.h new file mode 100644 index 0000000..914160d --- /dev/null +++ b/patches/recompui_event_structs.h @@ -0,0 +1,52 @@ +#ifndef __UI_FUNCS_H__ +#define __UI_FUNCS_H__ + +// These two enums must be kept in sync with src/ui/elements/ui_types.h! +typedef enum { + UI_EVENT_NONE, + UI_EVENT_CLICK, + UI_EVENT_FOCUS, + UI_EVENT_HOVER, + UI_EVENT_ENABLE, + UI_EVENT_DRAG, + UI_EVENT_RESERVED1, // Would be UI_EVENT_TEXT but text events aren't usable in mods currently + UI_EVENT_UPDATE, + UI_EVENT_COUNT +} RecompuiEventType; + +typedef enum { + UI_DRAG_NONE, + UI_DRAG_START, + UI_DRAG_MOVE, + UI_DRAG_END +} RecompuiDragPhase; + +typedef struct { + RecompuiEventType type; + union { + struct { + float x; + float y; + } click; + + struct { + bool active; + } focus; + + struct { + bool active; + } hover; + + struct { + bool active; + } enable; + + struct { + float x; + float y; + RecompuiDragPhase phase; + } drag; + } data; +} RecompuiEventData; + +#endif diff --git a/patches/syms.ld b/patches/syms.ld index cd67a44..2e80775 100644 --- a/patches/syms.ld +++ b/patches/syms.ld @@ -45,3 +45,4 @@ recomp_high_precision_fb_enabled = 0x8F0000A8; recomp_get_resolution_scale = 0x8F0000AC; recomp_get_analog_inverted_axes = 0x8F0000B0; recomp_get_window_resolution = 0x8F0000B4; +recomp_run_ui_callbacks = 0x8F0000B8; diff --git a/patches/ui_funcs.h b/patches/ui_funcs.h new file mode 100644 index 0000000..ad6e572 --- /dev/null +++ b/patches/ui_funcs.h @@ -0,0 +1,9 @@ +#ifndef __UI_FUNCS_INTERNAL_H__ +#define __UI_FUNCS_INTERNAL_H__ + +#include "patch_helpers.h" +#include "recompui_event_structs.h" + +DECLARE_FUNC(void, recomp_run_ui_callbacks); + +#endif diff --git a/src/ui/core/ui_context.cpp b/src/ui/core/ui_context.cpp index b9fd1d0..ff457b8 100644 --- a/src/ui/core/ui_context.cpp +++ b/src/ui/core/ui_context.cpp @@ -367,7 +367,7 @@ void recompui::ContextId::process_updates() { continue; } - static_cast(cur_resource->get())->process_event(update_event); + static_cast(cur_resource->get())->handle_event(update_event); } } diff --git a/src/ui/elements/ui_element.cpp b/src/ui/elements/ui_element.cpp index 8a8f470..efb35d7 100644 --- a/src/ui/elements/ui_element.cpp +++ b/src/ui/elements/ui_element.cpp @@ -110,7 +110,7 @@ void Element::propagate_disabled(bool disabled) { base->SetAttribute("disabled", attribute_state); if (events_enabled & Events(EventType::Enable)) { - process_event(Event::enable_event(!attribute_state)); + handle_event(Event::enable_event(!attribute_state)); } for (auto &child : children) { @@ -119,6 +119,14 @@ void Element::propagate_disabled(bool disabled) { } } +void Element::handle_event(const Event& event) { + for (const auto& callback : callbacks) { + recompui::queue_ui_callback(resource_id, event, callback); + } + + process_event(event); +} + void Element::ProcessEvent(Rml::Event &event) { ContextId context = ContextId::null(); Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument(); @@ -134,10 +142,10 @@ void Element::ProcessEvent(Rml::Event &event) { // Events that are processed during any phase. switch (event.GetId()) { case Rml::EventId::Mousedown: - process_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f))); + handle_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f))); break; case Rml::EventId::Drag: - process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move)); + handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move)); break; default: break; @@ -147,28 +155,28 @@ void Element::ProcessEvent(Rml::Event &event) { if (event.GetPhase() == Rml::EventPhase::Target) { switch (event.GetId()) { case Rml::EventId::Mouseover: - process_event(Event::hover_event(true)); + handle_event(Event::hover_event(true)); break; case Rml::EventId::Mouseout: - process_event(Event::hover_event(false)); + handle_event(Event::hover_event(false)); break; case Rml::EventId::Focus: - process_event(Event::focus_event(true)); + handle_event(Event::focus_event(true)); break; case Rml::EventId::Blur: - process_event(Event::focus_event(false)); + handle_event(Event::focus_event(false)); break; case Rml::EventId::Dragstart: - process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start)); + handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start)); break; case Rml::EventId::Dragend: - process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End)); + handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End)); break; case Rml::EventId::Change: { if (events_enabled & Events(EventType::Text)) { Rml::Variant *value_variant = base->GetAttribute("value"); if (value_variant != nullptr) { - process_event(Event::text_event(value_variant->Get())); + handle_event(Event::text_event(value_variant->Get())); } } @@ -309,4 +317,8 @@ void Element::queue_update() { cur_context.queue_element_update(resource_id); } +void Element::register_callback(PTR(void) callback, PTR(void) userdata) { + callbacks.emplace_back(UICallback{.callback = callback, .userdata = userdata}); +} + } \ No newline at end of file diff --git a/src/ui/elements/ui_element.h b/src/ui/elements/ui_element.h index 506f850..319615f 100644 --- a/src/ui/elements/ui_element.h +++ b/src/ui/elements/ui_element.h @@ -3,14 +3,22 @@ #include "ui_style.h" #include "../core/ui_context.h" +#include "recomp.h" +#include + #include namespace recompui { +struct UICallback { + PTR(void) callback; + PTR(void) userdata; +}; + class ContextId; class Element : public Style, public Rml::EventListener { friend ContextId create_context(const std::filesystem::path& path); friend ContextId create_context(); - friend class ContextId; // To allow ContextId to call the process_event method directly. + friend class ContextId; // To allow ContextId to call the handle_event method directly. private: Rml::Element *base = nullptr; Rml::ElementPtr base_owning = {}; @@ -19,6 +27,7 @@ private: std::vector styles_counter; std::unordered_set style_active_set; std::unordered_multimap style_name_index_map; + std::vector callbacks; std::vector children; bool shim = false; bool enabled = true; @@ -30,6 +39,7 @@ private: void apply_style(Style *style); void apply_styles(); void propagate_disabled(bool disabled); + void handle_event(const Event &e); // Style overrides. virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override; @@ -63,6 +73,9 @@ public: float get_client_width(); float get_client_height(); void queue_update(); + void register_callback(PTR(void) callback, PTR(void) userdata); }; +void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback); + } // namespace recompui \ No newline at end of file diff --git a/src/ui/ui_api.cpp b/src/ui/ui_api.cpp index 39f4362..8bab53b 100644 --- a/src/ui/ui_api.cpp +++ b/src/ui/ui_api.cpp @@ -19,6 +19,7 @@ #include "librecomp/overlays.hpp" #include "librecomp/helpers.hpp" +#include "ultramodern/error_handling.hpp" using namespace recompui; @@ -696,6 +697,22 @@ void recompui_set_tab_index(uint8_t* rdram, recomp_context* ctx) { resource->set_tab_index(static_cast(tab_index)); } +void recompui_register_callback(uint8_t* rdram, recomp_context* ctx) { + Style* resource = arg_style<0>(rdram, ctx); + + if (!resource->is_element()) { + recompui::message_box("Fatal error in mod - attempted to register callback on non-element"); + assert(false); + ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__); + } + + Element* element = static_cast(resource); + PTR(void) callback = _arg<1, PTR(void)>(rdram, ctx); + PTR(void) userdata = _arg<2, PTR(void)>(rdram, ctx); + + element->register_callback(callback, userdata); +} + #define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name) void recompui::register_ui_exports() { @@ -777,4 +794,5 @@ void recompui::register_ui_exports() { REGISTER_FUNC(recompui_set_column_gap); REGISTER_FUNC(recompui_set_drag); REGISTER_FUNC(recompui_set_tab_index); + REGISTER_FUNC(recompui_register_callback); } diff --git a/src/ui/ui_api_events.cpp b/src/ui/ui_api_events.cpp new file mode 100644 index 0000000..c9b5014 --- /dev/null +++ b/src/ui/ui_api_events.cpp @@ -0,0 +1,118 @@ +#include "concurrentqueue.h" + +#include "recomp_ui.h" + +#include "core/ui_context.h" +#include "core/ui_resource.h" + +#include "elements/ui_element.h" +#include "elements/ui_button.h" +#include "elements/ui_clickable.h" +#include "elements/ui_container.h" +#include "elements/ui_image.h" +#include "elements/ui_label.h" +#include "elements/ui_radio.h" +#include "elements/ui_scroll_container.h" +#include "elements/ui_slider.h" +#include "elements/ui_style.h" +#include "elements/ui_text_input.h" +#include "elements/ui_toggle.h" +#include "elements/ui_types.h" + +#include "librecomp/overlays.hpp" +#include "librecomp/helpers.hpp" + +#include "../patches/ui_funcs.h" + +template +struct overloaded : Ts... { using Ts::operator()...; }; +template +overloaded(Ts...) -> overloaded; + +struct QueuedCallback { + recompui::ResourceId resource; + recompui::Event event; + recompui::UICallback callback; +}; + +moodycamel::ConcurrentQueue queued_callbacks{}; + +void recompui::queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback) { + queued_callbacks.enqueue(QueuedCallback{ .resource = resource, .event = e, .callback = callback }); +} + +bool convert_event(const recompui::Event& in, RecompuiEventData& out) { + bool skip = false; + out = {}; + out.type = static_cast(in.type); + + switch (in.type) { + default: + case recompui::EventType::None: + case recompui::EventType::Count: + skip = true; + break; + case recompui::EventType::Click: + { + const recompui::EventClick &click = std::get(in.variant); + out.data.click.x = click.x; + out.data.click.y = click.y; + } + break; + case recompui::EventType::Focus: + { + const recompui::EventFocus &focus = std::get(in.variant); + out.data.focus.active = focus.active; + } + break; + case recompui::EventType::Hover: + { + const recompui::EventHover &hover = std::get(in.variant); + out.data.hover.active = hover.active; + } + break; + case recompui::EventType::Enable: + { + const recompui::EventEnable &enable = std::get(in.variant); + out.data.enable.active = enable.active; + } + break; + case recompui::EventType::Drag: + { + const recompui::EventDrag &drag = std::get(in.variant); + out.data.drag.phase = static_cast(drag.phase); + out.data.drag.x = drag.x; + out.data.drag.y = drag.y; + } + break; + case recompui::EventType::Text: + skip = true; // Text events aren't supported in the UI mod API. + break; + case recompui::EventType::Update: + // No data for an update event. + break; + } + + return !skip; +} + +extern "C" void recomp_run_ui_callbacks(uint8_t* rdram, recomp_context* ctx) { + // Allocate the event on the stack. + gpr stack_frame = ctx->r29; + ctx->r29 -= sizeof(RecompuiEventData); + RecompuiEventData* event_data = TO_PTR(RecompuiEventData, stack_frame); + + QueuedCallback cur_callback; + + while (queued_callbacks.try_dequeue(cur_callback)) { + if (convert_event(cur_callback.event, *event_data)) { + ctx->r4 = static_cast(cur_callback.resource.slot_id); + ctx->r5 = stack_frame; + ctx->r6 = cur_callback.callback.userdata; + + LOOKUP_FUNC(cur_callback.callback.callback)(rdram, ctx); + } + } + + ctx->r29 += sizeof(RecompuiEventData); +}