Update codebase with changes for Zelda 64: Recompiled 1.2

This commit is contained in:
Mr-Wiseguy
2025-06-03 01:14:09 -04:00
parent 6a10095c88
commit 1e19dad587
71 changed files with 4913 additions and 674 deletions
+174 -14
View File
@@ -1,8 +1,10 @@
#include <mutex>
#include <string>
#include <unordered_map>
#include <fstream>
#include "slot_map.h"
#include "RmlUi/Core/StreamMemory.h"
#include "ultramodern/error_handling.hpp"
#include "recomp_ui.h"
@@ -32,8 +34,12 @@ namespace recompui {
resource_slotmap resources;
Rml::ElementDocument* document;
Element root_element;
Element* autofocus_element = nullptr;
std::vector<Element*> loose_elements;
std::unordered_set<ResourceId> to_update;
std::vector<std::tuple<Element*, ResourceId, std::string>> to_set_text;
bool captures_input = true;
bool captures_mouse = true;
Context(Rml::ElementDocument* document) : document(document), root_element(document) {}
};
} // namespace recompui
@@ -41,10 +47,11 @@ namespace recompui {
using context_slotmap = dod::slot_map32<recompui::Context>;
static struct {
std::mutex all_contexts_lock;
std::recursive_mutex all_contexts_lock;
context_slotmap all_contexts;
std::unordered_set<recompui::ContextId> opened_contexts;
std::unordered_map<Rml::ElementDocument*, recompui::ContextId> documents_to_contexts;
Rml::SharedPtr<Rml::StyleSheetContainer> style_sheet;
} context_state;
thread_local recompui::Context* opened_context = nullptr;
@@ -61,12 +68,16 @@ enum class ContextErrorType {
AddResourceToWrongContext,
UpdateElementWithoutContext,
UpdateElementInWrongContext,
SetTextElementWithoutContext,
SetTextElementInWrongContext,
GetResourceWithoutOpen,
GetResourceFailed,
DestroyResourceWithoutOpen,
DestroyResourceInWrongContext,
DestroyResourceNotFound,
GetDocumentInvalidContext,
GetAutofocusInvalidContext,
SetAutofocusInvalidContext,
InternalError,
};
@@ -111,6 +122,12 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
case ContextErrorType::UpdateElementInWrongContext:
error_message = "Attempted to update a UI element in a different UI context than the one that's open";
break;
case ContextErrorType::SetTextElementWithoutContext:
error_message = "Attempted to set the text of a UI element with no open UI context";
break;
case ContextErrorType::SetTextElementInWrongContext:
error_message = "Attempted to set the text of a UI element 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;
@@ -129,6 +146,12 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
case ContextErrorType::GetDocumentInvalidContext:
error_message = "Attempted to get the document of an invalid UI context";
break;
case ContextErrorType::GetAutofocusInvalidContext:
error_message = "Attempted to get the autofocus element of an invalid UI context";
break;
case ContextErrorType::SetAutofocusInvalidContext:
error_message = "Attempted to set the autofocus element of an invalid UI context";
break;
case ContextErrorType::InternalError:
error_message = "Internal error in UI context";
break;
@@ -166,6 +189,21 @@ recompui::ContextId create_context_impl(Rml::ElementDocument* document) {
return ret;
}
void recompui::init_styling(const std::filesystem::path& rcss_file) {
std::string style{};
{
std::ifstream style_stream{rcss_file};
style_stream.seekg(0, std::ios::end);
style.resize(style_stream.tellg());
style_stream.seekg(0, std::ios::beg);
style_stream.read(style.data(), style.size());
}
std::unique_ptr<Rml::StreamMemory> rml_stream = std::make_unique<Rml::StreamMemory>(reinterpret_cast<Rml::byte*>(style.data()), style.size());
rml_stream->SetSourceURL(rcss_file.filename().string());
context_state.style_sheet = Rml::Factory::InstanceStyleSheetStream(rml_stream.get());
}
recompui::ContextId recompui::create_context(const std::filesystem::path& path) {
ContextId new_context = create_context_impl(nullptr);
@@ -193,29 +231,20 @@ recompui::ContextId recompui::create_context(Rml::ElementDocument* document) {
recompui::ContextId recompui::create_context() {
Rml::ElementDocument* doc = create_empty_document();
doc->SetStyleSheetContainer(context_state.style_sheet);
ContextId ret = create_context_impl(doc);
Element* root = ret.get_root_element();
// Mark the root element as not being a shim, as that's only needed for elements that were parented to Rml ones manually.
root->shim = false;
// TODO move these defaults elsewhere. Copied from the existing rcss.
ret.open();
root->set_width(100.0f, Unit::Percent);
root->set_height(100.0f, Unit::Percent);
root->set_display(Display::Flex);
root->set_opacity(1.0f);
root->set_font_family("LatoLatin");
root->set_font_style(FontStyle::Normal);
root->set_font_weight(400);
float sz = 16.0f;
float spacing = 0.0f;
float sz_add = sz + 4;
root->set_font_size(sz_add, Unit::Dp);
root->set_letter_spacing(sz_add * spacing, Unit::Dp);
root->set_line_height(sz_add, Unit::Dp);
ret.close();
doc->Hide();
return ret;
}
@@ -307,6 +336,15 @@ void recompui::ContextId::open() {
opened_context_id = *this;
}
bool recompui::ContextId::open_if_not_already() {
if (opened_context_id == *this) {
return false;
}
open();
return true;
}
void recompui::ContextId::close() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
@@ -330,6 +368,15 @@ void recompui::ContextId::close() {
}
}
recompui::ContextId recompui::try_close_current_context() {
if (opened_context_id != ContextId::null()) {
ContextId prev_context = opened_context_id;
opened_context_id.close();
return prev_context;
}
return ContextId::null();
}
void recompui::ContextId::process_updates() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
@@ -367,8 +414,83 @@ void recompui::ContextId::process_updates() {
continue;
}
static_cast<Element*>(cur_resource->get())->process_event(update_event);
static_cast<Element*>(cur_resource->get())->handle_event(update_event);
}
std::vector<std::tuple<Element*, ResourceId, std::string>> to_set_text = std::move(opened_context->to_set_text);
// Delete the Rml elements that are pending deletion.
for (auto cur_text_update : to_set_text) {
Element* element_ptr = std::get<0>(cur_text_update);
ResourceId resource = std::get<1>(cur_text_update);
std::string& text = std::get<2>(cur_text_update);
// If the resource ID is valid, prefer that as we can quickly validate if the resource still exists.
if (resource != ResourceId::null()) {
resource_slotmap::key cur_key{ resource.slot_id };
std::unique_ptr<Style>* cur_resource = opened_context->resources.get(cur_key);
// Make sure the resource exists before setting its text, as it may have been deleted.
if (cur_resource == nullptr) {
continue;
}
// Perform the text update.
static_cast<Element*>(cur_resource->get())->base->SetInnerRML(text);
}
// Otherwise we use the element pointer, but we need to validate that it still exists before doing so.
else {
// Scan the current resources to find the target element.
for (const std::unique_ptr<Style>& cur_e : opened_context->resources) {
if (cur_e.get() == element_ptr) {
element_ptr->base->SetInnerRML(text);
// We can stop after finding the element.
break;
}
}
}
}
}
bool recompui::ContextId::captures_input() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return false;
}
return ctx->captures_input;
}
bool recompui::ContextId::captures_mouse() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return false;
}
return ctx->captures_mouse;
}
void recompui::ContextId::set_captures_input(bool captures_input) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return;
}
ctx->captures_input = captures_input;
}
void recompui::ContextId::set_captures_mouse(bool captures_mouse) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return;
}
ctx->captures_mouse = captures_mouse;
}
recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&& resource) {
@@ -387,6 +509,8 @@ recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&&
auto key = opened_context->resources.emplace(std::move(resource));
if (is_element) {
Element* element_ptr = static_cast<Element*>(resource_ptr);
element_ptr->set_id(std::string{element_ptr->get_type_name()} + "-" + std::to_string(key.raw));
key.set_tag(static_cast<uint8_t>(SlotTag::Element));
// Send one update to the element.
opened_context->to_update.emplace(ResourceId{ key.raw });
@@ -433,6 +557,20 @@ void recompui::ContextId::queue_element_update(ResourceId element) {
opened_context->to_update.emplace(element);
}
void recompui::ContextId::queue_set_text(Element* element, std::string&& text) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::SetTextElementWithoutContext);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::SetTextElementInWrongContext);
}
opened_context->to_set_text.emplace_back(std::make_tuple(element, element->resource_id, std::move(text)));
}
recompui::Style* recompui::ContextId::create_style() {
return add_resource_impl(std::make_unique<Style>());
}
@@ -502,6 +640,28 @@ recompui::Element* recompui::ContextId::get_root_element() {
return &ctx->root_element;
}
recompui::Element* recompui::ContextId::get_autofocus_element() {
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::GetAutofocusInvalidContext);
}
return ctx->autofocus_element;
}
void recompui::ContextId::set_autofocus_element(Element* element) {
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::SetAutofocusInvalidContext);
}
ctx->autofocus_element = element;
}
recompui::ContextId recompui::get_current_context() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
+9 -2
View File
@@ -31,6 +31,7 @@ namespace recompui {
void add_loose_element(Element* element);
void queue_element_update(ResourceId element);
void queue_set_text(Element* element, std::string&& text);
Style* create_style();
@@ -40,15 +41,21 @@ namespace recompui {
Rml::ElementDocument* get_document();
Element* get_root_element();
Element* get_autofocus_element();
void set_autofocus_element(Element* element);
void open();
bool open_if_not_already();
void close();
void process_updates();
static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; }
// TODO
bool takes_input() { return true; }
bool captures_input();
bool captures_mouse();
void set_captures_input(bool captures_input);
void set_captures_mouse(bool captures_input);
};
ContextId create_context(const std::filesystem::path& path);
+25 -3
View File
@@ -4,9 +4,11 @@
namespace recompui {
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "button", true) {
this->style = style;
enable_focus();
set_text(text);
set_display(Display::Block);
set_padding(23.0f);
@@ -21,6 +23,7 @@ namespace recompui {
set_color(Color{ 204, 204, 204, 255 });
set_tab_index(TabIndex::Auto);
hover_style.set_color(Color{ 242, 242, 242, 255 });
focus_style.set_color(Color{ 242, 242, 242, 255 });
disabled_style.set_color(Color{ 204, 204, 204, 128 });
hover_disabled_style.set_color(Color{ 242, 242, 242, 128 });
@@ -34,6 +37,8 @@ namespace recompui {
set_background_color({ 185, 125, 242, background_opacity });
hover_style.set_border_color({ 185, 125, 242, border_hover_opacity });
hover_style.set_background_color({ 185, 125, 242, background_hover_opacity });
focus_style.set_border_color({ 185, 125, 242, border_hover_opacity });
focus_style.set_background_color({ 185, 125, 242, background_hover_opacity });
disabled_style.set_border_color({ 185, 125, 242, border_opacity / 4 });
disabled_style.set_background_color({ 185, 125, 242, background_opacity / 4 });
hover_disabled_style.set_border_color({ 185, 125, 242, border_hover_opacity / 4 });
@@ -45,6 +50,8 @@ namespace recompui {
set_background_color({ 23, 214, 232, background_opacity });
hover_style.set_border_color({ 23, 214, 232, border_hover_opacity });
hover_style.set_background_color({ 23, 214, 232, background_hover_opacity });
focus_style.set_border_color({ 23, 214, 232, border_hover_opacity });
focus_style.set_background_color({ 23, 214, 232, background_hover_opacity });
disabled_style.set_border_color({ 23, 214, 232, border_opacity / 4 });
disabled_style.set_background_color({ 23, 214, 232, background_opacity / 4 });
hover_disabled_style.set_border_color({ 23, 214, 232, border_hover_opacity / 4 });
@@ -57,6 +64,7 @@ namespace recompui {
}
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
add_style(&disabled_style, disabled_state);
add_style(&hover_disabled_style, { hover_state, disabled_state });
@@ -73,10 +81,24 @@ namespace recompui {
}
break;
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
{
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
}
break;
case EventType::Focus:
set_style_enabled(focus_state, std::get<EventFocus>(e.variant).active);
break;
case EventType::Update:
break;
+6
View File
@@ -13,15 +13,21 @@ namespace recompui {
protected:
ButtonStyle style = ButtonStyle::Primary;
Style hover_style;
Style focus_style;
Style disabled_style;
Style hover_disabled_style;
std::list<std::function<void()>> pressed_callbacks;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Button"; }
public:
Button(Element *parent, const std::string &text, ButtonStyle style);
void add_pressed_callback(std::function<void()> callback);
Style* get_hover_style() { return &hover_style; }
Style* get_focus_style() { return &focus_style; }
Style* get_disabled_style() { return &disabled_style; }
Style* get_hover_disabled_style() { return &hover_disabled_style; }
};
} // namespace recompui
+43 -12
View File
@@ -2,7 +2,8 @@
namespace recompui {
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::MouseButton, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
set_cursor(Cursor::Pointer);
if (draggable) {
set_drag(Drag::Drag);
}
@@ -11,30 +12,60 @@ namespace recompui {
void Clickable::process_event(const Event &e) {
switch (e.type) {
case EventType::Click: {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : pressed_callbacks) {
function(click.x, click.y);
if (is_enabled()) {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : clicked_callbacks) {
function(click.x, click.y);
}
break;
}
}
case EventType::MouseButton: {
if (is_enabled()) {
const EventMouseButton &mousebutton = std::get<EventMouseButton>(e.variant);
if (mousebutton.button == MouseButton::Left && mousebutton.pressed) {
for (const auto &function : pressed_callbacks) {
function(mousebutton.x, mousebutton.y);
}
}
break;
}
break;
}
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
break;
case EventType::Drag: {
const EventDrag &drag = std::get<EventDrag>(e.variant);
for (const auto &function : dragged_callbacks) {
function(drag.x, drag.y, drag.phase);
{
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
}
break;
case EventType::Drag: {
if (is_enabled()) {
const EventDrag &drag = std::get<EventDrag>(e.variant);
for (const auto &function : dragged_callbacks) {
function(drag.x, drag.y, drag.phase);
}
break;
}
}
default:
break;
}
}
void Clickable::add_clicked_callback(std::function<void(float, float)> callback) {
clicked_callbacks.emplace_back(callback);
}
void Clickable::add_pressed_callback(std::function<void(float, float)> callback) {
pressed_callbacks.emplace_back(callback);
}
+3
View File
@@ -6,13 +6,16 @@ namespace recompui {
class Clickable : public Element {
protected:
std::vector<std::function<void(float, float)>> clicked_callbacks;
std::vector<std::function<void(float, float)>> pressed_callbacks;
std::vector<std::function<void(float, float, DragPhase)>> dragged_callbacks;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Clickable"; }
public:
Clickable(Element *parent, bool draggable = false);
void add_clicked_callback(std::function<void(float, float)> callback);
void add_pressed_callback(std::function<void(float, float)> callback);
void add_dragged_callback(std::function<void(float, float, DragPhase)> callback);
};
+1 -2
View File
@@ -4,9 +4,8 @@
namespace recompui {
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content) : Element(parent) {
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled) : Element(parent, events_enabled) {
set_display(Display::Flex);
set_flex(1.0f, 1.0f);
set_flex_direction(direction);
set_justify_content(justify_content);
}
+3 -1
View File
@@ -5,8 +5,10 @@
namespace recompui {
class Container : public Element {
protected:
std::string_view get_type_name() override { return "Container"; }
public:
Container(Element* parent, FlexDirection direction, JustifyContent justify_content);
Container(Element* parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled = 0);
};
} // namespace recompui
+219 -15
View File
@@ -1,3 +1,7 @@
#include "RmlUi/Core/StringUtilities.h"
#include "overloaded.h"
#include "recomp_ui.h"
#include "ui_element.h"
#include "../core/ui_context.h"
@@ -13,7 +17,7 @@ Element::Element(Rml::Element *base) {
this->shim = true;
}
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class) {
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class, bool can_set_text) : can_set_text(can_set_text) {
ContextId context = get_current_context();
base_owning = context.get_document()->CreateElement(base_class);
@@ -25,6 +29,9 @@ Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_clas
base = base_owning.get();
}
set_display(Display::Block);
set_property(Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox);
register_event_listeners(events_enabled);
}
@@ -39,6 +46,10 @@ Element::~Element() {
void Element::add_child(Element *child) {
assert(child != nullptr);
if (can_set_text) {
assert(false && "Elements with settable text cannot have children");
return;
}
children.emplace_back(child);
@@ -61,7 +72,12 @@ void Element::register_event_listeners(uint32_t events_enabled) {
this->events_enabled = events_enabled;
if (events_enabled & Events(EventType::Click)) {
base->AddEventListener(Rml::EventId::Click, this);
}
if (events_enabled & Events(EventType::MouseButton)) {
base->AddEventListener(Rml::EventId::Mousedown, this);
base->AddEventListener(Rml::EventId::Mouseup, this);
}
if (events_enabled & Events(EventType::Focus)) {
@@ -83,11 +99,20 @@ void Element::register_event_listeners(uint32_t events_enabled) {
if (events_enabled & Events(EventType::Text)) {
base->AddEventListener(Rml::EventId::Change, this);
}
if (events_enabled & Events(EventType::Navigate)) {
base->AddEventListener(Rml::EventId::Keydown, this);
}
}
void Element::apply_style(Style *style) {
for (auto it : style->property_map) {
base->SetProperty(it.first, it.second);
// Skip redundant SetProperty calls to prevent dirtying unnecessary state.
// This avoids expensive layout operations when a simple color-only style is applied.
const Rml::Property* cur_value = base->GetLocalProperty(it.first);
if (*cur_value != it.second) {
base->SetProperty(it.first, it.second);
}
}
}
@@ -110,7 +135,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,25 +144,86 @@ 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::set_id(const std::string& new_id) {
id = new_id;
base->SetId(new_id);
}
recompui::MouseButton convert_rml_mouse_button(int button) {
switch (button) {
case 0:
return recompui::MouseButton::Left;
case 1:
return recompui::MouseButton::Right;
case 2:
return recompui::MouseButton::Middle;
default:
return recompui::MouseButton::Count;
}
}
void Element::ProcessEvent(Rml::Event &event) {
ContextId prev_context = recompui::try_close_current_context();
ContextId context = ContextId::null();
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
if (doc != nullptr) {
context = get_context_from_document(doc);
}
bool did_open = false;
// TODO disallow null contexts once the entire UI system has been migrated.
if (context != ContextId::null()) {
context.open();
did_open = context.open_if_not_already();
}
// Events that are processed during any phase.
switch (event.GetId()) {
case Rml::EventId::Click:
handle_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
break;
case Rml::EventId::Mousedown:
process_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
{
MouseButton mouse_button = convert_rml_mouse_button(event.GetParameter("button", 3));
if (mouse_button != MouseButton::Count) {
handle_event(Event::mousebutton_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), mouse_button, true));
}
}
break;
case Rml::EventId::Mouseup:
{
MouseButton mouse_button = convert_rml_mouse_button(event.GetParameter("button", 3));
if (mouse_button != MouseButton::Count) {
handle_event(Event::mousebutton_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), mouse_button, false));
}
}
break;
case Rml::EventId::Keydown:
switch ((Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0)) {
case Rml::Input::KeyIdentifier::KI_LEFT:
handle_event(Event::navigate_event(NavDirection::Left));
break;
case Rml::Input::KeyIdentifier::KI_UP:
handle_event(Event::navigate_event(NavDirection::Up));
break;
case Rml::Input::KeyIdentifier::KI_RIGHT:
handle_event(Event::navigate_event(NavDirection::Right));
break;
case Rml::Input::KeyIdentifier::KI_DOWN:
handle_event(Event::navigate_event(NavDirection::Down));
break;
}
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 +233,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<std::string>()));
handle_event(Event::text_event(value_variant->Get<std::string>()));
}
}
@@ -179,9 +265,13 @@ void Element::ProcessEvent(Rml::Event &event) {
}
}
if (context != ContextId::null()) {
if (context != ContextId::null() && did_open) {
context.close();
}
if (prev_context != ContextId::null()) {
prev_context.open();
}
}
void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) {
@@ -192,6 +282,15 @@ void Element::process_event(const Event &) {
// Does nothing by default.
}
void Element::enable_focus() {
set_tab_index_auto();
set_focusable(true);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_nav_auto(NavDirection::Left);
set_nav_auto(NavDirection::Right);
}
void Element::clear_children() {
if (children.empty()) {
return;
@@ -207,6 +306,24 @@ void Element::clear_children() {
children.clear();
}
bool Element::remove_child(ResourceId child) {
bool found = false;
ContextId context = get_current_context();
for (auto it = children.begin(); it != children.end(); ++it) {
Element* cur_child = *it;
if (cur_child->get_resource_id() == child) {
children.erase(it);
context.destroy_resource(cur_child);
found = true;
break;
}
}
return found;
}
void Element::add_style(Style *style, const std::string_view style_name) {
add_style(style, { style_name });
}
@@ -238,8 +355,46 @@ bool Element::is_enabled() const {
return enabled && !disabled_from_parent;
}
// Adapted from RmlUi's `EncodeRml`.
std::string escape_rml(std::string_view string)
{
std::string result;
result.reserve(string.size());
for (char c : string)
{
switch (c)
{
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '&': result += "&amp;"; break;
case '"': result += "&quot;"; break;
case '\n': result += "<br/>"; break;
default: result += c; break;
}
}
return result;
}
void Element::set_text(std::string_view text) {
base->SetInnerRML(std::string(text));
if (can_set_text) {
// Queue the text update. If it's applied immediately, it might happen
// while the document is being updated or rendered. This can cause a crash
// due to the child elements being deleted while the document is being updated.
// Queueing them defers it to the update thread, which prevents that issue.
// Escape the string into Rml to prevent element injection.
get_current_context().queue_set_text(this, escape_rml(text));
}
else {
assert(false && "Attempted to set text of an element that cannot have its text set.");
}
}
std::string Element::get_input_text() {
return base->GetAttribute("value", std::string{});
}
void Element::set_input_text(std::string_view val) {
base->SetAttribute("value", std::string{ val });
}
void Element::set_src(std::string_view src) {
@@ -274,6 +429,10 @@ void Element::set_style_enabled(std::string_view style_name, bool enable) {
apply_styles();
}
bool Element::is_style_enabled(std::string_view style_name) {
return style_active_set.contains(style_name);
}
float Element::get_absolute_left() {
return base->GetAbsoluteLeft();
}
@@ -298,6 +457,47 @@ float Element::get_client_height() {
return base->GetClientHeight();
}
uint32_t Element::get_input_value_u32() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return (uint32_t)d; },
[](float f) { return (uint32_t)f; },
[](uint32_t u) { return u; },
[](std::monostate) { return 0U; }
}, value);
}
float Element::get_input_value_float() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return (float)d; },
[](float f) { return f; },
[](uint32_t u) { return (float)u; },
[](std::monostate) { return 0.0f; }
}, value);
}
double Element::get_input_value_double() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return d; },
[](float f) { return (double)f; },
[](uint32_t u) { return (double)u; },
[](std::monostate) { return 0.0; }
}, value);
}
void Element::focus() {
base->Focus();
}
void Element::blur() {
base->Blur();
}
void Element::queue_update() {
ContextId cur_context = get_current_context();
@@ -309,4 +509,8 @@ void Element::queue_update() {
cur_context.queue_element_update(resource_id);
}
void Element::register_callback(ContextId context, PTR(void) callback, PTR(void) userdata) {
callbacks.emplace_back(UICallback{.context = context, .callback = callback, .userdata = userdata});
}
}
+41 -3
View File
@@ -3,14 +3,26 @@
#include "ui_style.h"
#include "../core/ui_context.h"
#include "recomp.h"
#include <ultramodern/ultra64.h>
#include <unordered_set>
#include <variant>
namespace recompui {
struct UICallback {
ContextId context;
PTR(void) callback;
PTR(void) userdata;
};
using ElementValue = std::variant<uint32_t, float, double, std::monostate>;
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,17 +31,21 @@ private:
std::vector<uint32_t> styles_counter;
std::unordered_set<std::string_view> style_active_set;
std::unordered_multimap<std::string_view, uint32_t> style_name_index_map;
std::vector<UICallback> callbacks;
std::vector<Element *> children;
std::string id;
bool shim = false;
bool enabled = true;
bool disabled_attribute = false;
bool disabled_from_parent = false;
bool can_set_text = false;
void add_child(Element *child);
void register_event_listeners(uint32_t events_enabled);
void apply_style(Style *style);
void apply_styles();
void propagate_disabled(bool disabled);
void handle_event(const Event &e);
void set_id(const std::string& new_id);
// Style overrides.
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override;
@@ -40,21 +56,30 @@ protected:
// Use of this method in inherited classes is discouraged unless it's necessary.
void set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value);
virtual void process_event(const Event &e);
virtual ElementValue get_element_value() { return std::monostate{}; }
virtual void set_input_value(const ElementValue&) {}
virtual std::string_view get_type_name() { return "Element"; }
public:
// Used for backwards compatibility with legacy UI elements.
Element(Rml::Element *base);
// Used to actually construct elements.
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div");
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div", bool can_set_text = false);
virtual ~Element();
void clear_children();
bool remove_child(ResourceId child);
bool remove_child(Element *child) { return remove_child(child->get_resource_id()); }
void add_style(Style *style, std::string_view style_name);
void add_style(Style *style, const std::initializer_list<std::string_view> &style_names);
void set_enabled(bool enabled);
bool is_enabled() const;
void set_text(std::string_view text);
std::string get_input_text();
void set_input_text(std::string_view text);
void set_src(std::string_view src);
void set_style_enabled(std::string_view style_name, bool enabled);
bool is_style_enabled(std::string_view style_name);
void apply_styles();
bool is_element() override { return true; }
float get_absolute_left();
float get_absolute_top();
@@ -62,7 +87,20 @@ public:
float get_client_top();
float get_client_width();
float get_client_height();
void enable_focus();
void focus();
void blur();
void queue_update();
void register_callback(ContextId context, PTR(void) callback, PTR(void) userdata);
uint32_t get_input_value_u32();
float get_input_value_float();
double get_input_value_double();
void set_input_value_u32(uint32_t val) { set_input_value(val); }
void set_input_value_float(float val) { set_input_value(val); }
void set_input_value_double(double val) { set_input_value(val); }
const std::string& get_id() { return id; }
};
void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback);
} // namespace recompui
+2
View File
@@ -5,6 +5,8 @@
namespace recompui {
class Image : public Element {
protected:
std::string_view get_type_name() override { return "ImageView"; }
public:
Image(Element *parent, std::string_view src);
};
+1 -1
View File
@@ -4,7 +4,7 @@
namespace recompui {
Label::Label(Element *parent, LabelStyle label_style) : Element(parent) {
Label::Label(Element *parent, LabelStyle label_style) : Element(parent, 0U, "div", true) {
switch (label_style) {
case LabelStyle::Annotation:
set_color(Color{ 185, 125, 242, 255 });
+2
View File
@@ -12,6 +12,8 @@ namespace recompui {
};
class Label : public Element {
protected:
std::string_view get_type_name() override { return "Label"; }
public:
Label(Element *parent, LabelStyle label_style);
Label(Element *parent, const std::string &text, LabelStyle label_style);
+170 -8
View File
@@ -1,12 +1,15 @@
#include "overloaded.h"
#include "ui_radio.h"
#include "../ui_utils.h"
namespace recompui {
// RadioOption
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "label") {
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::MouseButton, EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable, EventType::Update), "label", true) {
this->index = index;
enable_focus();
set_text(name);
set_cursor(Cursor::Pointer);
set_font_size(20.0f);
@@ -14,29 +17,44 @@ namespace recompui {
set_line_height(20.0f);
set_font_weight(400);
set_font_style(FontStyle::Normal);
set_border_color(Color{ 242, 242, 242, 255 });
set_border_bottom_width(0.0f);
set_border_color(Color{ 242, 242, 242, 0 });
set_border_bottom_width(1.0f);
set_color(Color{ 255, 255, 255, 153 });
set_padding_bottom(8.0f);
set_text_transform(TextTransform::Uppercase);
set_height_auto();
hover_style.set_color(Color{ 255, 255, 255, 204 });
checked_style.set_color(Color{ 255, 255, 255, 255 });
checked_style.set_border_bottom_width(1.0f);
checked_style.set_border_color(Color{ 242, 242, 242, 255 });
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
add_style(&hover_style, { hover_state });
add_style(&checked_style, { checked_state });
add_style(&pulsing_style, { focus_state });
}
void RadioOption::set_pressed_callback(std::function<void(uint32_t)> callback) {
pressed_callback = callback;
}
void RadioOption::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
void RadioOption::set_selected_state(bool enable) {
set_style_enabled(checked_state, enable);
}
void RadioOption::process_event(const Event &e) {
switch (e.type) {
case EventType::MouseButton:
{
const EventMouseButton &mousebutton = std::get<EventMouseButton>(e.variant);
if (mousebutton.button == MouseButton::Left && mousebutton.pressed) {
pressed_callback(index);
}
}
break;
case EventType::Click:
pressed_callback(index);
break;
@@ -46,6 +64,25 @@ namespace recompui {
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
break;
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, active);
if (active) {
queue_update();
}
if (focus_callback != nullptr) {
focus_callback(active);
}
}
break;
case EventType::Update:
if (is_style_enabled(focus_state)) {
pulsing_style.set_color(recompui::get_pulse_color(750));
apply_styles();
queue_update();
}
break;
default:
break;
}
@@ -70,10 +107,41 @@ namespace recompui {
void Radio::option_selected(uint32_t index) {
set_index_internal(index, false, true);
}
void Radio::set_input_value(const ElementValue& val) {
std::visit(overloaded {
[this](uint32_t u) { set_index(u); },
[this](float f) { set_index(f); },
[this](double d) { set_index(d); },
[](std::monostate) {}
}, val);
}
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart) {
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus, EventType::Update)) {
set_gap(24.0f);
set_flex_grow(0.0f);
set_align_items(AlignItems::FlexStart);
enable_focus();
}
void Radio::process_event(const Event &e) {
switch (e.type) {
case EventType::Focus:
if (!options.empty()) {
if (std::get<EventFocus>(e.variant).active) {
blur();
queue_child_focus();
}
if (focus_callback != nullptr) {
focus_callback(std::get<EventFocus>(e.variant).active);
}
}
break;
case EventType::Update:
if (child_focus_queued) {
child_focus_queued = false;
options[index]->focus();
}
}
}
Radio::~Radio() {
@@ -82,13 +150,23 @@ namespace recompui {
void Radio::add_option(std::string_view name) {
RadioOption *option = get_current_context().create_element<RadioOption>(this, name, uint32_t(options.size()));
option->set_pressed_callback(std::bind(&Radio::option_selected, this, std::placeholders::_1));
option->set_pressed_callback([this](uint32_t index){ options[index]->focus(); option_selected(index); });
option->set_focus_callback([this](bool active) {
if (focus_callback != nullptr) {
focus_callback(active);
}
});
options.emplace_back(option);
// The first option was added, select it.
if (options.size() == 1) {
set_index_internal(0, true, false);
}
// At least one other option already existed, so set up navigation.
else {
options[options.size() - 2]->set_nav(NavDirection::Right, options[options.size() - 1]);
options[options.size() - 1]->set_nav(NavDirection::Left, options[options.size() - 2]);
}
}
void Radio::set_index(uint32_t index) {
@@ -103,4 +181,88 @@ namespace recompui {
index_changed_callbacks.emplace_back(callback);
}
};
void Radio::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
void Radio::set_nav_auto(NavDirection dir) {
Element::set_nav_auto(dir);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_auto(dir);
}
break;
case NavDirection::Left:
options.front()->set_nav_auto(dir);
break;
case NavDirection::Right:
options.back()->set_nav_auto(dir);
break;
}
}
}
void Radio::set_nav_none(NavDirection dir) {
Element::set_nav_none(dir);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_none(dir);
}
break;
case NavDirection::Left:
options.front()->set_nav_none(dir);
break;
case NavDirection::Right:
options.back()->set_nav_none(dir);
break;
}
}
}
void Radio::set_nav(NavDirection dir, Element* element) {
Element::set_nav(dir, element);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav(dir, element);
}
break;
case NavDirection::Left:
options.front()->set_nav(dir, element);
break;
case NavDirection::Right:
options.back()->set_nav(dir, element);
break;
}
}
}
void Radio::set_nav_manual(NavDirection dir, const std::string& target) {
Element::set_nav_manual(dir, target);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_manual(dir, target);
}
break;
case NavDirection::Left:
options.front()->set_nav_manual(dir, target);
break;
case NavDirection::Right:
options.back()->set_nav_manual(dir, target);
break;
}
}
}
};
+21 -1
View File
@@ -8,13 +8,17 @@ namespace recompui {
private:
Style hover_style;
Style checked_style;
Style pulsing_style;
std::function<void(uint32_t)> pressed_callback = nullptr;
std::function<void(bool)> focus_callback = nullptr;
uint32_t index = 0;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "LabelRadioOption"; }
public:
RadioOption(Element *parent, std::string_view name, uint32_t index);
void set_pressed_callback(std::function<void(uint32_t)> callback);
void set_focus_callback(std::function<void(bool)> callback);
void set_selected_state(bool enable);
};
@@ -23,9 +27,17 @@ namespace recompui {
std::vector<RadioOption *> options;
uint32_t index = 0;
std::vector<std::function<void(uint32_t)>> index_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
bool child_focus_queued = false;
void set_index_internal(uint32_t index, bool setup, bool trigger_callbacks);
void option_selected(uint32_t index);
void set_input_value(const ElementValue& val) override;
ElementValue get_element_value() override { return get_index(); }
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "LabelRadio"; }
void queue_child_focus() { child_focus_queued = true; queue_update(); }
public:
Radio(Element *parent);
virtual ~Radio();
@@ -33,6 +45,14 @@ namespace recompui {
void set_index(uint32_t index);
uint32_t get_index() const;
void add_index_changed_callback(std::function<void(uint32_t)> callback);
void set_focus_callback(std::function<void(bool)> callback);
size_t num_options() const { return options.size(); }
RadioOption* get_option_element(size_t option_index) { return options[option_index]; }
RadioOption* get_current_option_element() { return options.empty() ? nullptr : options[index]; }
void set_nav_auto(NavDirection dir) override;
void set_nav_none(NavDirection dir) override;
void set_nav(NavDirection dir, Element* element) override;
void set_nav_manual(NavDirection dir, const std::string& target) override;
};
} // namespace recompui
} // namespace recompui
+2
View File
@@ -10,6 +10,8 @@ namespace recompui {
};
class ScrollContainer : public Element {
protected:
std::string_view get_type_name() override { return "ScrollContainer"; }
public:
ScrollContainer(Element *parent, ScrollDirection direction);
};
+118 -20
View File
@@ -1,4 +1,6 @@
#include "overloaded.h"
#include "ui_slider.h"
#include "../ui_utils.h"
#include <cmath>
#include <charconv>
@@ -23,7 +25,7 @@ namespace recompui {
}
}
void Slider::bar_clicked(float x, float) {
void Slider::bar_pressed(float x, float) {
update_value_from_mouse(x);
}
@@ -44,29 +46,104 @@ namespace recompui {
void Slider::update_circle_position() {
double ratio = std::clamp((value - min_value) / (max_value - min_value), 0.0, 1.0);
circle_element->set_left(slider_width_dp * ratio);
circle_element->set_left(ratio * 100.0, Unit::Percent);
}
void Slider::update_label_text() {
char text_buffer[32];
int precision = type == SliderType::Double ? 1 : 0;
auto result = std::to_chars(text_buffer, text_buffer + sizeof(text_buffer) - 1, value, std::chars_format::fixed, precision);
if (result.ec == std::errc()) {
if (type == SliderType::Percent) {
*result.ptr = '%';
result.ptr++;
}
value_label->set_text(std::string(text_buffer, result.ptr));
if (type == SliderType::Double) {
std::snprintf(text_buffer, sizeof(text_buffer), "%.1f", value);
} else if (type == SliderType::Percent) {
std::snprintf(text_buffer, sizeof(text_buffer), "%d%%", static_cast<int>(value));
} else {
std::snprintf(text_buffer, sizeof(text_buffer), "%d", static_cast<int>(value));
}
value_label->set_text(text_buffer);
}
void Slider::set_input_value(const ElementValue& val) {
std::visit(overloaded {
[this](uint32_t u) { set_value(u); },
[this](float f) { set_value(f); },
[this](double d) { set_value(d); },
[](std::monostate) {}
}, val);
}
void Slider::process_event(const Event& e) {
switch (e.type) {
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
circle_element->set_style_enabled(focus_state, active);
if (active) {
queue_update();
}
if (focus_callback != nullptr) {
focus_callback(active);
}
}
break;
case EventType::Update:
if (is_enabled()) {
if (circle_element->is_style_enabled(focus_state)) {
circle_element->set_background_color(recompui::get_pulse_color(750));
queue_update();
}
else {
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
}
}
else {
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
}
break;
case EventType::Navigate:
{
NavDirection dir = std::get<EventNavigate>(e.variant).direction;
if (dir == NavDirection::Left) {
do_step(false);
}
else if (dir == NavDirection::Right) {
do_step(true);
}
}
break;
case EventType::Enable:
{
bool enable_active = std::get<EventEnable>(e.variant).active;
circle_element->set_enabled(enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
}
else {
set_cursor(Cursor::None);
set_focusable(false);
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
}
}
break;
default:
break;
}
}
Slider::Slider(Element *parent, SliderType type) : Element(parent) {
Slider::Slider(Element *parent, SliderType type) : Element(parent, Events(EventType::Focus, EventType::Update, EventType::Navigate, EventType::Enable)) {
this->type = type;
set_cursor(Cursor::Pointer);
set_display(Display::Flex);
set_flex(1.0f, 1.0f, 100.0f, Unit::Percent);
set_flex_direction(FlexDirection::Row);
set_text_align(TextAlign::Left);
set_min_width(120.0f);
enable_focus();
set_nav_none(NavDirection::Left);
set_nav_none(NavDirection::Right);
ContextId context = get_current_context();
@@ -75,8 +152,10 @@ namespace recompui {
value_label->set_min_width(60.0f);
value_label->set_max_width(60.0f);
slider_element = context.create_element<Element>(this);
slider_element->set_width(slider_width_dp);
slider_element = context.create_element<Clickable>(this, true);
slider_element->set_flex(1.0f, 0.0f);
slider_element->add_pressed_callback([this](float x, float y){ bar_pressed(x, y); focus(); });
slider_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
{
bar_element = context.create_element<Clickable>(slider_element, true);
@@ -84,19 +163,20 @@ namespace recompui {
bar_element->set_height(2.0f);
bar_element->set_margin_top(8.0f);
bar_element->set_background_color(Color{ 255, 255, 255, 50 });
bar_element->add_pressed_callback(std::bind(&Slider::bar_clicked, this, std::placeholders::_1, std::placeholders::_2));
bar_element->add_dragged_callback(std::bind(&Slider::bar_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
bar_element->add_pressed_callback([this](float x, float y){ bar_pressed(x, y); focus(); });
bar_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
circle_element = context.create_element<Clickable>(slider_element, true);
circle_element = context.create_element<Clickable>(bar_element, true);
circle_element->set_position(Position::Relative);
circle_element->set_width(16.0f);
circle_element->set_height(16.0f);
circle_element->set_margin_top(-8.0f);
circle_element->set_margin_top(-7.0f);
circle_element->set_margin_right(-8.0f);
circle_element->set_margin_left(-8.0f);
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
circle_element->set_border_radius(8.0f);
circle_element->add_dragged_callback(std::bind(&Slider::circle_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
circle_element->add_pressed_callback([this](float, float){ focus(); });
circle_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ circle_dragged(x, y, phase); focus(); });
circle_element->set_cursor(Cursor::Pointer);
}
@@ -142,4 +222,22 @@ namespace recompui {
value_changed_callbacks.emplace_back(callback);
}
} // namespace recompui
void Slider::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
void Slider::do_step(bool increment) {
double new_value = value;
if (increment) {
new_value += step_value;
}
else {
new_value -= step_value;
}
new_value = std::clamp(new_value, min_value, max_value);
if (new_value != value) {
set_value_internal(new_value, false, true);
}
}
} // namespace recompui
+11 -3
View File
@@ -15,23 +15,29 @@ namespace recompui {
private:
SliderType type = SliderType::Percent;
Label *value_label = nullptr;
Element *slider_element = nullptr;
Clickable *slider_element = nullptr;
Clickable *bar_element = nullptr;
Clickable *circle_element = nullptr;
double value = 50.0;
double min_value = 0.0;
double max_value = 100.0;
double step_value = 0.0;
float slider_width_dp = 300.0;
std::vector<std::function<void(double)>> value_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
void set_value_internal(double v, bool setup, bool trigger_callbacks);
void bar_clicked(float x, float y);
void bar_pressed(float x, float y);
void bar_dragged(float x, float y, DragPhase phase);
void circle_dragged(float x, float y, DragPhase phase);
void update_value_from_mouse(float x);
void update_circle_position();
void update_label_text();
void set_input_value(const ElementValue& val) override;
ElementValue get_element_value() override { return get_value(); }
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Slider"; }
public:
Slider(Element *parent, SliderType type);
@@ -45,6 +51,8 @@ namespace recompui {
void set_step_value(double v);
double get_step_value() const;
void add_value_changed_callback(std::function<void(double)> callback);
void do_step(bool increment);
void set_focus_callback(std::function<void(bool)> callback);
};
} // namespace recompui
+15
View File
@@ -0,0 +1,15 @@
#include "ui_span.h"
#include <cassert>
namespace recompui {
Span::Span(Element *parent) : Element(parent, 0, "span", true) {
set_font_style(FontStyle::Normal);
}
Span::Span(Element *parent, const std::string &text) : Span(parent) {
set_text(text);
}
};
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "ui_element.h"
#include "ui_label.h"
namespace recompui {
class Span : public Element {
protected:
std::string_view get_type_name() override { return "Span"; }
public:
Span(Element *parent);
Span(Element *parent, const std::string &text);
};
} // namespace recompui
+64 -1
View File
@@ -1,4 +1,5 @@
#include "ui_style.h"
#include "ui_element.h"
#include <cassert>
@@ -169,6 +170,22 @@ namespace recompui {
}
}
static Rml::PropertyId nav_to_property(NavDirection dir) {
switch (dir) {
case NavDirection::Up:
return Rml::PropertyId::NavUp;
case NavDirection::Right:
return Rml::PropertyId::NavRight;
case NavDirection::Down:
return Rml::PropertyId::NavDown;
case NavDirection::Left:
return Rml::PropertyId::NavLeft;
default:
assert(false && "Unknown nav direction.");
return Rml::PropertyId::Invalid;
}
}
void Style::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
property_map[property_id] = property;
}
@@ -181,6 +198,17 @@ namespace recompui {
}
void Style::set_visibility(Visibility visibility) {
switch (visibility) {
case Visibility::Visible:
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
break;
case Visibility::Hidden:
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden);
break;
}
}
void Style::set_position(Position position) {
switch (position) {
case Position::Absolute:
@@ -401,7 +429,7 @@ namespace recompui {
void Style::set_cursor(Cursor cursor) {
switch (cursor) {
case Cursor::None:
assert(false && "Unimplemented.");
set_property(Rml::PropertyId::Cursor, Rml::Property("", Rml::Unit::STRING));
break;
case Cursor::Pointer:
set_property(Rml::PropertyId::Cursor, Rml::Property("pointer", Rml::Unit::STRING));
@@ -460,6 +488,12 @@ namespace recompui {
case FlexDirection::Column:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column);
break;
case FlexDirection::RowReverse:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::RowReverse);
break;
case FlexDirection::ColumnReverse:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::ColumnReverse);
break;
default:
assert(false && "Unknown flex direction.");
break;
@@ -545,5 +579,34 @@ namespace recompui {
void Style::set_font_family(std::string_view family) {
set_property(Rml::PropertyId::FontFamily, Rml::Property(Rml::String{ family }, Rml::Unit::UNKNOWN));
}
void Style::set_nav_auto(NavDirection dir) {
set_property(nav_to_property(dir), Rml::Style::Nav::Auto);
}
void Style::set_nav_none(NavDirection dir) {
set_property(nav_to_property(dir), Rml::Style::Nav::None);
}
void Style::set_nav(NavDirection dir, Element* element) {
set_property(nav_to_property(dir), Rml::Property(Rml::String{ "#" + element->get_id() }, Rml::Unit::STRING));
}
void Style::set_nav_manual(NavDirection dir, const std::string& target) {
set_property(nav_to_property(dir), Rml::Property(target, Rml::Unit::STRING));
}
void Style::set_tab_index_auto() {
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::Auto);
}
void Style::set_tab_index_none() {
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::None);
}
void Style::set_focusable(bool focusable) {
set_property(Rml::PropertyId::Focus, focusable ? Rml::Style::Focus::Auto : Rml::Style::Focus::None);
}
} // namespace recompui
+8
View File
@@ -20,6 +20,7 @@ namespace recompui {
public:
Style();
virtual ~Style();
void set_visibility(Visibility visibility);
void set_position(Position position);
void set_left(float left, Unit unit = Unit::Dp);
void set_top(float top, Unit unit = Unit::Dp);
@@ -93,6 +94,13 @@ namespace recompui {
void set_drag(Drag drag);
void set_tab_index(TabIndex focus);
void set_font_family(std::string_view family);
virtual void set_nav_auto(NavDirection dir);
virtual void set_nav_none(NavDirection dir);
virtual void set_nav(NavDirection dir, Element* element);
virtual void set_nav_manual(NavDirection dir, const std::string& target);
void set_tab_index_auto();
void set_tab_index_none();
void set_focusable(bool focusable);
virtual bool is_element() { return false; }
ResourceId get_resource_id() { return resource_id; }
};
+19 -3
View File
@@ -16,17 +16,30 @@ namespace recompui {
break;
}
case EventType::Focus: {
const EventFocus &event = std::get<EventFocus>(e.variant);
if (focus_callback != nullptr) {
focus_callback(event.active);
}
break;
}
default:
break;
}
}
TextInput::TextInput(Element *parent) : Element(parent, Events(EventType::Text), "input") {
TextInput::TextInput(Element *parent, bool text_visible) : Element(parent, Events(EventType::Text, EventType::Focus), "input") {
if (!text_visible) {
set_attribute("type", "password");
}
set_min_width(60.0f);
set_max_width(400.0f);
set_border_color(Color{ 242, 242, 242, 255 });
set_border_bottom_width(1.0f);
set_padding_bottom(6.0f);
set_focusable(true);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_tab_index_auto();
}
void TextInput::set_text(std::string_view text) {
@@ -42,4 +55,7 @@ namespace recompui {
text_changed_callbacks.emplace_back(callback);
}
};
void TextInput::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
};
+4 -1
View File
@@ -8,13 +8,16 @@ namespace recompui {
private:
std::string text;
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "TextInput"; }
public:
TextInput(Element *parent);
TextInput(Element *parent, bool text_visible = true);
void set_text(std::string_view text);
const std::string &get_text();
void add_text_changed_callback(std::function<void(const std::string &)> callback);
void set_focus_callback(std::function<void(bool)> callback);
};
} // namespace recompui
+23 -2
View File
@@ -6,7 +6,9 @@
namespace recompui {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
enable_focus();
set_width(162.0f);
set_height(72.0f);
set_border_radius(36.0f);
@@ -18,13 +20,19 @@ namespace recompui {
checked_style.set_border_color(Color{ 34, 177, 76, 255 });
hover_style.set_border_color(Color{ 177, 76, 34, 255 });
hover_style.set_background_color(Color{ 206, 120, 68, 76 });
focus_style.set_border_color(Color{ 177, 76, 34, 255 });
focus_style.set_background_color(Color{ 206, 120, 68, 76 });
checked_hover_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_hover_style.set_background_color(Color{ 68, 206, 120, 76 });
checked_focus_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_focus_style.set_background_color(Color{ 68, 206, 120, 76 });
disabled_style.set_border_color(Color{ 177, 76, 34, 128 });
checked_disabled_style.set_border_color(Color{ 34, 177, 76, 128 });
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
add_style(&checked_hover_style, { checked_state, hover_state });
add_style(&checked_focus_style, { checked_state, focus_state });
add_style(&disabled_style, disabled_state);
add_style(&checked_disabled_style, { checked_state, disabled_state });
@@ -85,15 +93,28 @@ namespace recompui {
break;
case EventType::Hover: {
bool hover_active = std::get<EventHover>(e.variant).active;
bool hover_active = std::get<EventHover>(e.variant).active && is_enabled();
set_style_enabled(hover_state, hover_active);
floater->set_style_enabled(hover_state, hover_active);
break;
}
case EventType::Focus: {
bool focus_active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, focus_active);
break;
}
case EventType::Enable: {
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
floater->set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
break;
}
case EventType::Update: {
+3
View File
@@ -12,7 +12,9 @@ namespace recompui {
std::list<std::function<void(bool)>> checked_callbacks;
Style checked_style;
Style hover_style;
Style focus_style;
Style checked_hover_style;
Style checked_focus_style;
Style disabled_style;
Style checked_disabled_style;
Style floater_checked_style;
@@ -25,6 +27,7 @@ namespace recompui {
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Toggle"; }
public:
Toggle(Element *parent);
void set_checked(bool checked);
+52 -2
View File
@@ -7,6 +7,7 @@ namespace recompui {
constexpr std::string_view checked_state = "checked";
constexpr std::string_view hover_state = "hover";
constexpr std::string_view focus_state = "focus";
constexpr std::string_view disabled_state = "disabled";
struct Color {
@@ -21,6 +22,7 @@ namespace recompui {
Pointer
};
// These two enums must be kept in sync with patches/recompui_event_structs.h!
enum class EventType {
None,
Click,
@@ -30,6 +32,8 @@ namespace recompui {
Drag,
Text,
Update,
Navigate,
MouseButton,
Count
};
@@ -40,6 +44,20 @@ namespace recompui {
End
};
enum class NavDirection {
Up,
Right,
Down,
Left
};
enum class MouseButton {
Left,
Right,
Middle,
Count
};
template <typename Enum, typename = std::enable_if_t<std::is_enum_v<Enum>>>
constexpr uint32_t Events(Enum first) {
return 1u << static_cast<uint32_t>(first);
@@ -77,7 +95,18 @@ namespace recompui {
std::string text;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, std::monostate>;
struct EventNavigate {
NavDirection direction;
};
struct EventMouseButton {
float x;
float y;
MouseButton button;
bool pressed;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, EventMouseButton, std::monostate>;
struct Event {
EventType type;
@@ -132,6 +161,20 @@ namespace recompui {
e.variant = std::monostate{};
return e;
}
static Event navigate_event(NavDirection direction) {
Event e;
e.type = EventType::Navigate;
e.variant = EventNavigate{ direction };
return e;
}
static Event mousebutton_event(float x, float y, MouseButton button, bool pressed) {
Event e;
e.type = EventType::MouseButton;
e.variant = EventMouseButton{ x, y, button, pressed };
return e;
}
};
enum class Display {
@@ -151,6 +194,11 @@ namespace recompui {
TableCell
};
enum class Visibility {
Visible,
Hidden
};
enum class Position {
Absolute,
Relative
@@ -167,7 +215,9 @@ namespace recompui {
enum class FlexDirection {
Row,
Column
Column,
RowReverse,
ColumnReverse
};
enum class AlignItems {
+335 -84
View File
@@ -1,5 +1,8 @@
#include "recomp_ui.h"
#include "ui_helpers.h"
#include "ui_api_images.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
@@ -12,6 +15,7 @@
#include "elements/ui_radio.h"
#include "elements/ui_scroll_container.h"
#include "elements/ui_slider.h"
#include "elements/ui_span.h"
#include "elements/ui_style.h"
#include "elements/ui_text_input.h"
#include "elements/ui_toggle.h"
@@ -19,92 +23,11 @@
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "ultramodern/error_handling.hpp"
using namespace recompui;
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
// Helpers
ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
return ContextId{ .slot_id = context_id };
}
template <int arg_index>
std::string arg_string(uint8_t* rdram, recomp_context* ctx) {
PTR(char) str = _arg<arg_index, PTR(char)>(rdram, ctx);
// Get the length of the byteswapped string.
size_t len = 0;
while (MEM_B(str, len) != 0x00) {
len++;
}
std::string ret{};
ret.reserve(len + 1);
for (size_t i = 0; i < len; i++) {
ret += (char)MEM_B(str, i);
}
return ret;
}
template <int arg_index>
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
return ResourceId{ .slot_id = slot_id };
}
template <int arg_index>
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
return ui_context.get_root_element();
}
return resource.as_element();
}
template <int arg_index>
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
ContextId ui_context = recompui::get_current_context();
return ui_context.get_root_element();
}
return *resource;
}
template <int arg_index>
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
Color ret{};
ret.r = MEM_B(0, color_arg);
ret.g = MEM_B(1, color_arg);
ret.b = MEM_B(2, color_arg);
ret.a = MEM_B(3, color_arg);
return ret;
}
void return_resource(recomp_context* ctx, ResourceId resource) {
_return<uint32_t>(ctx, resource.slot_id);
}
// Contexts
void recompui_create_context(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
@@ -144,6 +67,20 @@ void recompui_hide_context(uint8_t* rdram, recomp_context* ctx) {
recompui::hide_context(ui_context);
}
void recompui_set_context_captures_input(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
bool captures_input = _arg<1, int>(rdram, ctx) != 0;
ui_context.set_captures_input(captures_input);
}
void recompui_set_context_captures_mouse(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
bool captures_mouse = _arg<1, int>(rdram, ctx) != 0;
ui_context.set_captures_mouse(captures_mouse);
}
// Resources
void recompui_create_style(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
@@ -160,17 +97,110 @@ void recompui_create_element(uint8_t* rdram, recomp_context* ctx) {
return_resource(ctx, ret->get_resource_id());
}
void recompui_destroy_element(uint8_t* rdram, recomp_context* ctx) {
Style* parent_resource = arg_style<0>(rdram, ctx);
if (!parent_resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to remove child from non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* parent = static_cast<Element*>(parent_resource);
ResourceId to_remove = arg_resource_id<1>(rdram, ctx);
if (!parent->remove_child(to_remove)) {
recompui::message_box("Fatal error in mod - attempted to remove child from wrong parent");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
}
void recompui_create_button(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
std::string text = arg_string<2>(rdram, ctx);
std::string text = _arg_string<2>(rdram, ctx);
uint32_t style = _arg<3, uint32_t>(rdram, ctx);
Button* ret = ui_context.create_element<Button>(parent, text, static_cast<ButtonStyle>(style));
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_label(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
std::string text = _arg_string<2>(rdram, ctx);
uint32_t style = _arg<3, uint32_t>(rdram, ctx);
Element* ret = ui_context.create_element<Label>(parent, text, static_cast<LabelStyle>(style));
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_span(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
std::string text = _arg_string<2>(rdram, ctx);
Element* ret = ui_context.create_element<Span>(parent, text);
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_textinput(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
Element* ret = ui_context.create_element<TextInput>(parent);
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_passwordinput(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
Element* ret = ui_context.create_element<TextInput>(parent, false);
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_labelradio(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
PTR(PTR(char)) options = _arg<2, PTR(PTR(char))>(rdram, ctx);
uint32_t num_options = _arg<3, uint32_t>(rdram, ctx);
Radio* ret = ui_context.create_element<Radio>(parent);
for (size_t i = 0; i < num_options; i++) {
ret->add_option(decode_string(rdram, MEM_W(sizeof(uint32_t) * i, options)));
}
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_slider(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
uint32_t type = _arg<2, uint32_t>(rdram, ctx);
float min_value = arg_float3(rdram, ctx);
float max_value = arg_float4(rdram, ctx);
float step = arg_float5(rdram, ctx);
float initial_value = arg_float6(rdram, ctx);
Slider* ret = ui_context.create_element<Slider>(parent, static_cast<SliderType>(type));
ret->set_min_value(min_value);
ret->set_max_value(max_value);
ret->set_step_value(step);
ret->set_value(initial_value);
return_resource(ctx, ret->get_resource_id());
}
// Position and Layout
void recompui_set_visibility(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t visibility = _arg<1, uint32_t>(rdram, ctx);
resource->set_visibility(static_cast<Visibility>(visibility));
}
void recompui_set_position(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t position = _arg<1, uint32_t>(rdram, ctx);
@@ -610,6 +640,19 @@ void recompui_set_overflow_y(uint8_t* rdram, recomp_context* ctx) {
}
// Text and Fonts
void recompui_set_text(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set text of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_text(_arg_string<1>(rdram, ctx));
}
void recompui_set_font_size(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float size = _arg_float_a1(rdram, ctx);
@@ -696,6 +739,192 @@ void recompui_set_tab_index(uint8_t* rdram, recomp_context* ctx) {
resource->set_tab_index(static_cast<TabIndex>(tab_index));
}
// Values
void recompui_get_input_value_u32(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to get value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
_return<uint32_t>(ctx, element->get_input_value_u32());
}
void recompui_get_input_value_float(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to get value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
_return<float>(ctx, element->get_input_value_float());
}
void recompui_get_input_text(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to get input text of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
std::string ret = element->get_input_text();
return_string(rdram, ctx, ret);
}
void recompui_set_input_value_u32(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t value = _arg<1, uint32_t>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_input_value_u32(value);
}
void recompui_set_input_value_float(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float value = _arg_float_a1(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set value of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_input_value_float(value);
}
void recompui_set_input_text(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set input text of non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_input_text(_arg_string<1>(rdram, ctx));
}
// Callbacks
void recompui_register_callback(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to register callback with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to register callback on non-element or element not found in context");
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(ui_context, callback, userdata);
}
// Navigation
void recompui_set_nav_auto(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
u32 nav_dir = _arg<1, u32>(rdram, ctx);
element->set_nav_auto(static_cast<recompui::NavDirection>(nav_dir));
}
void recompui_set_nav_none(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
u32 nav_dir = _arg<1, u32>(rdram, ctx);
element->set_nav_none(static_cast<recompui::NavDirection>(nav_dir));
}
void recompui_set_nav(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = recompui::get_current_context();
if (ui_context == ContextId::null()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* resource = arg_style<0>(rdram, ctx);
if (resource == nullptr || !resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Style* target_resource = arg_style<2>(rdram, ctx);
if (target_resource == nullptr || !target_resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set element navigation to non-element or target element not found in context");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
Element* target_element = static_cast<Element*>(target_resource);
u32 nav_dir = _arg<1, u32>(rdram, ctx);
element->set_nav(static_cast<recompui::NavDirection>(nav_dir), target_element);
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_exports() {
@@ -705,9 +934,19 @@ void recompui::register_ui_exports() {
REGISTER_FUNC(recompui_context_root);
REGISTER_FUNC(recompui_show_context);
REGISTER_FUNC(recompui_hide_context);
REGISTER_FUNC(recompui_set_context_captures_input);
REGISTER_FUNC(recompui_set_context_captures_mouse);
REGISTER_FUNC(recompui_create_style);
REGISTER_FUNC(recompui_create_element);
REGISTER_FUNC(recompui_destroy_element);
REGISTER_FUNC(recompui_create_button);
REGISTER_FUNC(recompui_create_label);
// REGISTER_FUNC(recompui_create_span);
REGISTER_FUNC(recompui_create_textinput);
REGISTER_FUNC(recompui_create_passwordinput);
REGISTER_FUNC(recompui_create_labelradio);
REGISTER_FUNC(recompui_create_slider);
REGISTER_FUNC(recompui_set_visibility);
REGISTER_FUNC(recompui_set_position);
REGISTER_FUNC(recompui_set_left);
REGISTER_FUNC(recompui_set_top);
@@ -766,6 +1005,7 @@ void recompui::register_ui_exports() {
REGISTER_FUNC(recompui_set_overflow);
REGISTER_FUNC(recompui_set_overflow_x);
REGISTER_FUNC(recompui_set_overflow_y);
REGISTER_FUNC(recompui_set_text);
REGISTER_FUNC(recompui_set_font_size);
REGISTER_FUNC(recompui_set_letter_spacing);
REGISTER_FUNC(recompui_set_line_height);
@@ -777,4 +1017,15 @@ 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_get_input_value_u32);
REGISTER_FUNC(recompui_get_input_value_float);
REGISTER_FUNC(recompui_get_input_text);
REGISTER_FUNC(recompui_set_input_value_u32);
REGISTER_FUNC(recompui_set_input_value_float);
REGISTER_FUNC(recompui_set_input_text);
REGISTER_FUNC(recompui_set_nav_auto);
REGISTER_FUNC(recompui_set_nav_none);
REGISTER_FUNC(recompui_set_nav);
REGISTER_FUNC(recompui_register_callback);
register_ui_image_exports();
}
+118
View File
@@ -0,0 +1,118 @@
#include "concurrentqueue.h"
#include "overloaded.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"
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)) {
recompui::ContextId cur_context = cur_callback.callback.context;
cur_context.open();
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);
cur_context.close();
}
}
ctx->r29 += sizeof(RecompuiEventData);
}
+131
View File
@@ -0,0 +1,131 @@
#include <mutex>
#include <unordered_set>
#include "recomp_ui.h"
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "ultramodern/error_handling.hpp"
#include "ui_helpers.h"
#include "ui_api_images.h"
#include "elements/ui_image.h"
using namespace recompui;
struct {
std::mutex mutex;
std::unordered_set<uint32_t> textures{};
uint32_t textures_created = 0;
} TextureState;
const std::string mod_texture_prefix = "?/mod_api/";
static std::string get_texture_name(uint32_t texture_id) {
return mod_texture_prefix + std::to_string(texture_id);
}
static uint32_t get_new_texture_id() {
std::lock_guard lock{TextureState.mutex};
uint32_t cur_id = TextureState.textures_created++;
TextureState.textures.emplace(cur_id);
return cur_id;
}
static void release_texture(uint32_t texture_id) {
std::string texture_name = get_texture_name(texture_id);
std::lock_guard lock{TextureState.mutex};
if (TextureState.textures.erase(texture_id) == 0) {
recompui::message_box("Fatal error in mod - attempted to destroy texture that doesn't exist!");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
recompui::release_image(texture_name);
}
thread_local std::vector<char> swapped_image_bytes;
void recompui_create_texture_rgba32(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t width = _arg<1, uint32_t>(rdram, ctx);
uint32_t height = _arg<2, uint32_t>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's pixel data.
size_t size_bytes = width * height * 4 * sizeof(uint8_t);
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the pixel data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_rgba32(texture_name, swapped_image_bytes, width, height);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_create_texture_image_bytes(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t size_bytes = _arg<1, u32>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's data.
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the image's data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_file(texture_name, swapped_image_bytes);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_destroy_texture(uint8_t* rdram, recomp_context* ctx) {
uint32_t texture_id = _arg<0, uint32_t>(rdram, ctx);
release_texture(texture_id);
}
void recompui_create_imageview(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
uint32_t texture_id = _arg<2, uint32_t>(rdram, ctx);
Element* ret = ui_context.create_element<Image>(parent, get_texture_name(texture_id));
return_resource(ctx, ret->get_resource_id());
}
void recompui_set_imageview_texture(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t texture_id = _arg<1, uint32_t>(rdram, ctx);
if (!resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set texture of non-element");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_src(get_texture_name(texture_id));
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_image_exports() {
REGISTER_FUNC(recompui_create_texture_rgba32);
REGISTER_FUNC(recompui_create_texture_image_bytes);
REGISTER_FUNC(recompui_destroy_texture);
REGISTER_FUNC(recompui_create_imageview);
REGISTER_FUNC(recompui_set_imageview_texture);
}
+10
View File
@@ -0,0 +1,10 @@
#ifndef __UI_API_IMAGES_H__
#define __UI_API_IMAGES_H__
#include <cstdint>
namespace recompui {
void register_ui_image_exports();
}
#endif
+99 -60
View File
@@ -4,6 +4,7 @@
#include "banjo_config.h"
#include "banjo_debug.h"
#include "banjo_render.h"
#include "banjo_support.h"
#include "promptfont.h"
#include "ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
@@ -21,6 +22,26 @@ Rml::DataModelHandle sound_options_model_handle;
// True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
bool configuring_controller = false;
int recompui::config_tab_to_index(recompui::ConfigTab tab) {
switch (tab) {
case recompui::ConfigTab::General:
return 0;
case recompui::ConfigTab::Controls:
return 1;
case recompui::ConfigTab::Graphics:
return 2;
case recompui::ConfigTab::Sound:
return 3;
case recompui::ConfigTab::Mods:
return 4;
case recompui::ConfigTab::Debug:
return 5;
default:
assert(false && "Unknown config tab.");
return 0;
}
}
template <typename T>
void get_option(const T& input, Rml::Variant& output) {
std::string value = "";
@@ -164,7 +185,7 @@ void apply_graphics_config(void) {
void close_config_menu() {
if (ultramodern::renderer::get_graphics_config() != new_options) {
recompui::open_prompt(
recompui::open_choice_prompt(
"Graphics options have changed",
"Would you like to apply or discard the changes?",
"Apply",
@@ -191,7 +212,7 @@ void close_config_menu() {
}
void banjo::open_quit_game_prompt() {
recompui::open_prompt(
recompui::open_choice_prompt(
"Are you sure you want to quit?",
"Any progress since your last save will be lost.",
"Quit",
@@ -376,7 +397,45 @@ recompui::ContextId recompui::get_config_context_id() {
return config_context;
}
// Helper copied from RmlUi to get a named child.
Rml::Element* recompui::get_child_by_tag(Rml::Element* parent, const std::string& tag)
{
// Look for the existing child
for (int i = 0; i < parent->GetNumChildren(); i++)
{
Rml::Element* child = parent->GetChild(i);
if (child->GetTagName() == tag)
return child;
}
return nullptr;
}
class ConfigTabsetListener : public Rml::EventListener {
void ProcessEvent(Rml::Event& event) override {
if (event.GetId() == Rml::EventId::Tabchange) {
int tab_index = event.GetParameter<int>("tab_index", 0);
bool in_mod_tab = (tab_index == recompui::config_tab_to_index(recompui::ConfigTab::Mods));
if (in_mod_tab) {
recompui::set_config_tabset_mod_nav();
}
else {
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
Rml::Element* tabs = recompui::get_child_by_tag(tabset, "tabs");
if (tabs != nullptr) {
size_t num_children = tabs->GetNumChildren();
for (size_t i = 0; i < num_children; i++) {
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
}
}
}
}
}
};
class ConfigMenu : public recompui::MenuController {
private:
ConfigTabsetListener config_tabset_listener;
public:
ConfigMenu() {
@@ -384,11 +443,10 @@ public:
~ConfigMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
config_context = recompui::create_context("assets/config_menu.rml");
Rml::ElementDocument* ret = config_context.get_document();
return ret;
void load_document() override {
config_context = recompui::create_context(banjo::get_asset_path("config_menu.rml"));
recompui::update_mod_list(false);
recompui::get_config_tabset()->AddEventListener(Rml::EventId::Tabchange, &config_tabset_listener);
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "apply_options",
@@ -563,7 +621,7 @@ public:
throw std::runtime_error("Failed to make RmlUi data model for the controls config menu");
}
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } );
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = static_cast<uint64_t>(recomp::get_num_inputs()); } );
constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } );
constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) {
@@ -851,64 +909,45 @@ void recompui::toggle_fullscreen() {
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;
}
void recompui::set_config_tab(ConfigTab tab) {
get_config_tabset()->SetActiveTab(config_tab_to_index(tab));
}
Rml::ElementTabSet* recompui::get_config_tabset() {
ContextId config_context = recompui::get_config_context_id();
ContextId old_context = recompui::try_close_current_context();
Rml::ElementDocument *doc = config_context.get_document();
assert(doc != nullptr);
Rml::Element *tabset_el = doc->GetElementById("config_tabset");
assert(tabset_el != nullptr);
Rml::ElementTabSet *tabset = rmlui_dynamic_cast<Rml::ElementTabSet *>(tabset_el);
assert(tabset != nullptr);
if (old_context != ContextId::null()) {
old_context.open();
}
return tabset;
}
Rml::Element* recompui::get_mod_tab() {
ContextId config_context = recompui::get_config_context_id();
ContextId old_context = recompui::try_close_current_context();
Rml::ElementDocument* doc = config_context.get_document();
assert(doc != nullptr);
Rml::Element* tabset_el = doc->GetElementById("config_tabset");
assert(tabset_el != nullptr);
Rml::Element* tab_el = doc->GetElementById("tab_mods");
assert(tab_el != nullptr);
Rml::ElementTabSet* tabset = rmlui_dynamic_cast<Rml::ElementTabSet*>(tabset_el);
assert(tabset != nullptr);
int tab_index = 0;
switch (tab) {
case ConfigTab::General:
tab_index = 0;
break;
case ConfigTab::Controls:
tab_index = 1;
break;
case ConfigTab::Graphics:
tab_index = 2;
break;
case ConfigTab::Sound:
tab_index = 3;
break;
case ConfigTab::Mods:
tab_index = 4;
break;
case ConfigTab::Debug:
tab_index = 5;
break;
default:
assert(false);
return;
if (old_context != ContextId::null()) {
old_context.open();
}
tabset->SetActiveTab(tab_index);
return tab_el;
}
+54 -21
View File
@@ -13,6 +13,9 @@ namespace recompui {
void ConfigOptionElement::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover:
if (hover_callback == nullptr) {
break;
}
hover_callback(this, std::get<EventHover>(e.variant).active);
break;
case EventType::Update:
@@ -36,8 +39,8 @@ ConfigOptionElement::~ConfigOptionElement() {
}
void ConfigOptionElement::set_id(std::string_view id) {
this->id = id;
void ConfigOptionElement::set_option_id(std::string_view id) {
this->option_id = id;
}
void ConfigOptionElement::set_name(std::string_view name) {
@@ -53,6 +56,10 @@ void ConfigOptionElement::set_hover_callback(std::function<void(ConfigOptionElem
hover_callback = callback;
}
void ConfigOptionElement::set_focus_callback(std::function<void(const std::string &, bool)> callback) {
focus_callback = callback;
}
const std::string &ConfigOptionElement::get_description() const {
return description;
}
@@ -60,45 +67,56 @@ const std::string &ConfigOptionElement::get_description() const {
// ConfigOptionSlider
void ConfigOptionSlider::slider_value_changed(double v) {
callback(id, v);
callback(option_id, v);
}
ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
slider = get_current_context().create_element<Slider>(this, percent ? SliderType::Percent : SliderType::Double);
slider->set_max_width(380.0f);
slider->set_min_value(min_value);
slider->set_max_value(max_value);
slider->set_step_value(step_value);
slider->set_value(value);
slider->add_value_changed_callback(std::bind(&ConfigOptionSlider::slider_value_changed, this, std::placeholders::_1));
slider->add_value_changed_callback([this](double v){ slider_value_changed(v); });
slider->set_focus_callback([this](bool active) {
focus_callback(option_id, active);
});
}
// ConfigOptionTextInput
void ConfigOptionTextInput::text_changed(const std::string &text) {
callback(id, text);
callback(option_id, text);
}
ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
text_input = get_current_context().create_element<TextInput>(this);
text_input->set_max_width(400.0f);
text_input->set_text(value);
text_input->add_text_changed_callback(std::bind(&ConfigOptionTextInput::text_changed, this, std::placeholders::_1));
text_input->add_text_changed_callback([this](const std::string &text){ text_changed(text); });
text_input->set_focus_callback([this](bool active) {
focus_callback(option_id, active);
});
}
// ConfigOptionRadio
void ConfigOptionRadio::index_changed(uint32_t index) {
callback(id, index);
callback(option_id, index);
}
ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
radio = get_current_context().create_element<Radio>(this);
radio->add_index_changed_callback(std::bind(&ConfigOptionRadio::index_changed, this, std::placeholders::_1));
radio->set_focus_callback([this](bool active) {
focus_callback(option_id, active);
});
radio->add_index_changed_callback([this](uint32_t index){ index_changed(index); });
for (std::string_view option : options) {
radio->add_option(option);
}
@@ -117,21 +135,22 @@ void ConfigSubMenu::back_button_pressed() {
recompui::hide_context(sub_menu_context);
recompui::show_context(config_context, "");
recompui::focus_mod_configure_button();
}
void ConfigSubMenu::option_hovered(ConfigOptionElement *option, bool active) {
void ConfigSubMenu::set_description_option_element(ConfigOptionElement *option, bool active) {
if (active) {
hover_option_elements.emplace(option);
description_option_element = option;
}
else {
hover_option_elements.erase(option);
else if (description_option_element == option) {
description_option_element = nullptr;
}
if (hover_option_elements.empty()) {
if (description_option_element == nullptr) {
description_label->set_text("");
}
else {
description_label->set_text((*hover_option_elements.begin())->get_description());
description_label->set_text(description_option_element->get_description());
}
}
@@ -147,12 +166,12 @@ ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
header_container->set_flex_grow(0.0f);
header_container->set_align_items(AlignItems::Center);
header_container->set_padding_left(12.0f);
header_container->set_padding(12.0f);
header_container->set_gap(24.0f);
{
back_button = context.create_element<Button>(header_container, "Back", ButtonStyle::Secondary);
back_button->add_pressed_callback(std::bind(&ConfigSubMenu::back_button_pressed, this));
back_button->add_pressed_callback([this](){ back_button_pressed(); });
title_label = context.create_element<Label>(header_container, "Title", LabelStyle::Large);
}
@@ -167,9 +186,13 @@ ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
config_scroll_container = context.create_element<ScrollContainer>(config_container, ScrollDirection::Vertical);
}
description_label = context.create_element<Label>(body_container, "Description", LabelStyle::Small);
description_label = context.create_element<Label>(body_container, "", LabelStyle::Small);
description_label->set_min_width(800.0f);
description_label->set_padding_left(16.0f);
description_label->set_padding_right(16.0f);
}
recompui::get_current_context().set_autofocus_element(back_button);
}
ConfigSubMenu::~ConfigSubMenu() {
@@ -183,14 +206,24 @@ void ConfigSubMenu::enter(std::string_view title) {
void ConfigSubMenu::clear_options() {
config_scroll_container->clear_children();
config_option_elements.clear();
hover_option_elements.clear();
description_option_element = nullptr;
}
void ConfigSubMenu::add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description) {
option->set_id(id);
option->set_option_id(id);
option->set_name(name);
option->set_description(description);
option->set_hover_callback(std::bind(&ConfigSubMenu::option_hovered, this, std::placeholders::_1, std::placeholders::_2));
option->set_hover_callback([this](ConfigOptionElement *option, bool active){ set_description_option_element(option, active); });
option->set_focus_callback([this, option](const std::string &id, bool active) { set_description_option_element(option, active); });
if (config_option_elements.empty()) {
back_button->set_nav(NavDirection::Down, option->get_focus_element());
option->set_nav(NavDirection::Up, back_button);
}
else {
config_option_elements.back()->set_nav(NavDirection::Down, option->get_focus_element());
option->set_nav(NavDirection::Up, config_option_elements.back()->get_focus_element());
}
config_option_elements.emplace_back(option);
}
@@ -233,4 +266,4 @@ ConfigSubMenu *ElementConfigSubMenu::get_config_sub_menu_element() const {
return config_sub_menu;
}
}
}
+21 -6
View File
@@ -16,20 +16,28 @@ namespace recompui {
class ConfigOptionElement : public Element {
protected:
Label *name_label = nullptr;
std::string id;
std::string option_id;
std::string name;
std::string description;
std::function<void(ConfigOptionElement *, bool)> hover_callback = nullptr;
std::function<void(const std::string &, bool)> focus_callback = nullptr;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ConfigOptionElement"; }
public:
ConfigOptionElement(Element *parent);
virtual ~ConfigOptionElement();
void set_id(std::string_view id);
void set_option_id(std::string_view id);
void set_name(std::string_view name);
void set_description(std::string_view description);
void set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback);
void set_focus_callback(std::function<void(const std::string &, bool)> callback);
const std::string &get_description() const;
void set_nav_auto(NavDirection dir) override { get_focus_element()->set_nav_auto(dir); }
void set_nav_none(NavDirection dir) override { get_focus_element()->set_nav_none(dir); }
void set_nav(NavDirection dir, Element* element) override { get_focus_element()->set_nav(dir, element); }
void set_nav_manual(NavDirection dir, const std::string& target) override { get_focus_element()->set_nav_manual(dir, target); }
virtual Element* get_focus_element() { return this; }
};
class ConfigOptionSlider : public ConfigOptionElement {
@@ -38,8 +46,10 @@ protected:
std::function<void(const std::string &, double)> callback;
void slider_value_changed(double v);
std::string_view get_type_name() override { return "ConfigOptionSlider"; }
public:
ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback);
Element* get_focus_element() override { return slider; }
};
class ConfigOptionTextInput : public ConfigOptionElement {
@@ -48,8 +58,10 @@ protected:
std::function<void(const std::string &, const std::string &)> callback;
void text_changed(const std::string &text);
std::string_view get_type_name() override { return "ConfigOptionTextInput"; }
public:
ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
Element* get_focus_element() override { return text_input; }
};
class ConfigOptionRadio : public ConfigOptionElement {
@@ -58,8 +70,10 @@ protected:
std::function<void(const std::string &, uint32_t)> callback;
void index_changed(uint32_t index);
std::string_view get_type_name() override { return "ConfigOptionRadio"; }
public:
ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
Element* get_focus_element() override { return radio; }
};
class ConfigSubMenu : public Element {
@@ -72,12 +86,13 @@ private:
Container *config_container = nullptr;
ScrollContainer *config_scroll_container = nullptr;
std::vector<ConfigOptionElement *> config_option_elements;
std::unordered_set<ConfigOptionElement *> hover_option_elements;
ConfigOptionElement * description_option_element = nullptr;
void back_button_pressed();
void option_hovered(ConfigOptionElement *option, bool active);
void set_description_option_element(ConfigOptionElement *option, bool active);
void add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description);
protected:
std::string_view get_type_name() override { return "ConfigSubMenu"; }
public:
ConfigSubMenu(Element *parent);
virtual ~ConfigSubMenu();
@@ -99,4 +114,4 @@ private:
};
}
#endif
#endif
+154
View File
@@ -0,0 +1,154 @@
#ifndef __UI_HELPERS_H__
#define __UI_HELPERS_H__
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "elements/ui_element.h"
#include "elements/ui_types.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
namespace recompui {
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
inline ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
return ContextId{ .slot_id = context_id };
}
inline float arg_float2(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<2, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float3(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<3, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float4(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x10, ctx->r29);
return val.f32;
}
inline float arg_float5(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x14, ctx->r29);
return val.f32;
}
inline float arg_float6(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x18, ctx->r29);
return val.f32;
}
template <int arg_index>
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
return ResourceId{ .slot_id = slot_id };
}
template <int arg_index>
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
return ui_context.get_root_element();
}
return resource.as_element();
}
template <int arg_index>
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
ContextId ui_context = recompui::get_current_context();
return ui_context.get_root_element();
}
return *resource;
}
template <int arg_index>
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
Color ret{};
ret.r = MEM_B(0, color_arg);
ret.g = MEM_B(1, color_arg);
ret.b = MEM_B(2, color_arg);
ret.a = MEM_B(3, color_arg);
return ret;
}
inline void return_resource(recomp_context* ctx, ResourceId resource) {
_return<uint32_t>(ctx, resource.slot_id);
}
inline void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) {
gpr addr = (reinterpret_cast<uint8_t*>(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL;
for (size_t i = 0; i < ret.size(); i++) {
MEM_B(i, addr) = ret[i];
}
MEM_B(ret.size(), addr) = '\x00';
_return<PTR(char)>(ctx, addr);
}
inline std::string decode_string(uint8_t* rdram, PTR(char) str) {
// Get the length of the byteswapped string.
size_t len = 0;
while (MEM_B(str, len) != 0x00) {
len++;
}
std::string ret{};
ret.reserve(len + 1);
for (size_t i = 0; i < len; i++) {
ret += (char)MEM_B(str, i);
}
return ret;
}
}
#endif
+32 -39
View File
@@ -1,5 +1,6 @@
#include "recomp_ui.h"
#include "banjo_config.h"
#include "banjo_support.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
@@ -15,41 +16,36 @@ extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
if (result == NFD_OKAY) {
std::filesystem::path path{native_path};
NFD_FreePathN(native_path);
native_path = nullptr;
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
bk_rom_valid = true;
model_handle.DirtyVariable("bk_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
banjo::open_file_dialog([](bool success, const std::filesystem::path& path) {
if (success) {
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
bk_rom_valid = true;
model_handle.DirtyVariable("bk_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
}
});
}
recompui::ContextId launcher_context;
@@ -66,11 +62,8 @@ public:
~LauncherMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
launcher_context = recompui::create_context("assets/launcher.rml");
Rml::ElementDocument* ret = launcher_context.get_document();
return ret;
void load_document() override {
launcher_context = recompui::create_context(banjo::get_asset_path("launcher.rml"));
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
+43 -11
View File
@@ -4,12 +4,13 @@
namespace recompui {
extern const std::string mod_tab_id;
ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
set_flex(1.0f, 1.0f, 200.0f);
set_height(100.0f, Unit::Percent);
set_display(Display::Flex);
set_flex_direction(FlexDirection::Column);
set_border_bottom_right_radius(16.0f);
set_background_color(Color{ 190, 184, 219, 25 });
ContextId context = get_current_context();
@@ -19,6 +20,8 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
header_container->set_padding(16.0f);
header_container->set_gap(16.0f);
header_container->set_background_color(Color{ 0, 0, 0, 89 });
header_container->set_border_bottom_width(1.1f);
header_container->set_border_bottom_color(Color{ 255, 255, 255, 25 });
{
thumbnail_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
thumbnail_container->set_flex(0.0f, 0.0f);
@@ -39,42 +42,48 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
}
}
body_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
body_container->set_flex(0.0f, 0.0f);
body_container = context.create_element<ScrollContainer>(this, ScrollDirection::Vertical);
body_container->set_text_align(TextAlign::Left);
body_container->set_padding(16.0f);
body_container->set_gap(16.0f);
{
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
authors_label = context.create_element<Label>(body_container, LabelStyle::Normal);
authors_label->set_margin_bottom(16.0f);
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
}
spacer_element = context.create_element<Element>(this);
spacer_element->set_flex(1.0f, 0.0f);
buttons_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceAround);
buttons_container->set_flex(0.0f, 0.0f);
buttons_container->set_padding(16.0f);
buttons_container->set_justify_content(JustifyContent::SpaceBetween);
buttons_container->set_border_top_width(1.1f);
buttons_container->set_border_top_color(Color{ 255, 255, 255, 25 });
buttons_container->set_background_color(Color{ 0, 0, 0, 89 });
{
enable_container = context.create_element<Container>(buttons_container, FlexDirection::Row, JustifyContent::FlexStart);
enable_container->set_align_items(AlignItems::Center);
enable_container->set_gap(16.0f);
{
enable_toggle = context.create_element<Toggle>(enable_container);
enable_toggle->add_checked_callback(std::bind(&ModDetailsPanel::enable_toggle_checked, this, std::placeholders::_1));
enable_toggle->add_checked_callback([this](bool checked){ enable_toggle_checked(checked); });
enable_toggle->set_nav_manual(NavDirection::Up, mod_tab_id);
enable_label = context.create_element<Label>(enable_container, "A currently enabled mod requires this mod", LabelStyle::Annotation);
}
configure_button = context.create_element<Button>(buttons_container, "Configure", recompui::ButtonStyle::Secondary);
configure_button->add_pressed_callback(std::bind(&ModDetailsPanel::configure_button_pressed, this));
configure_button->add_pressed_callback([this](){ configure_button_pressed(); });
configure_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
clear_mod_navigation();
}
ModDetailsPanel::~ModDetailsPanel() {
}
void ModDetailsPanel::disable_toggle() {
enable_toggle->set_enabled(false);
}
void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled) {
cur_details = details;
@@ -83,7 +92,7 @@ void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, c
title_label->set_text(cur_details.display_name);
version_label->set_text(cur_details.version.to_string());
std::string authors_str = "<i>Authors</i>:";
std::string authors_str = "Authors:";
bool first = true;
for (const std::string& author : details.authors) {
authors_str += (first ? " " : ", ") + author;
@@ -96,6 +105,13 @@ void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, c
enable_toggle->set_enabled(toggle_enabled);
configure_button->set_enabled(configure_enabled);
enable_label->set_display(toggle_label_visible ? Display::Block : Display::None);
if (configure_enabled) {
enable_toggle->set_nav(NavDirection::Right, configure_button);
}
else {
enable_toggle->set_nav_none(NavDirection::Right);
}
}
void ModDetailsPanel::set_mod_toggled_callback(std::function<void(bool)> callback) {
@@ -106,6 +122,22 @@ void ModDetailsPanel::set_mod_configure_pressed_callback(std::function<void()> c
mod_configure_pressed_callback = callback;
}
void ModDetailsPanel::setup_mod_navigation(Element* nav_target) {
enable_toggle->set_nav(NavDirection::Left, nav_target);
if (enable_toggle->is_enabled()) {
configure_button->set_nav(NavDirection::Left, enable_toggle);
}
else {
configure_button->set_nav(NavDirection::Left, nav_target);
}
}
void ModDetailsPanel::clear_mod_navigation() {
enable_toggle->set_nav_none(NavDirection::Left);
configure_button->set_nav_none(NavDirection::Left);
}
void ModDetailsPanel::enable_toggle_checked(bool checked) {
if (mod_toggled_callback != nullptr) {
mod_toggled_callback(checked);
+9 -2
View File
@@ -7,6 +7,7 @@
#include "elements/ui_image.h"
#include "elements/ui_label.h"
#include "elements/ui_toggle.h"
#include "elements/ui_scroll_container.h"
namespace recompui {
@@ -17,6 +18,13 @@ public:
void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled);
void set_mod_toggled_callback(std::function<void(bool)> callback);
void set_mod_configure_pressed_callback(std::function<void()> callback);
void setup_mod_navigation(Element* nav_target);
void clear_mod_navigation();
Toggle* get_enable_toggle() { return enable_toggle; }
Button* get_configure_button() { return configure_button; }
void disable_toggle();
protected:
std::string_view get_type_name() override { return "ModDetailsPanel"; }
private:
recomp::mods::ModDetails cur_details;
Container *thumbnail_container = nullptr;
@@ -25,10 +33,9 @@ private:
Container *header_details_container = nullptr;
Label *title_label = nullptr;
Label *version_label = nullptr;
Container *body_container = nullptr;
ScrollContainer *body_container = nullptr;
Label *description_label = nullptr;
Label *authors_label = nullptr;
Element *spacer_element = nullptr;
Container *buttons_container = nullptr;
Container *enable_container = nullptr;
Toggle *enable_toggle = nullptr;
+343
View File
@@ -0,0 +1,343 @@
#include "ui_mod_installer.h"
#include "librecomp/mods.hpp"
namespace recompui {
static const std::string ManifestFilename = "mod.json";
static const char *TextureDatabaseFilename = "rt64.json";
static const std::u8string OldExtension = u8".old";
static const std::u8string NewExtension = u8".new";
static bool is_dynamic_lib(const std::filesystem::path &file_path) {
#if defined(_WIN32)
return file_path.extension() == ".dll";
#elif defined(__linux__)
return file_path.extension() == ".so" || file_path.filename().string().find(".so.") != std::string::npos;
#elif defined(__APPLE__)
return file_path.extension() == ".dylib";
#else
static_assert(false, "Unimplemented for this platform.");
#endif
}
size_t zip_write_func(void *opaque, mz_uint64 offset, const void *bytes, size_t count) {
std::ofstream &stream = *(std::ofstream *)(opaque);
stream.seekp(offset, std::ios::beg);
stream.write((const char *)(bytes), count);
return stream.bad() ? 0 : count;
}
void start_single_mod_installation(const std::filesystem::path &file_path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
// Check for the existence of the manifest file.
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
std::filesystem::path target_path = mods_directory / file_path.filename();
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
ModInstaller::Installation installation;
bool exists = false;
std::vector<char> manifest_bytes = file_handle.read_file(ManifestFilename, exists);
if (exists) {
// Parse the manifest file to check for its validity.
std::string error;
recomp::mods::ModManifest manifest;
recomp::mods::ModOpenError open_error = parse_manifest(manifest, manifest_bytes, error);
exists = (open_error == recomp::mods::ModOpenError::Good);
if (exists) {
installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name;
installation.mod_version = manifest.version;
installation.mod_file = target_path;
}
}
else if (file_path.extension() == ".rtz") {
// When it's an rtz file, check if the texture database file exists.
exists = mz_zip_reader_locate_file(file_handle.archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version{0, 0, 0, ""};
installation.mod_file = target_path;
}
}
std::error_code ec;
if (exists) {
std::filesystem::copy(file_path, target_write_path, ec);
if (ec) {
result.error_messages.emplace_back("Unable to install " + file_path.filename().string() + " to mod directory.");
return;
}
}
else {
result.error_messages.emplace_back(file_path.string() + " is not a mod.");
std::filesystem::remove(target_write_path, ec);
return;
}
if (std::filesystem::exists(installation.mod_file, ec)) {
installation.needs_overwrite_confirmation = true;
}
if (!installation.needs_overwrite_confirmation) {
// This check isn't really needed as additional_files will be empty for a single mod installation,
// but it's good to have in case this logic ever changes.
for (const std::filesystem::path &path : installation.additional_files) {
if (std::filesystem::exists(path, ec)) {
installation.needs_overwrite_confirmation = true;
break;
}
}
}
result.pending_installations.emplace_back(installation);
}
void start_package_mod_installation(const std::filesystem::path &path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
std::error_code ec;
char filename[1024];
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
mz_zip_archive *zip_archive = file_handle.archive.get();
mz_uint num_files = mz_zip_reader_get_num_files(file_handle.archive.get());
std::list<std::filesystem::path> dynamic_lib_files;
std::list<ModInstaller::Installation>::iterator first_nrm_iterator = result.pending_installations.end();
bool found_mod = false;
for (mz_uint i = 0; i < num_files; i++) {
mz_uint filename_length = mz_zip_reader_get_filename(zip_archive, i, filename, sizeof(filename));
if (filename_length == 0) {
continue;
}
std::filesystem::path target_path = mods_directory / std::u8string_view((const char8_t *)(filename));
if ((target_path.extension() == ".rtz") || (target_path.extension() == ".nrm")) {
found_mod = true;
ModInstaller::Installation installation;
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) {
result.error_messages.emplace_back("Unable to write to mod directory.");
continue;
}
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close();
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
// Try to load the extracted file as a mod file handle.
recomp::mods::ModOpenError open_error;
std::unique_ptr<recomp::mods::ZipModFileHandle> extracted_file_handle = std::make_unique<recomp::mods::ZipModFileHandle>(target_write_path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
extracted_file_handle.reset();
std::filesystem::remove(target_write_path, ec);
continue;
}
// Check for the existence of the manifest file.
bool exists = false;
std::vector<char> manifest_bytes = extracted_file_handle->read_file(ManifestFilename, exists);
if (exists) {
// Parse the manifest file to check for its validity.
std::string error;
recomp::mods::ModManifest manifest;
open_error = parse_manifest(manifest, manifest_bytes, error);
exists = (open_error == recomp::mods::ModOpenError::Good);
if (exists) {
installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name;
installation.mod_version = manifest.version;
installation.mod_file = target_path;
}
}
else if (target_path.extension() == ".rtz") {
// When it's an rtz file, check if the texture database file exists.
exists = mz_zip_reader_locate_file(extracted_file_handle->archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version();
installation.mod_file = target_path;
}
}
if (!exists) {
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
extracted_file_handle.reset();
std::filesystem::remove(target_write_path, ec);
continue;
}
if (std::filesystem::exists(installation.mod_file, ec)) {
installation.needs_overwrite_confirmation = true;
}
if (!installation.needs_overwrite_confirmation) {
// This check isn't really needed as additional_files will be empty at this point,
// but it's good to have in case this logic ever changes.
for (const std::filesystem::path &path : installation.additional_files) {
if (std::filesystem::exists(path, ec)) {
installation.needs_overwrite_confirmation = true;
break;
}
}
}
result.pending_installations.emplace_back(installation);
// Store the first nrm found for any dynamic libraries that might be found.
if ((first_nrm_iterator == result.pending_installations.end()) && (target_path.extension() == ".nrm")) {
first_nrm_iterator = std::prev(result.pending_installations.end());
}
}
if (is_dynamic_lib(target_path)) {
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) {
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close();
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
dynamic_lib_files.emplace_back(target_path);
}
}
if (!dynamic_lib_files.empty()) {
if (first_nrm_iterator != result.pending_installations.end()) {
// Associate all these files to the first mod that is found.
for (const std::filesystem::path &path : dynamic_lib_files) {
first_nrm_iterator->additional_files.emplace_back(path);
// Run verification against for overwrite confirmations.
if (std::filesystem::exists(path, ec)) {
first_nrm_iterator->needs_overwrite_confirmation = true;
}
}
}
else {
// These library files were not required by any mod, just delete them.
for (const std::filesystem::path &path : dynamic_lib_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
}
}
if (!found_mod) {
result.error_messages.emplace_back("No mods found in " + path.filename().string() + ".");
}
}
void remove_and_rename(std::vector<std::string>& error_messages, const std::filesystem::path& path) {
std::error_code ec;
std::filesystem::path old_path(path.u8string() + OldExtension);
std::filesystem::path new_path(path.u8string() + NewExtension);
// Rename the current path to a temporary old path, but only if the current path already exists.
if (std::filesystem::exists(path, ec)) {
std::filesystem::remove(old_path, ec);
std::filesystem::rename(path, old_path, ec);
if (ec) {
// If it fails, remove the new path.
std::filesystem::remove(new_path, ec);
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
return;
}
}
// Rename the new path to the current path.
std::filesystem::rename(new_path, path, ec);
if (ec) {
// If it fails, remove the new path and also restore the temporary old path to the current path.
std::filesystem::remove(new_path, ec);
std::filesystem::rename(old_path, path, ec);
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
return;
}
// If nothing failed, just remove the temporary old path.
std::filesystem::remove(old_path, ec);
};
void ModInstaller::start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result) {
result = Result();
for (const std::filesystem::path &path : file_paths) {
if (is_dynamic_lib(path)) {
result.error_messages.emplace_back("The provided mod(s) must be installed without extracting the ZIP file(s). Please install the mod ZIP file(s) directly.");
return;
}
}
for (const std::filesystem::path &path : file_paths) {
recomp::mods::ModOpenError open_error;
recomp::mods::ZipModFileHandle file_handle(path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back(path.filename().string() + " is not a valid zip or mod.");
continue;
}
// First we verify if the container itself isn't a mod already.
// TODO hook into the runtime's container registration to check the extension instead of using hardcoded values.
if ((path.extension() == ".rtz") || (path.extension() == ".nrm")) {
start_single_mod_installation(path, file_handle, progress_callback, result);
}
else {
// Scan the container for compatible mods instead. This is the case for packages made by users or how they're tipically uploaded to Thunderstore.
start_package_mod_installation(path, file_handle, progress_callback, result);
}
}
}
void ModInstaller::cancel_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
// Delete all the files that were extracted for all mods.
for (const Installation &installation : result.pending_installations) {
std::filesystem::path new_path(installation.mod_file.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
for (const std::filesystem::path &path : installation.additional_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
}
}
void ModInstaller::finish_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
for (const Installation &installation : result.pending_installations) {
// Overwrite the mod files.
remove_and_rename(error_messages, installation.mod_file);
for (const std::filesystem::path &path : installation.additional_files) {
remove_and_rename(error_messages, path);
}
}
}
};
+42
View File
@@ -0,0 +1,42 @@
#ifndef RECOMPUI_MOD_INSTALLER_H
#define RECOMPUI_MOD_INSTALLER_H
#include <librecomp/game.hpp>
#include <unordered_set>
#include <vector>
#include <string>
#include <list>
namespace recompui {
struct ModInstaller {
struct Installation {
std::string mod_id;
std::string display_name;
recomp::Version mod_version;
std::filesystem::path mod_file;
std::list<std::filesystem::path> additional_files;
bool needs_overwrite_confirmation = false;
};
struct Confirmation {
std::string old_display_name;
std::string new_display_name;
std::string old_mod_id;
std::string new_mod_id;
recomp::Version old_version;
recomp::Version new_version;
};
struct Result {
std::list<std::string> error_messages;
std::list<Installation> pending_installations;
};
static void start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result);
static void cancel_mod_installation(const Result& result, std::vector<std::string>& errors);
static void finish_mod_installation(const Result &result, std::vector<std::string>& errors);
};
};
#endif
+271 -44
View File
@@ -1,5 +1,8 @@
#include "ui_mod_menu.h"
#include "ui_mod_menu.h"
#include "ui_utils.h"
#include "recomp_ui.h"
#include "banjo_support.h"
#include "banjo_render.h"
#include "librecomp/mods.hpp"
@@ -24,51 +27,65 @@ static bool is_mod_enabled_or_auto(const std::string &mod_id) {
}
// ModEntryView
#define COL_TEXT_DEFAULT 242, 242, 242
#define COL_TEXT_DIM 204, 204, 204
#define COL_SECONDARY 23, 214, 232
constexpr float modEntryHeight = 120.0f;
constexpr float modEntryPadding = 4.0f;
ModEntryView::ModEntryView(Element *parent) : Element(parent) {
extern const std::string mod_tab_id;
const std::string mod_tab_id = "#tab_mods";
ModEntryView::ModEntryView(Element *parent) : Element(parent, Events(EventType::Update)) {
ContextId context = get_current_context();
set_display(Display::Flex);
set_flex_direction(FlexDirection::Row);
set_width(100.0f, Unit::Percent);
set_height_auto();
set_padding_top(4.0f);
set_padding_right(8.0f);
set_padding_bottom(4.0f);
set_padding_left(8.0f);
set_border_width(1.1f);
set_border_color(Color{ 242, 242, 242, 12 });
set_background_color(Color{ 242, 242, 242, 12 });
set_padding(modEntryPadding);
set_border_left_width(2.0f);
set_border_color(Color{ COL_TEXT_DEFAULT, 12 });
set_background_color(Color{ COL_TEXT_DEFAULT, 12 });
set_cursor(Cursor::Pointer);
set_color(Color{ COL_TEXT_DEFAULT, 255 });
checked_style.set_border_color(Color{ 242, 242, 242, 160 });
hover_style.set_border_color(Color{ 242, 242, 242, 64 });
checked_hover_style.set_border_color(Color{ 242, 242, 242, 204 });
checked_style.set_border_color(Color{ COL_TEXT_DEFAULT, 160 });
checked_style.set_color(Color{ 255, 255, 255, 255 });
checked_style.set_background_color(Color{ 26, 24, 32, 255 });
hover_style.set_border_color(Color{ COL_TEXT_DEFAULT, 64 });
checked_hover_style.set_border_color(Color{ COL_TEXT_DEFAULT, 255 });
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
{
thumbnail_image = context.create_element<Image>(this, "");
thumbnail_image->set_width(100.0f);
thumbnail_image->set_height(100.0f);
thumbnail_image->set_min_width(100.0f);
thumbnail_image->set_min_height(100.0f);
thumbnail_image->set_width(modEntryHeight);
thumbnail_image->set_height(modEntryHeight);
thumbnail_image->set_min_width(modEntryHeight);
thumbnail_image->set_min_height(modEntryHeight);
thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 });
body_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
body_container = context.create_element<Element>(this);
body_container->set_width_auto();
body_container->set_height(100.0f);
body_container->set_margin_left(16.0f);
body_container->set_overflow(Overflow::Hidden);
body_container->set_padding_top(8.0f);
body_container->set_padding_bottom(8.0f);
body_container->set_max_height(modEntryHeight);
body_container->set_overflow_y(Overflow::Hidden);
{
name_label = context.create_element<Label>(body_container, LabelStyle::Normal);
description_label = context.create_element<Label>(body_container, LabelStyle::Small);
description_label->set_margin_top(4.0f);
description_label->set_color(Color{ COL_TEXT_DIM, 255 });
} // body_container
} // this
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&checked_hover_style, { checked_state, hover_state });
add_style(&pulsing_style, { focus_state });
}
ModEntryView::~ModEntryView() {
@@ -92,12 +109,32 @@ void ModEntryView::set_selected(bool selected) {
set_style_enabled(checked_state, selected);
}
void ModEntryView::set_focused(bool focused) {
set_style_enabled(focus_state, focused);
}
void ModEntryView::process_event(const Event &e) {
switch (e.type) {
case EventType::Update:
if (is_style_enabled(focus_state)) {
pulsing_style.set_color(recompui::get_pulse_color(750));
apply_styles();
queue_update();
}
break;
default:
break;
}
}
// ModEntryButton
ModEntryButton::ModEntryButton(Element *parent, uint32_t mod_index) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Focus, EventType::Drag)) {
this->mod_index = mod_index;
set_drag(Drag::Drag);
enable_focus();
ContextId context = get_current_context();
view = context.create_element<ModEntryView>(this);
@@ -131,16 +168,20 @@ void ModEntryButton::set_selected(bool selected) {
view->set_selected(selected);
}
void ModEntryButton::set_focused(bool focused) {
view->set_focused(focused);
view->queue_update();
}
void ModEntryButton::process_event(const Event& e) {
switch (e.type) {
case EventType::Click:
case EventType::Focus:
selected_callback(mod_index);
set_focused(std::get<EventFocus>(e.variant).active);
break;
case EventType::Hover:
view->set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
break;
case EventType::Focus:
break;
case EventType::Drag:
drag_callback(mod_index, std::get<EventDrag>(e.variant));
break;
@@ -205,13 +246,15 @@ void ModEntrySpacer::set_target_height(float target_height, bool animate_to_targ
// ModMenu
void ModMenu::refresh_mods() {
void ModMenu::refresh_mods(bool scan_mods) {
for (const std::string &thumbnail : loaded_thumbnails) {
recompui::release_image(thumbnail);
}
recomp::mods::scan_mods();
mod_details = recomp::mods::get_mod_details(game_mod_id);
if (scan_mods) {
recomp::mods::scan_mods();
}
mod_details = recomp::mods::get_all_mod_details(game_mod_id);
create_mod_list();
}
@@ -221,13 +264,30 @@ void ModMenu::open_mods_folder() {
std::wstring path_wstr = mods_directory.wstring();
ShellExecuteW(NULL, L"open", path_wstr.c_str(), NULL, NULL, SW_SHOWDEFAULT);
#elif defined(__linux__)
std::string command = "xdg-open " + mods_directory.string() + " &";
std::string command = "xdg-open \"" + mods_directory.string() + "\" &";
std::system(command.c_str());
#elif defined(__APPLE__)
std::string command = "open \"" + mods_directory.string() + "\"";
std::system(command.c_str());
#else
static_assert(false, "Not implemented for this platform.");
#endif
}
void ModMenu::open_install_dialog() {
banjo::open_file_dialog_multiple([](bool success, const std::list<std::filesystem::path>& paths) {
if (success) {
ContextId old_context = recompui::try_close_current_context();
recompui::drop_files(paths);
if (old_context != ContextId::null()) {
old_context.open();
}
}
});
}
void ModMenu::mod_toggled(bool enabled) {
if (active_mod_index >= 0) {
recomp::mods::enable_mod(mod_details[active_mod_index].mod_id, enabled);
@@ -255,11 +315,41 @@ void ModMenu::mod_selected(uint32_t mod_index) {
bool configure_enabled = !config_schema.options.empty();
mod_details_panel->set_mod_details(mod_details[mod_index], thumbnail_src, toggle_checked, toggle_enabled, auto_enabled, configure_enabled);
mod_entry_buttons[active_mod_index]->set_selected(true);
mod_details_panel->setup_mod_navigation(mod_entry_buttons[mod_index]);
// Navigation from the bottom bar.
Button *configure_button = mod_details_panel->get_configure_button();
Toggle *enable_toggle = mod_details_panel->get_enable_toggle();
if (configure_enabled) {
refresh_button->set_nav(NavDirection::Up, configure_button);
mods_folder_button->set_nav(NavDirection::Up, configure_button);
}
else if (toggle_enabled) {
refresh_button->set_nav(NavDirection::Up, enable_toggle);
mods_folder_button->set_nav(NavDirection::Up, enable_toggle);
}
else {
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
// Navigation from the mod list.
if (toggle_enabled) {
mod_entry_buttons[active_mod_index]->set_nav(NavDirection::Right, enable_toggle);
}
else if (configure_enabled) {
mod_entry_buttons[active_mod_index]->set_nav(NavDirection::Right, configure_button);
}
else {
mod_entry_buttons[active_mod_index]->set_nav_none(NavDirection::Right);
}
}
}
void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
constexpr float spacer_height = 110.0f;
constexpr float spacer_height = modEntryHeight + modEntryPadding * 2.0f;
switch (drag.phase) {
case DragPhase::Start: {
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
@@ -274,6 +364,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
float left = mod_entry_buttons[mod_index]->get_absolute_left() - get_absolute_left();
float top = mod_entry_buttons[mod_index]->get_absolute_top() - (height / 2.0f); // TODO: Figure out why this adjustment is even necessary.
mod_entry_buttons[mod_index]->set_display(Display::None);
mod_entry_buttons[mod_index]->set_focused(false);
mod_entry_floating_view->set_display(Display::Flex);
mod_entry_floating_view->set_mod_details(mod_details[mod_index]);
mod_entry_floating_view->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id));
@@ -332,7 +423,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
// Re-order the mods and update all the details on the menu.
recomp::mods::set_mod_index(game_mod_id, mod_details[mod_index].mod_id, mod_drag_target_index);
mod_details = recomp::mods::get_mod_details(game_mod_id);
mod_details = recomp::mods::get_all_mod_details(game_mod_id);
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
mod_entry_buttons[i]->set_mod_details(mod_details[i]);
mod_entry_buttons[i]->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[i].mod_id));
@@ -356,6 +447,22 @@ ContextId get_config_sub_menu_context_id() {
return sub_menu_context;
}
bool ModMenu::handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value) {
if (banjo::renderer::is_texture_pack_enable_config_option(option, true)) {
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options,
[this](const std::string &id, uint32_t value) {
mod_enum_option_changed(id, value);
mod_hd_textures_enabled_changed(value);
});
return true;
}
return false;
}
void ModMenu::mod_configure_requested() {
if (active_mod_index >= 0) {
// Record the context that was open when this function was called and close it.
@@ -373,19 +480,26 @@ void ModMenu::mod_configure_requested() {
continue;
}
if (handle_special_config_options(option, config_value)) {
continue;
}
switch (option.type) {
case recomp::mods::ConfigOptionType::Enum: {
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options, std::bind(&ModMenu::mod_enum_option_changed, this, std::placeholders::_1, std::placeholders::_2));
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options,
[this](const std::string &id, uint32_t value){ mod_enum_option_changed(id, value); });
break;
}
case recomp::mods::ConfigOptionType::Number: {
const recomp::mods::ConfigOptionNumber &option_number = std::get<recomp::mods::ConfigOptionNumber>(option.variant);
config_sub_menu->add_slider_option(option.id, option.name, option.description, std::get<double>(config_value), option_number.min, option_number.max, option_number.step, option_number.percent, std::bind(&ModMenu::mod_number_option_changed, this, std::placeholders::_1, std::placeholders::_2));
config_sub_menu->add_slider_option(option.id, option.name, option.description, std::get<double>(config_value), option_number.min, option_number.max, option_number.step, option_number.percent,
[this](const std::string &id, double value){ mod_number_option_changed(id, value); });
break;
}
case recomp::mods::ConfigOptionType::String: {
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value), std::bind(&ModMenu::mod_string_option_changed, this, std::placeholders::_1, std::placeholders::_2));
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value),
[this](const std::string &id, const std::string &value){ mod_string_option_changed(id, value); });
break;
}
default:
@@ -424,6 +538,17 @@ void ModMenu::mod_number_option_changed(const std::string &id, double value) {
}
}
void ModMenu::mod_hd_textures_enabled_changed(uint32_t value) {
if (active_mod_index >= 0) {
if (value) {
banjo::renderer::secondary_enable_texture_pack(mod_details[active_mod_index].mod_id);
}
else {
banjo::renderer::secondary_disable_texture_pack(mod_details[active_mod_index].mod_id);
}
}
}
void ModMenu::create_mod_list() {
ContextId context = get_current_context();
@@ -432,12 +557,14 @@ void ModMenu::create_mod_list() {
mod_entry_buttons.clear();
mod_entry_spacers.clear();
Toggle* enable_toggle = mod_details_panel->get_enable_toggle();
// Create the child elements for the list scroll.
for (size_t mod_index = 0; mod_index < mod_details.size(); mod_index++) {
const std::vector<char> &thumbnail = recomp::mods::get_mod_thumbnail(mod_details[mod_index].mod_id);
std::string thumbnail_name = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id);
if (!thumbnail.empty()) {
recompui::queue_image_from_bytes(thumbnail_name, thumbnail);
recompui::queue_image_from_bytes_file(thumbnail_name, thumbnail);
loaded_thumbnails.emplace(thumbnail_name);
}
@@ -445,14 +572,28 @@ void ModMenu::create_mod_list() {
mod_entry_spacers.emplace_back(spacer);
ModEntryButton *mod_entry = context.create_element<ModEntryButton>(list_scroll_container, mod_index);
mod_entry->set_mod_selected_callback(std::bind(&ModMenu::mod_selected, this, std::placeholders::_1));
mod_entry->set_mod_drag_callback(std::bind(&ModMenu::mod_dragged, this, std::placeholders::_1, std::placeholders::_2));
mod_entry->set_mod_selected_callback([this](uint32_t mod_index){ mod_selected(mod_index); });
mod_entry->set_mod_drag_callback([this](uint32_t mod_index, recompui::EventDrag drag){ mod_dragged(mod_index, drag); });
mod_entry->set_mod_details(mod_details[mod_index]);
mod_entry->set_mod_thumbnail(thumbnail_name);
mod_entry->set_mod_enabled(is_mod_enabled_or_auto(mod_details[mod_index].mod_id));
mod_entry_buttons.emplace_back(mod_entry);
}
if (!mod_entry_buttons.empty()) {
mod_entry_buttons.front()->set_nav_manual(NavDirection::Up, mod_tab_id);
mod_entry_buttons.back()->set_nav(NavDirection::Down, install_mods_button);
install_mods_button->set_nav(NavDirection::Up, mod_entry_buttons.back());
}
else {
install_mods_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
if (tabset && tabset->GetActiveTab() == recompui::config_tab_to_index(ConfigTab::Mods)) {
recompui::set_config_tabset_mod_nav();
}
// Add one extra spacer at the bottom.
ModEntrySpacer *spacer = context.create_element<ModEntrySpacer>(list_scroll_container);
mod_entry_spacers.emplace_back(spacer);
@@ -467,6 +608,27 @@ void ModMenu::create_mod_list() {
}
}
void ModMenu::process_event(const Event &e) {
if (e.type == EventType::Update) {
if (mods_dirty) {
refresh_mods(mod_scan_queued);
mods_dirty = false;
mod_scan_queued = false;
}
if (ultramodern::is_game_started()) {
install_mods_button->set_enabled(false);
refresh_button->set_enabled(false);
}
if (active_mod_index != -1) {
bool auto_enabled = recomp::mods::is_mod_auto_enabled(mod_details[active_mod_index].mod_id);
bool toggle_enabled = !auto_enabled && (mod_details[active_mod_index].runtime_toggleable || !ultramodern::is_game_started());
if (!toggle_enabled) {
mod_details_panel->disable_toggle();
}
}
}
}
ModMenu::ModMenu(Element *parent) : Element(parent) {
game_mod_id = "bk";
@@ -498,8 +660,8 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
} // list_container
mod_details_panel = context.create_element<ModDetailsPanel>(body_container);
mod_details_panel->set_mod_toggled_callback(std::bind(&ModMenu::mod_toggled, this, std::placeholders::_1));
mod_details_panel->set_mod_configure_pressed_callback(std::bind(&ModMenu::mod_configure_requested, this));
mod_details_panel->set_mod_toggled_callback([this](bool enabled){ mod_toggled(enabled); });
mod_details_panel->set_mod_configure_pressed_callback([this](){ mod_configure_requested(); });
} // body_container
body_empty_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::SpaceBetween);
@@ -511,23 +673,32 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
context.create_element<Element>(body_empty_container);
} // body_empty_container
footer_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceBetween);
footer_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
footer_container->set_width(100.0f, recompui::Unit::Percent);
footer_container->set_align_items(recompui::AlignItems::Center);
footer_container->set_background_color(Color{ 0, 0, 0, 89 });
footer_container->set_border_top_width(1.1f);
footer_container->set_border_top_color(Color{ 255, 255, 255, 25 });
footer_container->set_padding(20.0f);
footer_container->set_gap(20.0f);
footer_container->set_border_bottom_left_radius(16.0f);
footer_container->set_border_bottom_right_radius(16.0f);
{
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
refresh_button->add_pressed_callback(std::bind(&ModMenu::refresh_mods, this));
Button* configure_button = mod_details_panel->get_configure_button();
install_mods_button = context.create_element<Button>(footer_container, "Install Mods", recompui::ButtonStyle::Primary);
install_mods_button->add_pressed_callback([this](){ open_install_dialog(); });
context.create_element<Label>(footer_container, "⚠ UNDER CONSTRUCTION ⚠", LabelStyle::Small);
Element* footer_spacer = context.create_element<Element>(footer_container);
footer_spacer->set_flex(1.0f, 0.0f);
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
refresh_button->add_pressed_callback([this](){ refresh_mods(true); });
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
mods_folder_button = context.create_element<Button>(footer_container, "Open Mods Folder", recompui::ButtonStyle::Primary);
mods_folder_button->add_pressed_callback(std::bind(&ModMenu::open_mods_folder, this));
mods_folder_button->add_pressed_callback([this](){ open_mods_folder(); });
mods_folder_button->set_nav(NavDirection::Up, configure_button);
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
} // footer_container
} // this
@@ -536,11 +707,9 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
mod_entry_floating_view->set_position(Position::Absolute);
mod_entry_floating_view->set_selected(true);
refresh_mods();
context.close();
sub_menu_context = recompui::create_context("assets/config_sub_menu.rml");
sub_menu_context = recompui::create_context(banjo::get_asset_path("config_sub_menu.rml"));
sub_menu_context.open();
Rml::ElementDocument* sub_menu_doc = sub_menu_context.get_document();
Rml::Element* config_sub_menu_generic = sub_menu_doc->GetElementById("config_sub_menu");
@@ -556,6 +725,64 @@ ModMenu::~ModMenu() {
// Placeholder class until the rest of the UI refactor is finished.
recompui::ModMenu* mod_menu;
void update_mod_list(bool scan_mods) {
if (mod_menu) {
recompui::ContextId ui_context = recompui::get_config_context_id();
bool opened = ui_context.open_if_not_already();
mod_menu->set_mods_dirty(scan_mods);
mod_menu->queue_update();
if (opened) {
ui_context.close();
}
}
}
void process_game_started() {
if (mod_menu) {
recompui::ContextId ui_context = recompui::get_config_context_id();
bool opened = ui_context.open_if_not_already();
mod_menu->queue_update();
if (opened) {
ui_context.close();
}
}
}
void set_config_tabset_mod_nav() {
if (mod_menu) {
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
Rml::Element* tabs = recompui::get_child_by_tag(tabset, "tabs");
if (tabs != nullptr) {
size_t num_children = tabs->GetNumChildren();
Element* first_mod_entry = mod_menu->get_first_mod_entry();
if (first_mod_entry != nullptr) {
std::string id = "#" + first_mod_entry->get_id();
for (size_t i = 0; i < num_children; i++) {
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Property{ id, Rml::Unit::STRING });
}
}
else {
for (size_t i = 0; i < num_children; i++) {
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
}
}
}
}
}
void focus_mod_configure_button() {
Element* configure_button = mod_menu->get_mod_configure_button();
if (configure_button) {
configure_button->focus();
}
}
ElementModMenu::ElementModMenu(const Rml::String &tag) : Rml::Element(tag) {
SetProperty("width", "100%");
SetProperty("height", "100%");
+22 -4
View File
@@ -18,14 +18,19 @@ public:
void set_mod_thumbnail(const std::string &thumbnail);
void set_mod_enabled(bool enabled);
void set_selected(bool selected);
void set_focused(bool focused);
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntryView"; }
private:
Image *thumbnail_image = nullptr;
Container *body_container = nullptr;
Element *body_container = nullptr;
Label *name_label = nullptr;
Label *description_label = nullptr;
Style checked_style;
Style hover_style;
Style checked_hover_style;
Style pulsing_style;
};
class ModEntryButton : public Element {
@@ -38,8 +43,10 @@ public:
void set_mod_thumbnail(const std::string &thumbnail);
void set_mod_enabled(bool enabled);
void set_selected(bool selected);
void set_focused(bool focused);
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntryButton"; }
private:
uint32_t mod_index = 0;
ModEntryView *view = nullptr;
@@ -56,6 +63,7 @@ private:
void check_height_distance();
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntrySpacer"; }
public:
ModEntrySpacer(Element *parent);
void set_target_height(float target_height, bool animate_to_target);
@@ -65,17 +73,26 @@ class ModMenu : public Element {
public:
ModMenu(Element *parent);
virtual ~ModMenu();
void set_mods_dirty(bool scan_mods) { mods_dirty = true; mod_scan_queued = scan_mods; }
Element* get_first_mod_entry() { return !mod_entry_buttons.empty() ? mod_entry_buttons[0] : nullptr; }
Element* get_mod_configure_button() { return mod_details_panel != nullptr ? mod_details_panel->get_configure_button() : nullptr; }
protected:
std::string_view get_type_name() override { return "ModMenu"; }
private:
void refresh_mods();
void refresh_mods(bool scan_mods);
void open_mods_folder();
void open_install_dialog();
void mod_toggled(bool enabled);
void mod_selected(uint32_t mod_index);
void mod_dragged(uint32_t mod_index, EventDrag drag);
void mod_configure_requested();
bool handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value);
void mod_enum_option_changed(const std::string &id, uint32_t value);
void mod_string_option_changed(const std::string &id, const std::string &value);
void mod_number_option_changed(const std::string &id, double value);
void mod_hd_textures_enabled_changed(uint32_t value);
void create_mod_list();
void process_event(const Event &e) override;
Container *body_container = nullptr;
Container *list_container = nullptr;
@@ -83,6 +100,7 @@ private:
ModDetailsPanel *mod_details_panel = nullptr;
Container *body_empty_container = nullptr;
Container *footer_container = nullptr;
Button *install_mods_button = nullptr;
Button *refresh_button = nullptr;
Button *mods_folder_button = nullptr;
int32_t active_mod_index = -1;
@@ -96,6 +114,8 @@ private:
std::vector<recomp::mods::ModDetails> mod_details{};
std::unordered_set<std::string> loaded_thumbnails;
std::string game_mod_id;
bool mods_dirty = false;
bool mod_scan_queued = false;
ConfigSubMenu *config_sub_menu;
};
@@ -104,8 +124,6 @@ class ElementModMenu : public Rml::Element {
public:
ElementModMenu(const Rml::String& tag);
virtual ~ElementModMenu();
private:
ModMenu *mod_menu;
};
} // namespace recompui
+360
View File
@@ -0,0 +1,360 @@
#include <mutex>
#include "recomp_ui.h"
#include "elements/ui_element.h"
#include "elements/ui_label.h"
#include "elements/ui_button.h"
struct {
recompui::ContextId ui_context;
recompui::Label* prompt_header;
recompui::Label* prompt_label;
recompui::Element* prompt_controls;
recompui::Button* confirm_button;
recompui::Button* cancel_button;
std::function<void()> confirm_action;
std::function<void()> cancel_action;
std::string return_element_id;
std::mutex mutex;
} prompt_state;
void run_confirm_callback() {
std::function<void()> confirm_action;
{
std::lock_guard lock{ prompt_state.mutex };
confirm_action = std::move(prompt_state.confirm_action);
}
if (confirm_action) {
confirm_action();
}
recompui::hide_context(prompt_state.ui_context);
// TODO nav: focus on return_element_id
// or just remove it as the usage of the prompt can change now
}
void run_cancel_callback() {
std::function<void()> cancel_action;
{
std::lock_guard lock{ prompt_state.mutex };
cancel_action = std::move(prompt_state.cancel_action);
}
if (cancel_action) {
cancel_action();
}
recompui::hide_context(prompt_state.ui_context);
// TODO nav: focus on return_element_id
// or just remove it as the usage of the prompt can change now
}
void recompui::init_prompt_context() {
ContextId context = create_context();
std::lock_guard lock{ prompt_state.mutex };
context.open();
prompt_state.ui_context = context;
Element* window = context.create_element<Element>(context.get_root_element());
window->set_display(Display::Flex);
window->set_flex_direction(FlexDirection::Column);
window->set_background_color({0, 0, 0, 0});
Element* prompt_overlay = context.create_element<Element>(window);
prompt_overlay->set_background_color(Color{ 190, 184, 219, 25 });
prompt_overlay->set_position(Position::Absolute);
prompt_overlay->set_top(0);
prompt_overlay->set_right(0);
prompt_overlay->set_bottom(0);
prompt_overlay->set_left(0);
Element* prompt_content_wrapper = context.create_element<Element>(window);
prompt_content_wrapper->set_display(Display::Flex);
prompt_content_wrapper->set_position(Position::Absolute);
prompt_content_wrapper->set_top(0);
prompt_content_wrapper->set_right(0);
prompt_content_wrapper->set_bottom(0);
prompt_content_wrapper->set_left(0);
prompt_content_wrapper->set_align_items(AlignItems::Center);
prompt_content_wrapper->set_justify_content(JustifyContent::Center);
Element* prompt_content = context.create_element<Element>(prompt_content_wrapper);
prompt_content->set_display(Display::Flex);
prompt_content->set_position(Position::Relative);
prompt_content->set_flex(1.0f, 1.0f);
prompt_content->set_flex_basis(100, Unit::Percent);
prompt_content->set_flex_direction(FlexDirection::Column);
prompt_content->set_width(100, Unit::Percent);
prompt_content->set_max_width(700, Unit::Dp);
prompt_content->set_height_auto();
prompt_content->set_margin_auto();
prompt_content->set_border_width(1.1, Unit::Dp);
prompt_content->set_border_radius(16, Unit::Dp);
prompt_content->set_border_color(Color{ 255, 255, 255, 51 });
prompt_content->set_background_color(Color{ 8, 7, 13, 229 });
prompt_state.prompt_header = context.create_element<Label>(prompt_content, "", LabelStyle::Large);
prompt_state.prompt_header->set_margin(24, Unit::Dp);
prompt_state.prompt_label = context.create_element<Label>(prompt_content, "", LabelStyle::Small);
prompt_state.prompt_label->set_margin(24, Unit::Dp);
prompt_state.prompt_label->set_margin_top(0);
prompt_state.prompt_controls = context.create_element<Element>(prompt_content);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.prompt_controls->set_flex_direction(FlexDirection::Row);
prompt_state.prompt_controls->set_justify_content(JustifyContent::Center);
prompt_state.prompt_controls->set_padding_top(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_bottom(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_left(12, Unit::Dp);
prompt_state.prompt_controls->set_padding_right(12, Unit::Dp);
prompt_state.prompt_controls->set_border_top_width(1.1, Unit::Dp);
prompt_state.prompt_controls->set_border_top_color({ 255, 255, 255, 25 });
prompt_state.confirm_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.confirm_button->set_min_width(185.0f, Unit::Dp);
prompt_state.confirm_button->set_margin_top(0);
prompt_state.confirm_button->set_margin_bottom(0);
prompt_state.confirm_button->set_margin_left(12, Unit::Dp);
prompt_state.confirm_button->set_margin_right(12, Unit::Dp);
prompt_state.confirm_button->set_text_align(TextAlign::Center);
prompt_state.confirm_button->set_color(Color{ 204, 204, 204, 255 });
prompt_state.confirm_button->add_pressed_callback(run_confirm_callback);
Style* confirm_hover_style = prompt_state.confirm_button->get_hover_style();
confirm_hover_style->set_border_color(Color{ 69, 208, 67, 255 });
confirm_hover_style->set_background_color(Color{ 69, 208, 67, 76 });
confirm_hover_style->set_color(Color{ 242, 242, 242, 255 });
Style* confirm_focus_style = prompt_state.confirm_button->get_focus_style();
confirm_focus_style->set_border_color(Color{ 69, 208, 67, 255 });
confirm_focus_style->set_background_color(Color{ 69, 208, 67, 76 });
confirm_focus_style->set_color(Color{ 242, 242, 242, 255 });
prompt_state.cancel_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.cancel_button->set_min_width(185.0f, Unit::Dp);
prompt_state.cancel_button->set_margin_top(0);
prompt_state.cancel_button->set_margin_bottom(0);
prompt_state.cancel_button->set_margin_left(12, Unit::Dp);
prompt_state.cancel_button->set_margin_right(12, Unit::Dp);
prompt_state.cancel_button->set_text_align(TextAlign::Center);
prompt_state.cancel_button->set_color(Color{ 204, 204, 204, 255 });
prompt_state.cancel_button->add_pressed_callback(run_cancel_callback);
Style* cancel_hover_style = prompt_state.cancel_button->get_hover_style();
cancel_hover_style->set_border_color(Color{ 248, 96, 57, 255 });
cancel_hover_style->set_background_color(Color{ 248, 96, 57, 76 });
cancel_hover_style->set_color(Color{ 242, 242, 242, 255 });
Style* cancel_focus_style = prompt_state.cancel_button->get_focus_style();
cancel_focus_style->set_border_color(Color{ 248, 96, 57, 255 });
cancel_focus_style->set_background_color(Color{ 248, 96, 57, 76 });
cancel_focus_style->set_color(Color{ 242, 242, 242, 255 });
context.close();
}
void style_button(recompui::Button* button, recompui::ButtonVariant variant) {
recompui::Color button_color{};
switch (variant) {
case recompui::ButtonVariant::Primary:
button_color = { 185, 125, 242, 255 };
break;
case recompui::ButtonVariant::Secondary:
button_color = { 23, 214, 232, 255 };
break;
case recompui::ButtonVariant::Tertiary:
button_color = { 242, 242, 242, 255 };
break;
case recompui::ButtonVariant::Success:
button_color = { 69, 208, 67, 255 };
break;
case recompui::ButtonVariant::Error:
button_color = { 248, 96, 57, 255 };
break;
case recompui::ButtonVariant::Warning:
button_color = { 233, 205, 53, 255 };
break;
default:
assert(false);
break;
}
recompui::Color border_color = button_color;
recompui::Color background_color = button_color;
border_color.a = 0.8f * 255;
background_color.a = 0.05f * 255;
button->set_border_color(border_color);
button->set_background_color(background_color);
recompui::Color hover_border_color = button_color;
recompui::Color hover_background_color = button_color;
hover_border_color.a = 255;
hover_background_color.a = 0.3f * 255;
recompui::Style* hover_style = button->get_hover_style();
hover_style->set_border_color(hover_border_color);
hover_style->set_background_color(hover_background_color);
recompui::Style* focus_style = button->get_focus_style();
focus_style->set_border_color(hover_border_color);
focus_style->set_background_color(hover_background_color);
recompui::Color disabled_color { 255, 255, 255, 0.6f * 255 };
recompui::Style* disabled_style = button->get_disabled_style();
disabled_style->set_color(disabled_color);
}
// Must be called while prompt_state.mutex is locked.
void show_prompt(std::function<void()>& prev_cancel_action, bool focus_on_cancel) {
if (focus_on_cancel) {
prompt_state.ui_context.set_autofocus_element(prompt_state.cancel_button);
}
else {
prompt_state.ui_context.set_autofocus_element(prompt_state.confirm_button);
}
if (!recompui::is_context_shown(prompt_state.ui_context)) {
recompui::show_context(prompt_state.ui_context, "");
}
else {
// Call the previous cancel action to effectively close the previous prompt.
if (prev_cancel_action) {
prev_cancel_action();
}
}
}
void recompui::open_choice_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& confirm_label_text,
const std::string& cancel_label_text,
std::function<void()> confirm_action,
std::function<void()> cancel_action,
ButtonVariant confirm_variant,
ButtonVariant cancel_variant,
bool focus_on_cancel,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::Block);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.confirm_button->set_text(confirm_label_text);
prompt_state.cancel_button->set_text(cancel_label_text);
prompt_state.confirm_action = confirm_action;
prompt_state.cancel_action = cancel_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.confirm_button, confirm_variant);
style_button(prompt_state.cancel_button, cancel_variant);
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, focus_on_cancel);
}
void recompui::open_info_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& okay_label_text,
std::function<void()> okay_action,
ButtonVariant okay_variant,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.cancel_button->set_text(okay_label_text);
prompt_state.confirm_action = {};
prompt_state.cancel_action = okay_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.cancel_button, okay_variant);
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, true);
}
void recompui::open_notification(
const std::string& header_text,
const std::string& content_text,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::None);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::None);
prompt_state.confirm_action = {};
prompt_state.cancel_action = {};
prompt_state.return_element_id = return_element_id;
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, false);
}
void recompui::close_prompt() {
std::lock_guard lock{ prompt_state.mutex };
if (recompui::is_context_shown(prompt_state.ui_context)) {
if (prompt_state.cancel_action) {
prompt_state.cancel_action();
}
recompui::hide_context(prompt_state.ui_context);
}
}
bool recompui::is_prompt_open() {
std::lock_guard lock{ prompt_state.mutex };
return recompui::is_context_shown(prompt_state.ui_context);
}
+80 -13
View File
@@ -22,6 +22,9 @@
#ifdef _WIN32
# include "InterfaceVS.hlsl.dxil.h"
# include "InterfacePS.hlsl.dxil.h"
#elif defined(__APPLE__)
# include "InterfaceVS.hlsl.metal.h"
# include "InterfacePS.hlsl.metal.h"
#endif
#ifdef _WIN32
@@ -31,6 +34,13 @@
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::DXIL ? std::size(name##BlobDXIL) : 0)
#elif defined(__APPLE__)
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : \
(format) == RT64::RenderShaderFormat::METAL ? name##BlobMSL : nullptr)
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::METAL ? std::size(name##BlobMSL) : 0)
#else
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : nullptr)
@@ -62,7 +72,19 @@ T from_bytes_le(const char* input) {
return *reinterpret_cast<const T*>(input);
}
typedef std::pair<std::string, std::vector<char>> ImageFromBytes;
enum class ImageType {
File,
RGBA32
};
struct ImageFromBytes {
ImageType type;
// Dimensions only used for RGBA32 data. Files pull the size from the file data.
uint32_t width;
uint32_t height;
std::string name;
std::vector<char> bytes;
};
namespace recompui {
class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
@@ -128,7 +150,7 @@ class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
bool scissor_enabled_ = false;
std::vector<std::unique_ptr<RT64::RenderBuffer>> stale_buffers_{};
moodycamel::ConcurrentQueue<ImageFromBytes> image_from_bytes_queue;
std::unordered_map<std::string, std::vector<char>> image_from_bytes_map;
std::unordered_map<std::string, ImageFromBytes> image_from_bytes_map;
public:
RmlRenderInterface_RT64_impl(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
interface_ = interface;
@@ -201,7 +223,19 @@ public:
// Create the pipeline description
RT64::RenderGraphicsPipelineDesc pipeline_desc{};
pipeline_desc.renderTargetBlend[0] = RT64::RenderBlendDesc::AlphaBlend();
// Set up alpha blending for non-premultiplied alpha. RmlUi recommends using premultiplied alpha normally,
// but that would require preprocessing all input files, which would be difficult for user-provided content (such as mods).
// This blending setup produces similar results as premultipled alpha but for normal assets as it multiplies during blending and
// computes the output alpha value the same way that a premultipled alpha blender would.
pipeline_desc.renderTargetBlend[0] = RT64::RenderBlendDesc {
.blendEnabled = true,
.srcBlend = RT64::RenderBlend::SRC_ALPHA,
.dstBlend = RT64::RenderBlend::INV_SRC_ALPHA,
.blendOp = RT64::RenderBlendOperation::ADD,
.srcBlendAlpha = RT64::RenderBlend::ONE,
.dstBlendAlpha = RT64::RenderBlend::INV_SRC_ALPHA,
.blendOpAlpha = RT64::RenderBlendOperation::ADD,
};
pipeline_desc.renderTargetFormat[0] = SwapChainFormat; // TODO: Use whatever format the swap chain was created with.
pipeline_desc.renderTargetCount = 1;
pipeline_desc.cullMode = RT64::RenderCullMode::NONE;
@@ -236,7 +270,7 @@ public:
}
copy_command_queue_ = device->createCommandQueue(RT64::RenderCommandListType::COPY);
copy_command_list_ = device->createCommandList(RT64::RenderCommandListType::COPY);
copy_command_list_ = copy_command_queue_->createCommandList(RT64::RenderCommandListType::COPY);
copy_command_fence_ = device->createCommandFence();
}
@@ -393,11 +427,30 @@ public:
return true;
}
// TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input.
std::vector<uint8_t> data_copy(it->second.data(), it->second.data() + it->second.size());
RT64::Texture* texture = nullptr;
std::unique_ptr<RT64::RenderBuffer> texture_buffer;
ImageFromBytes& img = it->second;
copy_command_list_->begin();
RT64::Texture *texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer);
switch (img.type) {
case ImageType::RGBA32:
{
// Read the image header (two 32-bit values for width and height respectively).
uint32_t rowPitch = img.width * 4;
size_t byteCount = img.height * rowPitch;
texture = new RT64::Texture();
RT64::TextureCache::setRGBA32(texture, device_, copy_command_list_.get(), reinterpret_cast<const uint8_t*>(img.bytes.data()), byteCount, img.width, img.height, rowPitch, texture_buffer, nullptr);
}
break;
case ImageType::File:
{
// TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input.
std::vector<uint8_t> data_copy(img.bytes.data(), img.bytes.data() + img.bytes.size());
texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer);
}
break;
}
copy_command_list_->end();
copy_command_queue_->executeCommandLists(copy_command_list_.get(), copy_command_fence_.get());
copy_command_queue_->waitForCommandFence(copy_command_fence_.get());
@@ -585,6 +638,7 @@ public:
list->setGraphicsPipelineLayout(layout_.get());
list->setGraphicsDescriptorSet(sampler_set_.get(), 0);
list->setGraphicsDescriptorSet(screen_descriptor_set_.get(), 1);
list->setScissors(RT64::RenderRect{ 0, 0, window_width_, window_height_ });
RT64::RenderVertexBufferView vertex_view(screen_vertex_buffer_.get(), screen_vertex_buffer_size_);
list->setVertexBuffers(0, &vertex_view, 1, &vertex_slot_);
@@ -604,14 +658,21 @@ public:
list_ = nullptr;
}
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
image_from_bytes_queue.enqueue(ImageFromBytes(src, bytes));
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
// Width and height aren't used for file images, so set them to 0.
image_from_bytes_queue.enqueue(ImageFromBytes{ .type = ImageType::File, .width = 0, .height = 0, .name = src, .bytes = bytes });
}
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
image_from_bytes_queue.enqueue(ImageFromBytes{ .type = ImageType::RGBA32, .width = width, .height = height, .name = src, .bytes = bytes });
}
void flush_image_from_bytes_queue() {
ImageFromBytes image_from_bytes;
while (image_from_bytes_queue.try_dequeue(image_from_bytes)) {
image_from_bytes_map.emplace(image_from_bytes.first, std::move(image_from_bytes.second));
// We can move the name into the map since the name in the actual entry is no longer needed.
// After that, move the entry itself into the map.
image_from_bytes_map.emplace(std::move(image_from_bytes.name), std::move(image_from_bytes));
}
}
};
@@ -647,8 +708,14 @@ void recompui::RmlRenderInterface_RT64::end(RT64::RenderCommandList* list, RT64:
impl->end(list, framebuffer);
}
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
assert(static_cast<bool>(impl));
impl->queue_image_from_bytes(src, bytes);
}
impl->queue_image_from_bytes_file(src, bytes);
}
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
assert(static_cast<bool>(impl));
impl->queue_image_from_bytes_rgba32(src, bytes, width, height);
}
+3 -1
View File
@@ -2,6 +2,7 @@
#define __UI_RENDERER_H__
#include <memory>
#include "recomp_ui.h"
namespace RT64 {
struct RenderInterface;
@@ -29,7 +30,8 @@ namespace recompui {
void start(RT64::RenderCommandList* list, int image_width, int image_height);
void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer);
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes);
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes);
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height);
};
} // namespace recompui
+252 -34
View File
@@ -4,6 +4,7 @@
#else
#include <SDL2/SDL_video.h>
#endif
#include <chrono>
#include "rt64_render_hooks.h"
@@ -19,9 +20,11 @@
#include "recomp_input.h"
#include "librecomp/game.hpp"
#include "banjo_config.h"
#include "banjo_support.h"
#include "ui_rml_hacks.hpp"
#include "ui_elements.h"
#include "ui_mod_menu.h"
#include "ui_mod_installer.h"
#include "ui_renderer.h"
bool can_focus(Rml::Element* element) {
@@ -150,7 +153,6 @@ Rml::Element* find_autofocus_element(Rml::Element* start) {
struct ContextDetails {
recompui::ContextId context;
Rml::ElementDocument* document;
bool takes_input;
};
class UIState {
@@ -209,8 +211,6 @@ public:
Rml::Debugger::Initialise(context);
{
const Rml::String directory = "assets/";
struct FontFace {
const char* filename;
bool fallback_face;
@@ -225,14 +225,17 @@ public:
};
for (const FontFace& face : font_faces) {
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
auto font = banjo::get_asset_path(face.filename);
Rml::LoadFontFace(font.string(), face.fallback_face);
}
}
}
void load_documents() {
launcher_menu_controller->load_document(context);
config_menu_controller->load_document(context);
void create_menus() {
recompui::init_styling(banjo::get_asset_path("recomp.rcss"));
launcher_menu_controller->load_document();
config_menu_controller->load_document();
recompui::init_prompt_context();
}
void unload() {
@@ -264,7 +267,7 @@ public:
recompui::set_cursor_visible(mouse_is_active);
}
Rml::ElementDocument* current_document = top_input_document();
Rml::ElementDocument* current_document = top_mouse_document();
if (current_document == nullptr) {
return;
}
@@ -284,7 +287,7 @@ public:
}
void update_focus(bool mouse_moved, bool non_mouse_interacted) {
Rml::ElementDocument* current_document = top_input_document();
Rml::ElementDocument* current_document = top_mouse_document();
if (current_document == nullptr) {
return;
@@ -339,12 +342,10 @@ public:
recompui::message_box("Attemped to show the same context twice");
assert(false);
}
bool takes_input = context.takes_input();
Rml::ElementDocument* document = context.get_document();
shown_contexts.push_back(ContextDetails{
.context = context,
.document = document,
.takes_input = takes_input
.document = document
});
// auto& on_show = context.on_show;
@@ -356,6 +357,10 @@ public:
document->PullToFront();
document->Show();
recompui::Element* default_element = context.get_autofocus_element();
if (default_element) {
default_element->focus();
}
}
void hide_context(recompui::ContextId context) {
@@ -381,8 +386,12 @@ public:
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [context](auto& c){ return c.context == context; }) != shown_contexts.end();
}
bool is_context_taking_input() {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.takes_input; }) != shown_contexts.end();
bool is_context_capturing_input() {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.context.captures_input(); }) != shown_contexts.end();
}
bool is_context_capturing_mouse() {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.context.captures_mouse(); }) != shown_contexts.end();
}
bool is_any_context_shown() {
@@ -392,7 +401,17 @@ public:
Rml::ElementDocument* top_input_document() {
// Iterate backwards and stop at the first context that takes input.
for (auto it = shown_contexts.rbegin(); it != shown_contexts.rend(); it++) {
if (it->takes_input) {
if (it->context.captures_input()) {
return it->document;
}
}
return nullptr;
}
Rml::ElementDocument* top_mouse_document() {
// Iterate backwards and stop at the first context that takes input.
for (auto it = shown_contexts.rbegin(); it != shown_contexts.rend(); it++) {
if (it->context.captures_mouse()) {
return it->document;
}
}
@@ -430,7 +449,7 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
std::locale::global(std::locale::classic());
#endif
ui_state = std::make_unique<UIState>(window, interface, device);
ui_state->load_documents();
ui_state->create_menus();
}
moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
@@ -553,9 +572,28 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
bool config_was_open = recompui::is_context_shown(recompui::get_config_context_id()) || recompui::is_context_shown(recompui::get_config_sub_menu_context_id());
using clock = std::chrono::system_clock;
// TODO move these into a more appropriate place.
constexpr clock::duration start_repeat_delay = std::chrono::milliseconds{500};
constexpr clock::duration repeat_rate = std::chrono::milliseconds{50};
static clock::time_point next_repeat_time = {};
static int latest_controller_key_pressed = SDLK_UNKNOWN;
while (recompui::try_deque_event(cur_event)) {
bool context_taking_input = recompui::is_context_taking_input();
bool context_capturing_input = recompui::is_context_capturing_input();
bool context_capturing_mouse = recompui::is_context_capturing_mouse();
// Handle up button events even when input is disabled to avoid missing them during binding.
if (cur_event.type == SDL_EventType::SDL_CONTROLLERBUTTONUP) {
int sdl_key = cont_button_to_key(cur_event.cbutton);
if (sdl_key == latest_controller_key_pressed) {
latest_controller_key_pressed = SDLK_UNKNOWN;
}
}
if (!recomp::all_input_disabled()) {
bool is_mouse_input = false;
// 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: {
@@ -580,12 +618,20 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
case SDL_EventType::SDL_MOUSEBUTTONDOWN:
mouse_moved = true;
mouse_clicked = true;
is_mouse_input = true;
break;
case SDL_EventType::SDL_MOUSEBUTTONUP:
case SDL_EventType::SDL_MOUSEWHEEL:
is_mouse_input = 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);
int sdl_key = cont_button_to_key(cur_event.cbutton);
if (context_capturing_input && sdl_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
latest_controller_key_pressed = sdl_key;
next_repeat_time = clock::now() + start_repeat_delay;
}
non_mouse_interacted = true;
cont_interacted = true;
@@ -594,6 +640,11 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
case SDL_EventType::SDL_KEYDOWN:
non_mouse_interacted = true;
kb_interacted = true;
if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_F8) {
if (banjo::get_debug_mode_enabled()) {
Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
}
}
break;
case SDL_EventType::SDL_USEREVENT:
if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
@@ -616,9 +667,11 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
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);
int sdl_key = cont_axis_to_key(cur_event.caxis, axis_value);
if (context_capturing_input && sdl_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
latest_controller_key_pressed = sdl_key;
next_repeat_time = clock::now() + start_repeat_delay;
}
}
non_mouse_interacted = true;
@@ -626,12 +679,25 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
}
else if (*await_stick_return && fabsf(axis_value) < 0.15f) {
*await_stick_return = false;
// Stop pressing the current key if the axis that was released was the one triggering key presses.
int sdl_key = cont_axis_to_key(cur_event.caxis, axis_value);
if (sdl_key == latest_controller_key_pressed) {
latest_controller_key_pressed = SDLK_UNKNOWN;
}
}
break;
}
if (context_taking_input) {
RmlSDL::InputEventHandler(ui_state->context, cur_event);
// Send the event to RmlUi if this type of event is being captured.
if (is_mouse_input) {
if (context_capturing_mouse) {
RmlSDL::InputEventHandler(ui_state->context, cur_event);
}
}
else {
if (context_capturing_input) {
RmlSDL::InputEventHandler(ui_state->context, cur_event);
}
}
}
@@ -662,6 +728,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
}
} // end dequeue event loop
// Handle controller key repeats.
if (latest_controller_key_pressed != SDLK_UNKNOWN) {
clock::time_point now = clock::now();
if (now >= next_repeat_time) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(latest_controller_key_pressed), 0);
next_repeat_time += repeat_rate;
}
}
if (cont_interacted || kb_interacted || mouse_clicked) {
recompui::set_cont_active(cont_interacted);
}
@@ -721,16 +796,28 @@ void recompui::message_box(const char* msg) {
}
void recompui::show_context(ContextId context, std::string_view param) {
std::lock_guard lock{ui_state_mutex};
ContextId prev_context = recompui::try_close_current_context();
{
std::lock_guard lock{ ui_state_mutex };
// TODO call the context's on_show callback with the param.
ui_state->show_context(context);
// TODO call the context's on_show callback with the param.
ui_state->show_context(context);
}
if (prev_context != ContextId::null()) {
prev_context.open();
}
}
void recompui::hide_context(ContextId context) {
std::lock_guard lock{ui_state_mutex};
ContextId prev_context = recompui::try_close_current_context();
{
std::lock_guard lock{ ui_state_mutex };
ui_state->hide_context(context);
ui_state->hide_context(context);
}
if (prev_context != ContextId::null()) {
prev_context.open();
}
}
void recompui::hide_all_contexts() {
@@ -751,14 +838,24 @@ bool recompui::is_context_shown(ContextId context) {
return ui_state->is_context_shown(context);
}
bool recompui::is_context_taking_input() {
bool recompui::is_context_capturing_input() {
std::lock_guard lock{ui_state_mutex};
if (!ui_state) {
return false;
}
return ui_state->is_context_taking_input();
return ui_state->is_context_capturing_input();
}
bool recompui::is_context_capturing_mouse() {
std::lock_guard lock{ui_state_mutex};
if (!ui_state) {
return false;
}
return ui_state->is_context_capturing_mouse();
}
bool recompui::is_any_context_shown() {
@@ -783,10 +880,131 @@ Rml::ElementDocument* recompui::create_empty_document() {
return ui_state->context->CreateDocument();
}
void recompui::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
ui_state->render_interface.queue_image_from_bytes(src, bytes);
void recompui::queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
ui_state->render_interface.queue_image_from_bytes_file(src, bytes);
}
void recompui::queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
ui_state->render_interface.queue_image_from_bytes_rgba32(src, bytes, width, height);
}
void recompui::release_image(const std::string &src) {
Rml::ReleaseTexture(src);
}
void recompui::drop_files(const std::list<std::filesystem::path> &file_list) {
// Prevent mod installation after the game has started.
if (ultramodern::is_game_started()) {
return;
}
recompui::open_notification("Installing Mods", "Please Wait");
// TODO: Needs a progress callback and a prompt for every mod that needs to be confirmed to be overwritten.
// TODO: Run this on a background thread and use the callbacks to advance the state instead of blocking.
ModInstaller::Result result;
ModInstaller::start_mod_installation(file_list, nullptr, result);
recompui::close_prompt();
if (!result.error_messages.empty()) {
std::string error_label = std::accumulate(result.error_messages.begin(), result.error_messages.end(), std::string{},
[](const std::string &lhs, const std::string &rhs)
{
return lhs.empty() ? rhs : lhs + '\n' + rhs;
});
recompui::open_info_prompt("Error Installing Mods", error_label, "OK", {}, recompui::ButtonVariant::Tertiary);
std::vector<std::string> dummy_error_messages{};
ModInstaller::cancel_mod_installation(result, dummy_error_messages);
return;
}
std::vector<ModInstaller::Confirmation> confirmations{};
for (const ModInstaller::Installation& pending_install : result.pending_installations) {
if (pending_install.needs_overwrite_confirmation) {
// Get the mod details for the current mod at this file path.
std::string old_mod_id = recomp::mods::get_mod_id_from_filename(pending_install.mod_file.filename());
std::optional<recomp::mods::ModDetails> old_mod_details = {};
if (!old_mod_id.empty()) {
old_mod_details = recomp::mods::get_details_for_mod(old_mod_id);
}
if (old_mod_details) {
confirmations.emplace_back(ModInstaller::Confirmation {
.old_display_name = old_mod_details->display_name,
.new_display_name = pending_install.display_name,
.old_mod_id = old_mod_details->mod_id,
.new_mod_id = pending_install.mod_id,
.old_version = old_mod_details->version,
.new_version = pending_install.mod_version
});
}
else {
confirmations.emplace_back(ModInstaller::Confirmation {
.old_display_name = "?",
.new_display_name = pending_install.display_name,
.old_mod_id = "",
.new_mod_id = pending_install.mod_id,
.old_version = recomp::Version{0, 0, 0, ""},
.new_version = pending_install.mod_version
});
}
}
}
if (confirmations.empty()) {
std::vector<std::string> error_messages{};
ModInstaller::finish_mod_installation(result, error_messages);
ContextId old_context = recompui::try_close_current_context();
recompui::update_mod_list();
if (old_context != ContextId::null()) {
old_context.open();
}
// TODO show errors
}
else {
std::string prompt_text = std::accumulate(confirmations.begin(), confirmations.end(), std::string{},
[](const std::string &cur_text, const ModInstaller::Confirmation &confirmation)
{
std::string new_text{};
if (confirmation.old_display_name == confirmation.new_display_name) {
new_text = confirmation.old_display_name + " (" + confirmation.old_version.to_string() + " -> " + confirmation.new_version.to_string() + ")";
}
else {
new_text =
confirmation.old_display_name + " (" + confirmation.old_version.to_string() + ") -> " +
confirmation.new_display_name + " (" + confirmation.new_version.to_string() + ")";
}
return cur_text.empty() ? new_text : cur_text + '\n' + new_text;
});
// open prompt where confirm finishes the mod installation with the overwritten files
recompui::open_choice_prompt("Overwrite Mods?",
prompt_text,
"Overwrite",
"Cancel",
[result]() {
std::vector<std::string> error_messages{};
recomp::mods::close_mods();
ModInstaller::finish_mod_installation(result, error_messages);
ContextId old_context = recompui::try_close_current_context();
recompui::update_mod_list();
if (old_context != ContextId::null()) {
old_context.open();
}
// TODO show errors
},
[result]() {
std::vector<std::string> error_messages{};
ModInstaller::cancel_mod_installation(result, error_messages);
// TODO show errors
},
recompui::ButtonVariant::Success,
recompui::ButtonVariant::Error,
true,
""
);
}
}
+20
View File
@@ -0,0 +1,20 @@
#include "ultramodern/ultramodern.hpp"
#include "ui_utils.h"
recompui::Color recompui::lerp_color(const recompui::Color& a, const recompui::Color& b, float factor) {
return recompui::Color{
static_cast<uint8_t>(std::lerp(float(a.r), float(b.r), factor)),
static_cast<uint8_t>(std::lerp(float(a.g), float(b.g), factor)),
static_cast<uint8_t>(std::lerp(float(a.b), float(b.b), factor)),
static_cast<uint8_t>(std::lerp(float(a.a), float(b.a), factor)),
};
}
recompui::Color recompui::get_pulse_color(uint32_t pulse_milliseconds) {
uint64_t millis = std::chrono::duration_cast<std::chrono::milliseconds>(ultramodern::time_since_start()).count();
uint32_t anim_offset = millis % pulse_milliseconds;
float factor = std::abs((2.0f * anim_offset / pulse_milliseconds) - 1.0f);
return lerp_color(Color{ 23, 214, 232, 255 }, Color{ 162, 239, 246, 255 }, factor);
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef __UI_UTILS_H__
#define __UI_UTILS_H__
#include "elements/ui_types.h"
namespace recompui {
Color lerp_color(const Color& a, const Color& b, float factor);
Color get_pulse_color(uint32_t millisecond_period);
}
#endif