mirror of
https://github.com/BanjoRecomp/BanjoRecomp
synced 2026-06-12 04:57:11 -04:00
Update codebase with changes for Zelda 64: Recompiled 1.2
This commit is contained in:
+174
-14
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 += "<"; break;
|
||||
case '>': result += ">"; break;
|
||||
case '&': result += "&"; break;
|
||||
case '"': result += """; 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});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user