1.2 Release Candidate (#572)

* Remove dummy description for mod config options

* Tag release candidate version

* Apply min width to element triggering rmlui assert (#573)

* Restore 0th day (#574)

* Handle controller up events even while binding inputs to avoid spamming the bind button

* Add MouseButton UI event and use it to fix focus issue on radio, also fix sliders not moving until mouse is released

* Bump version string to 1.2.0-rc2

* mod configure menu description padding set to 16

* Added the ability for focus to set the current mod config option description (#576)

* Added the ability for focus to set the current mod config option description

* add focus to text input

* only clear description if element matches

* Fix race condition crash when setting element text, bump version to 1.2.0-rc3

* Revert "Fix race condition crash when setting element text, bump version to 1.2.0-rc3"

This reverts commit 4934a04d8a.

* Defer setting an element's text if it has children to fix race condition crash, bump version to 1.2.0-rc3

* Defer remaining set_text calls to prevent another race conditionresource

* Update runtime to fix some issues that could happen after mod conflicts
and bump version to 1.2.0-rc4

* Update runtime to fix regenerated functions using the wrong event index and bump version to 1.2.0-rc5

* Add support for suffixed .so. files. Also prevent dropping extracted dynamic libraries.

* Update RT64 commit to fix cstdint include for re-spirv.

* Bump version to rc6.

* Dummy commit to fix CI bot

* Use compile-time macro for Flatpak instead.

* Rename macro.

* Bump version to 1.2.0-rc7

* Fix define on flatpak, add cwd behavior.

* Temporarily disable current working dir code.

* Add the cmake option for flatpak.

* Bump version to 1.2.0-rc8

* Update MacPorts. (#578)

* Update MacPorts.

* Try GitHub runner.

* Deselect universal, return to blaze.

* pull universal libiconv first

* Fix controller nav issues in config menu, bump version to 1.2.0-rc9

---------

Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com>
Co-authored-by: LittleCube <littlecubehax@gmail.com>
Co-authored-by: Dario <dariosamo@gmail.com>
This commit is contained in:
Wiseguy
2025-05-04 10:33:10 -04:00
committed by GitHub
parent 14f92c41ab
commit 983d7f43f8
30 changed files with 325 additions and 76 deletions

View File

@@ -1,4 +1,4 @@
version: '2.9.3'
version: '2.10.6'
prefix: '/opt/local'
variants:
select:
@@ -6,9 +6,11 @@ variants:
- metal
deselect: x11
ports:
- name: clang-18
- name: llvm-18
- name: libiconv
select: universal
- name: libsdl2
select: universal
- name: freetype
select: universal
- name: clang-18
- name: llvm-18

View File

@@ -25,6 +25,10 @@ if (APPLE)
enable_language(OBJC OBJCXX)
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
option(RECOMP_FLATPAK "Configure the build for Flatpak compatibility." OFF)
endif()
# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24:
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW)
@@ -41,6 +45,10 @@ set(RT64_STATIC TRUE)
set(RT64_SDL_WINDOW_VULKAN TRUE)
add_compile_definitions(HLSL_CPU)
if (RECOMP_FLATPAK)
add_compile_definitions(RECOMP_FLATPAK)
endif()
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/rt64 ${CMAKE_BINARY_DIR}/rt64)
# set(BUILD_SHARED_LIBS_SAVED "${BUILD_SHARED_LIBS}")

View File

@@ -1656,6 +1656,7 @@ scrollbarhorizontal sliderbar {
flex-direction: row;
justify-content: space-between;
width: 268dp;
min-width: 1dp;
height: 128dp;
margin-right: 10dp;
}

View File

@@ -342,6 +342,8 @@ $stick-size: 200;
flex-direction: row;
justify-content: space-between;
width: space(268);
// WORKAROUND FIX: prevents RMLui assert error
min-width: 1dp;
height: space(128);
margin-right: space(10);
}

View File

@@ -28,7 +28,7 @@
"./N64Recomp us.rev1.toml",
"./RSPRecomp aspMain.us.rev1.toml",
"./RSPRecomp njpgdspMain.us.rev1.toml",
"cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_MAKE_PROGRAM=ninja -DPATCHES_C_COMPILER=clang -DPATCHES_LD=ld.lld -G Ninja -S . -B cmake-build",
"cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_MAKE_PROGRAM=ninja -DPATCHES_C_COMPILER=clang -DPATCHES_LD=ld.lld -DRECOMP_FLATPAK=ON -G Ninja -S . -B cmake-build",
"cmake --build cmake-build --config Release --target Zelda64Recompiled --parallel",
"rm -rf assets/scss",
"mkdir -p /app/bin",

View File

@@ -69,6 +69,7 @@ namespace recompui {
};
void set_config_tab(ConfigTab tab);
int config_tab_to_index(ConfigTab tab);
Rml::ElementTabSet* get_config_tabset();
Rml::Element* get_mod_tab();
void set_config_tabset_mod_nav();

View File

@@ -11,7 +11,7 @@ void Sleep_Msec(u32 ms);
extern u16 D_801F6AF0;
extern u8 D_801F6AF2;
// @recomp Patched to not wait a hardcoded amount of time for the save to complete.
// @recomp Patched to wait a much shorter amount of time for the save to complete.
RECOMP_PATCH void Sram_UpdateWriteToFlashDefault(SramContext* sramCtx) {
if (sramCtx->status == 2) {
if (SysFlashrom_IsBusy() != 0) { // if task running
@@ -23,13 +23,13 @@ RECOMP_PATCH void Sram_UpdateWriteToFlashDefault(SramContext* sramCtx) {
sramCtx->status = 4;
}
}
} else if (sramCtx->status == 4) {
// @recomp Patched to check status instead of using a hardcoded wait.
} else if (OSTIME_TO_TIMER(osGetTime() - sramCtx->startWriteOsTime) >= SECONDS_TO_TIMER_PRECISE(0, 25)) {
// @recomp Patched to wait a much shorter amount of time.
sramCtx->status = 0;
}
}
// @recomp Patched to not wait a hardcoded amount of time for the save to complete.
// @recomp Patched to wait a much shorter amount of time for the save to complete.
RECOMP_PATCH void Sram_UpdateWriteToFlashOwlSave(SramContext* sramCtx) {
if (sramCtx->status == 7) {
if (SysFlashrom_IsBusy() != 0) { // Is task running
@@ -49,8 +49,8 @@ RECOMP_PATCH void Sram_UpdateWriteToFlashOwlSave(SramContext* sramCtx) {
sramCtx->status = 4;
}
}
} else if (sramCtx->status == 4) {
// @recomp Patched to check status instead of using a hardcoded wait.
} else if (OSTIME_TO_TIMER(osGetTime() - sramCtx->startWriteOsTime) >= SECONDS_TO_TIMER_PRECISE(0, 25)) {
// @recomp Patched to wait a much shorter amount of time.
sramCtx->status = 0;
bzero(sramCtx->saveBuf, SAVE_BUFFER_SIZE);
gSaveContext.save.isOwlSave = false;

View File

@@ -287,6 +287,10 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
case SDL_EventType::SDL_DROPCOMPLETE:
recompui::drop_files(DropState.files_dropped);
break;
case SDL_EventType::SDL_CONTROLLERBUTTONUP:
// Always queue button up events to avoid missing them during binding.
recompui::queue_event(*event);
break;
default:
queue_if_enabled(event);
break;

View File

@@ -48,7 +48,7 @@
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
const std::string version_string = "1.2.0-dev";
const std::string version_string = "1.2.0-rc9";
template<typename... Ts>
void exit_error(const char* str, Ts ...args) {
@@ -601,7 +601,14 @@ int main(int argc, char** argv) {
// Force wasapi on Windows, as there seems to be some issue with sample queueing with directsound currently.
SDL_setenv("SDL_AUDIODRIVER", "wasapi", true);
#endif
//printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
#if defined(__linux__) && defined(RECOMP_FLATPAK)
// When using Flatpak, applications tend to launch from the home directory by default.
// Mods might use the current working directory to store the data, so we switch it to a directory
// with persistent data storage and write permissions under Flatpak to ensure it works.
std::error_code ec;
std::filesystem::current_path("/var/data", ec);
#endif
// Initialize SDL audio and set the output frequency.
SDL_InitSubSystem(SDL_INIT_AUDIO);

View File

@@ -48,13 +48,11 @@ namespace zelda64 {
std::filesystem::path get_program_path() {
#if defined(__APPLE__)
return get_bundle_resource_directory();
#elif defined(__linux__)
std::error_code ec;
if (std::filesystem::exists("/.flatpak-info", ec)) {
#elif defined(__linux__) && defined(RECOMP_FLATPAK)
return "/app/bin";
}
#endif
#else
return "";
#endif
}
std::filesystem::path get_asset_path(const char* asset) {

View File

@@ -37,6 +37,7 @@ namespace recompui {
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) {}
@@ -67,6 +68,8 @@ enum class ContextErrorType {
AddResourceToWrongContext,
UpdateElementWithoutContext,
UpdateElementInWrongContext,
SetTextElementWithoutContext,
SetTextElementInWrongContext,
GetResourceWithoutOpen,
GetResourceFailed,
DestroyResourceWithoutOpen,
@@ -119,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;
@@ -407,6 +416,40 @@ void recompui::ContextId::process_updates() {
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() {
@@ -514,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>());
}

View File

@@ -31,6 +31,7 @@ namespace recompui {
void add_loose_element(Element* element);
void queue_element_update(ResourceId element);
void queue_set_text(Element* element, std::string&& text);
Style* create_style();

View File

@@ -2,7 +2,7 @@
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);
@@ -14,12 +14,23 @@ namespace recompui {
case EventType::Click: {
if (is_enabled()) {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : pressed_callbacks) {
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;
}
}
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
@@ -51,6 +62,10 @@ namespace recompui {
}
}
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);
}

View File

@@ -6,6 +6,7 @@ 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;
@@ -14,6 +15,7 @@ namespace recompui {
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);
};

View File

@@ -75,6 +75,11 @@ void Element::register_event_listeners(uint32_t events_enabled) {
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)) {
base->AddEventListener(Rml::EventId::Focus, this);
base->AddEventListener(Rml::EventId::Blur, this);
@@ -152,6 +157,19 @@ void Element::set_id(const std::string& 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();
@@ -172,6 +190,22 @@ void Element::ProcessEvent(Rml::Event &event) {
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:
{
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:
@@ -343,8 +377,12 @@ std::string escape_rml(std::string_view string)
void Element::set_text(std::string_view 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.
base->SetInnerRML(escape_rml(text));
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.");

View File

@@ -6,7 +6,7 @@ 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, EventType::Update), "label", true) {
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();
@@ -37,12 +37,24 @@ namespace recompui {
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;
@@ -59,6 +71,9 @@ namespace recompui {
if (active) {
queue_update();
}
if (focus_callback != nullptr) {
focus_callback(active);
}
}
break;
case EventType::Update:
@@ -67,10 +82,6 @@ namespace recompui {
apply_styles();
queue_update();
}
if (focus_queued) {
focus_queued = false;
focus();
}
break;
default:
break;
@@ -106,7 +117,7 @@ namespace recompui {
}, val);
}
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus)) {
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus, EventType::Update)) {
set_gap(24.0f);
set_align_items(AlignItems::FlexStart);
enable_focus();
@@ -118,10 +129,18 @@ namespace recompui {
if (!options.empty()) {
if (std::get<EventFocus>(e.variant).active) {
blur();
options[index]->queue_focus();
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();
}
}
}
@@ -131,7 +150,12 @@ 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([this](uint32_t index){ option_selected(index); });
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.
@@ -157,6 +181,10 @@ 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()) {

View File

@@ -10,16 +10,16 @@ namespace recompui {
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;
bool focus_queued = false;
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);
void queue_focus() { focus_queued = true; queue_update(); }
};
class Radio : public Container {
@@ -27,6 +27,8 @@ 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);
@@ -35,6 +37,7 @@ namespace recompui {
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();
@@ -42,6 +45,7 @@ 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]; }

View File

@@ -25,7 +25,7 @@ namespace recompui {
}
}
void Slider::bar_clicked(float x, float) {
void Slider::bar_pressed(float x, float) {
update_value_from_mouse(x);
}
@@ -81,6 +81,9 @@ namespace recompui {
if (active) {
queue_update();
}
if (focus_callback != nullptr) {
focus_callback(active);
}
}
break;
case EventType::Update:
@@ -151,7 +154,7 @@ namespace recompui {
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_clicked(x, y); focus(); });
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(); });
{
@@ -160,7 +163,7 @@ 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([this](float x, float y){ bar_clicked(x, y); focus(); });
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>(bar_element, true);
@@ -219,6 +222,10 @@ namespace recompui {
value_changed_callbacks.emplace_back(callback);
}
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) {

View File

@@ -23,9 +23,10 @@ namespace recompui {
double max_value = 100.0;
double step_value = 0.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);
@@ -51,6 +52,7 @@ namespace recompui {
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

View File

@@ -16,12 +16,19 @@ 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, bool text_visible) : 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");
}
@@ -48,4 +55,7 @@ namespace recompui {
text_changed_callbacks.emplace_back(callback);
}
void TextInput::set_focus_callback(std::function<void(bool)> callback) {
focus_callback = callback;
}
};

