Implemented mod UI callbacks

This commit is contained in:
Mr-Wiseguy 2025-02-20 02:34:53 -05:00
parent 584903ca18
commit 8f85fb74db
10 changed files with 250 additions and 12 deletions

View File

@ -166,6 +166,7 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_details_panel.cpp ${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_mod_menu.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_api.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/util/hsv.cpp
${CMAKE_SOURCE_DIR}/src/ui/core/ui_context.cpp ${CMAKE_SOURCE_DIR}/src/ui/core/ui_context.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp

View File

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

View File

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

View File

@ -45,3 +45,4 @@ recomp_high_precision_fb_enabled = 0x8F0000A8;
recomp_get_resolution_scale = 0x8F0000AC; recomp_get_resolution_scale = 0x8F0000AC;
recomp_get_analog_inverted_axes = 0x8F0000B0; recomp_get_analog_inverted_axes = 0x8F0000B0;
recomp_get_window_resolution = 0x8F0000B4; recomp_get_window_resolution = 0x8F0000B4;
recomp_run_ui_callbacks = 0x8F0000B8;

9
patches/ui_funcs.h Normal file
View File

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

View File

@ -367,7 +367,7 @@ void recompui::ContextId::process_updates() {
continue; continue;
} }
static_cast<Element*>(cur_resource->get())->process_event(update_event); static_cast<Element*>(cur_resource->get())->handle_event(update_event);
} }
} }

View File