View File

@@ -8,6 +8,7 @@ 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"; }
@@ -16,6 +17,7 @@ namespace recompui {
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

View File

@@ -33,6 +33,7 @@ namespace recompui {
Text,
Update,
Navigate,
MouseButton,
Count
};
@@ -50,6 +51,13 @@ namespace recompui {
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);
@@ -91,7 +99,14 @@ namespace recompui {
NavDirection direction;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, std::monostate>;
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;
@@ -153,6 +168,13 @@ namespace recompui {
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 {

View File

@@ -22,7 +22,7 @@ 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;
static int config_tab_to_index(recompui::ConfigTab tab) {
int recompui::config_tab_to_index(recompui::ConfigTab tab) {
switch (tab) {
case recompui::ConfigTab::General:
return 0;
@@ -472,7 +472,7 @@ 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 == config_tab_to_index(recompui::ConfigTab::Mods));
bool in_mod_tab = (tab_index == recompui::config_tab_to_index(recompui::ConfigTab::Mods));
if (in_mod_tab) {
recompui::set_config_tabset_mod_nav();
}

View File

@@ -13,6 +13,9 @@ namespace recompui {
void ConfigOptionElement::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover:
if (hover_callback == nullptr) {
break;
}
hover_callback(this, std::get<EventHover>(e.variant).active);
break;
case EventType::Update:
@@ -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;
}
@@ -73,6 +80,9 @@ ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min
slider->set_step_value(step_value);
slider->set_value(value);
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
@@ -88,6 +98,9 @@ ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view v
text_input->set_max_width(400.0f);
text_input->set_text(value);
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
@@ -100,6 +113,9 @@ ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std:
this->callback = callback;
radio = get_current_context().create_element<Radio>(this);
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);
@@ -122,19 +138,19 @@ void ConfigSubMenu::back_button_pressed() {
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());
}
}
@@ -170,8 +186,10 @@ 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);
@@ -188,14 +206,15 @@ 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_option_id(id);
option->set_name(name);
option->set_description(description);
option->set_hover_callback([this](ConfigOptionElement *option, bool active){ option_hovered(option, active); });
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);

View File

@@ -20,6 +20,7 @@ protected:
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"; }
@@ -30,6 +31,7 @@ public:
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); }
@@ -84,10 +86,10 @@ 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"; }

View File

@@ -8,6 +8,18 @@ namespace recompui {
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);
@@ -187,15 +199,8 @@ namespace recompui {
first_nrm_iterator = std::prev(result.pending_installations.end());
}
}
#if defined(_WIN32)
else if (target_path.extension() == ".dll") {
#elif defined(__linux__)
else if (target_path.extension() == ".so") {
#elif defined(__APPLE__)
else if (target_path.extension() == ".dylib") {
#else
static_assert(false, "Unimplemented for this platform."); {
#endif
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()) {
@@ -281,6 +286,13 @@ namespace recompui {
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);

View File

@@ -589,7 +589,10 @@ void ModMenu::create_mod_list() {
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);

View File

@@ -585,6 +585,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
while (recompui::try_deque_event(cur_event)) {
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.
@@ -630,13 +639,6 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
cont_interacted = true;
break;
}
case 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;
}
break;
}
case SDL_EventType::SDL_KEYDOWN:
non_mouse_interacted = true;
kb_interacted = true;