@ -110,7 +110,7 @@ void Element::propagate_disabled(bool disabled) {
base->SetAttribute("disabled", attribute_state); base->SetAttribute("disabled", attribute_state);
if (events_enabled & Events(EventType::Enable)) { if (events_enabled & Events(EventType::Enable)) {
process_event(Event::enable_event(!attribute_state)); handle_event(Event::enable_event(!attribute_state));
} }
for (auto &child : children) { 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) { void Element::ProcessEvent(Rml::Event &event) {
ContextId context = ContextId::null(); ContextId context = ContextId::null();
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument(); Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
@ -134,10 +142,10 @@ void Element::ProcessEvent(Rml::Event &event) {
// Events that are processed during any phase. // Events that are processed during any phase.
switch (event.GetId()) { switch (event.GetId()) {
case Rml::EventId::Mousedown: 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; break;
case Rml::EventId::Drag: 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; break;
default: default:
break; break;
@ -147,28 +155,28 @@ void Element::ProcessEvent(Rml::Event &event) {
if (event.GetPhase() == Rml::EventPhase::Target) { if (event.GetPhase() == Rml::EventPhase::Target) {
switch (event.GetId()) { switch (event.GetId()) {
case Rml::EventId::Mouseover: case Rml::EventId::Mouseover:
process_event(Event::hover_event(true)); handle_event(Event::hover_event(true));
break; break;
case Rml::EventId::Mouseout: case Rml::EventId::Mouseout:
process_event(Event::hover_event(false)); handle_event(Event::hover_event(false));
break; break;
case Rml::EventId::Focus: case Rml::EventId::Focus:
process_event(Event::focus_event(true)); handle_event(Event::focus_event(true));
break; break;
case Rml::EventId::Blur: case Rml::EventId::Blur:
process_event(Event::focus_event(false)); handle_event(Event::focus_event(false));
break; break;
case Rml::EventId::Dragstart: 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; break;
case Rml::EventId::Dragend: 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; break;
case Rml::EventId::Change: { case Rml::EventId::Change: {
if (events_enabled & Events(EventType::Text)) { if (events_enabled & Events(EventType::Text)) {
Rml::Variant *value_variant = base->GetAttribute("value"); Rml::Variant *value_variant = base->GetAttribute("value");
if (value_variant != nullptr) { if (value_variant != nullptr) {
process_event(Event::text_event(value_variant->Get<std::string>())); handle_event(Event::text_event(value_variant->Get<std::string>()));
} }
} }
@ -309,4 +317,8 @@ void Element::queue_update() {
cur_context.queue_element_update(resource_id); 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});
}
} }

View File

@ -3,14 +3,22 @@
#include "ui_style.h" #include "ui_style.h"
#include "../core/ui_context.h" #include "../core/ui_context.h"
#include "recomp.h"
#include <ultramodern/ultra64.h>
#include <unordered_set> #include <unordered_set>
namespace recompui { namespace recompui {
struct UICallback {
PTR(void) callback;
PTR(void) userdata;
};
class ContextId; class ContextId;
class Element : public Style, public Rml::EventListener { class Element : public Style, public Rml::EventListener {
friend ContextId create_context(const std::filesystem::path& path); friend ContextId create_context(const std::filesystem::path& path);
friend ContextId create_context(); 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: private:
Rml::Element *base = nullptr; Rml::Element *base = nullptr;
Rml::ElementPtr base_owning = {}; Rml::ElementPtr base_owning = {};
@ -19,6 +27,7 @@ private:
std::vector<uint32_t> styles_counter; std::vector<uint32_t> styles_counter;
std::unordered_set<std::string_view> style_active_set; std::unordered_set<std::string_view> style_active_set;
std::unordered_multimap<std::string_view, uint32_t> style_name_index_map; std::unordered_multimap<std::string_view, uint32_t> style_name_index_map;
std::vector<UICallback> callbacks;
std::vector<Element *> children; std::vector<Element *> children;
bool shim = false; bool shim = false;
bool enabled = true; bool enabled = true;
@ -30,6 +39,7 @@ private:
void apply_style(Style *style); void apply_style(Style *style);
void apply_styles(); void apply_styles();
void propagate_disabled(bool disabled); void propagate_disabled(bool disabled);
void handle_event(const Event &e);
// Style overrides. // Style overrides.
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override; 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_width();
float get_client_height(); float get_client_height();
void queue_update(); 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 } // namespace recompui

View File

@ -19,6 +19,7 @@
#include "librecomp/overlays.hpp" #include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp" #include "librecomp/helpers.hpp"
#include "ultramodern/error_handling.hpp"
using namespace recompui; using namespace recompui;
@ -696,6 +697,22 @@ void recompui_set_tab_index(uint8_t* rdram, recomp_context* ctx) {
resource->set_tab_index(static_cast<TabIndex>(tab_index)); resource->set_tab_index(static_cast<TabIndex>(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<Element*>(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) #define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_exports() { void recompui::register_ui_exports() {
@ -777,4 +794,5 @@ void recompui::register_ui_exports() {
REGISTER_FUNC(recompui_set_column_gap); REGISTER_FUNC(recompui_set_column_gap);
REGISTER_FUNC(recompui_set_drag); REGISTER_FUNC(recompui_set_drag);
REGISTER_FUNC(recompui_set_tab_index); REGISTER_FUNC(recompui_set_tab_index);
REGISTER_FUNC(recompui_register_callback);
} }

118
src/ui/ui_api_events.cpp Normal file
View File

@ -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<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
struct QueuedCallback {
recompui::ResourceId resource;
recompui::Event event;
recompui::UICallback callback;
};
moodycamel::ConcurrentQueue<QueuedCallback> 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<RecompuiEventType>(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<recompui::EventClick>(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<recompui::EventFocus>(in.variant);
out.data.focus.active = focus.active;
}
break;
case recompui::EventType::Hover:
{
const recompui::EventHover &hover = std::get<recompui::EventHover>(in.variant);
out.data.hover.active = hover.active;
}
break;
case recompui::EventType::Enable:
{
const recompui::EventEnable &enable = std::get<recompui::EventEnable>(in.variant);
out.data.enable.active = enable.active;
}
break;
case recompui::EventType::Drag:
{
const recompui::EventDrag &drag = std::get<recompui::EventDrag>(in.variant);
out.data.drag.phase = static_cast<RecompuiDragPhase>(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<int32_t>(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);
}