Modding menu and UI and additional mod exports (#535)

* init config opt system w/ 3 types and description support

* Move config registry/option to librecomp + added Color conf opt type

* Updated color option type styling

* Added dropdown option type

* Added TextField option type

* Button config type + callback wip

* init mod menu + bem class + button presets

* WIP mod menu, fix some warnings

* Rewrite mod details under new UI system.

* Refactored mods menu entirely.

* Remove ModMenu.scss.

* Take ownership of created pointers on Element class.

* Add styles.

* Multi-style state and disabled state propagation.

* Switch to string views.

* Convert to spaces, hook up mod enabled to toggle.

* Mod menu progress.

* Layout for mod details panel, add gap property setters

* Update RmlUi for gap property in flexbox

* Add slot_map and begin ui context

* Implement context and resource storage slotmaps

* Config submenu.

* Refactored to account for context changes.

* Turn off tab searching when submenu is open.

* Revert accidental RmlUi downgrade

* Upgrade RmlUi to 6.0 release

* Text input.

* Radio option.

* Cleanup.

* Refactor Rml document handling to use new ContextId system (prompts currently unimplemented)

* Add support for config schema.

* Split config sub menu into separate context and fix configure button, prevent infinite loop when looking for autofocus element

* Reimplement mechanism to open the config menu to a specific tab

* Begin implementing mod UI API

* Link storage to mod menu.

* Proper enum parsing.

* Enable mod reordering.

* Draggable improvements to mod menu and runtime update.

* Adjust styling of submenu.

* Mods folder button.

* Linux build fixes.

* Hook up new manifest fields to mod UI

* Add basic thumbnail parsing functionality.

* More style changes.

* Implement update event for elements

* Use RT64's texture laoding instead.

* Restore spacer animations.

* Animation API begone.

* Auto-enabled mods.

* Update runtime submodule and N64Recomp commit in CI for mod config API, remove unnecessary extern C

* Sub menu display name, assert on text input.

* Clamp delta time to fix UI disappearing on OS with timestamps that don't always increase.

* Add a state for when no mods are installed.

* Unify API function naming scheme and export relevant API functions

* Add actor update/init events and save init event (#536)

* Expose remaining property setters to mod UI API

* Implemented mod UI callbacks

* Implement actor extension data and use it for transform tagging

* Zero the memory allocated to hold extended actor data

* Implement label and textinput in mod UI API

* Patch virtual address translation to support entire extended RAM address space (#533)

* Download full target build of llvm in CI Windows runners to fix missing MIPS support and update N64Recomp CI commit

* Enable triple buffering in RT64 (#546)

* Implement controlling input capturing for mod UI contexts

* Created mod UI API functions for setting visibility, setting text, and destroying elements

* Fix errant RML tag in mod menu and insert breaks for newlines when setting element text

* Fix compilation after rebase

* Fixes for macOS

* Set the blender description manually for the UI renderer

* Created mod UI API functions for imageview elements

* Switch to designated initializers to work around missing aggregate initialization compiler support

* Update RT64 for driver bug workarounds and misc fixes

* Update RT64 to fix native sampler issues with tile copies

* Update RT64 for depth clear optimization and more native sampler changes

* Update RT64 and allow it to choose the graphics API when set to Auto

* Update runtime to allow renderers to choose the graphics API

* Update RT64 to enable early Z test shader optimization

* Implement data structure mod APIs

* Update lunasvg to increase its minimum cmake version

* Switch to runtime concatenation of function name in data API error reporting to fix Linux compilation issue

* Add missing typename to fix compilation on some compilers

* Update RT64 to fix failed assert with MSAA off

* Reimplement prompts as a separate UI context and make it so the quit game prompt doesn't bring up the config menu

* DnD prototype.

* Fix to dynamic lib path and runtime commit.

* Finish drag and drop mod installation, disable mod refresh button and code mod toggle when game starts

* Remove std::format usage and add missing <list> includes to fix Linux/MacOS compilation

* Switch to aggregate initialization for Version to work around missing implicit constructor on some compilers

* Replace use of std::bind with lambdas

* Add mod install button, put mod description in scroll container, minor mod menu tweaks

* Update runtime to fix renderer shutdown race condition

* Implement texture pack reordering

* Add mod UI API exports for slider, password input, and label radio and expose RmlUi debugger on F8

* Update runtime for mod version export

* Update runtime for save swapping mod API

* Apply recomp.rcss to mod UI contexts (fixes scrolls)

* Updated mod list styling (#561)

* Updated mod list styling

* mod entry max height

* Update RT64 for v5 texture hash

* Update runtime for mod API to get save file path

* Add special config option id to control texture pack state for code mods

* Update runtime for mod default enabled state

* Add exports for stars' display lists (#563)

* Update runtime to fix default value of enabled_by_default

* Update runtime to allow NULL recomp_free

* Implement navigation and focus styling for new UI framework (no manual overrides yet)

* Fix the previous scissor state bleeding when drawing the RmlUi output onto the swapchain buffer

* Use a multiple file select dialog for mod install button

* Add mod export for loading UI image from bytes (png/dds)

* Manual navigation in UI framework and WIP mod menu navigation

* Repeat key events when holding down controller inputs for UI navigation

* Patch AnimationContext_SetLoadFrame to allow custom animations (#564)

* Close context when showing or hiding a context and reopen afterwards to prevent deadlocks

* Add quotes around xdg-open and open commands to support paths with spaces

* Update RT64 for high precision texture coordinates when using texture replacements

* Add support for built-in mods and convert D-Pad to a built-in mod (#567)

* Add embedded mod (using mm_recomp_draw_distance as an example).

* Update runtime after merge

* Experiment with removing the D-Pad.

* Add event needed for dpad as mod, revert remaining changes in built-in patches for dpad

* Add built-in dpad mod, add remaining event calls to input.c

* Add built-in mods readme

---------

Co-authored-by: Dario <dariosamo@gmail.com>

* Fixing navigation of mods menu.

* Focused state for mod entry.

* Prevent hover styling and focus on input elements when disabled

* Fix up/down navigation on text input elements

* Set mod tab to navigate down to first mod, fix redundant mod scanning

* Remove more redundant mod scanning and fix mods being scanned during gameplay

* Update runtime for mod folder export

* Improve radio navigation and setup mod config submenu navigation setup

* Restore fd anywhere export functionality (#570)

* fix fd

* add comment back in

* Make config tabset navigate down to first mod entry when mod menu is open, make mod configure screen focus on configure button after closing

* Add navigation exports to mod UI API

* Fix opening the config menu via keyboard/controller causing a double animation warning in RmlUi

---------

Co-authored-by: Dario <dariosamo@gmail.com>
Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com>
Co-authored-by: Garrett Cox <garrettjcox@gmail.com>
Co-authored-by: David Chavez <david@dcvz.io>
Co-authored-by: danielryb <59661841+danielryb@users.noreply.github.com>
Co-authored-by: Reonu <danileon95@gmail.com>
Co-authored-by: LittleCube <littlecubehax@gmail.com>
This commit is contained in:
Wiseguy
2025-04-28 02:49:43 -04:00
committed by Mr-Wiseguy
parent 8ec7b282e3
commit d766cf328f
113 changed files with 15316 additions and 3987 deletions
+25 -7
View File
@@ -42,6 +42,10 @@ static struct {
bool rumble_active;
} InputState;
static struct {
std::list<std::filesystem::path> files_dropped;
} DropState;
std::atomic<recomp::InputDevice> scanning_device = recomp::InputDevice::COUNT;
std::atomic<recomp::InputField> scanned_input;
@@ -103,7 +107,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
SDL_KeyboardEvent* keyevent = &event->key;
// Skip repeated events when not in the menu
if (recompui::get_current_menu() == recompui::Menu::None &&
if (!recompui::is_context_capturing_input() &&
event->key.repeat) {
break;
}
@@ -156,10 +160,6 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
return true;
}
if (recompui::get_current_menu() != recompui::Menu::Config) {
recompui::set_current_menu(recompui::Menu::Config);
}
zelda64::open_quit_game_prompt();
recompui::activate_mouse();
break;
@@ -275,6 +275,18 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
InputState.pending_mouse_delta[0] += motion_event->xrel;
InputState.pending_mouse_delta[1] += motion_event->yrel;
}
queue_if_enabled(event);
break;
case SDL_EventType::SDL_DROPBEGIN:
DropState.files_dropped.clear();
break;
case SDL_EventType::SDL_DROPFILE:
DropState.files_dropped.emplace_back(std::filesystem::path(std::u8string_view((const char8_t *)(event->drop.file))));
SDL_free(event->drop.file);
break;
case SDL_EventType::SDL_DROPCOMPLETE:
recompui::drop_files(DropState.files_dropped);
break;
default:
queue_if_enabled(event);
break;
@@ -284,6 +296,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
void recomp::handle_events() {
SDL_Event cur_event;
static bool started = false;
static bool exited = false;
while (SDL_PollEvent(&cur_event) && !exited) {
exited = sdl_event_filter(nullptr, &cur_event);
@@ -300,6 +313,11 @@ void recomp::handle_events() {
SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE);
SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE);
}
if (!started && ultramodern::is_game_started()) {
started = true;
recompui::process_game_started();
}
}
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;
@@ -711,8 +729,8 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
}
bool recomp::game_input_disabled() {
// Disable input if any menu is open.
return recompui::get_current_menu() != recompui::Menu::None;
// Disable input if any menu that blocks input is open.
return recompui::is_context_capturing_input();
}
bool recomp::all_input_disabled() {
+237
View File
@@ -0,0 +1,237 @@
#include <vector>
#include <mutex>
#include "slot_map.h"
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "ultramodern/error_handling.hpp"
#include "recomp_ui.h"
#include "recomp_data.h"
#include "../patches/actor_funcs.h"
struct ExtensionInfo {
// Either the actor's type ID, or 0xFFFFFFFF if this is for generic data.
uint32_t actor_type;
// The offset from either the start of the actor's data or the start of the actor's specific extension data depending on the value of actor_type.
uint32_t data_offset;
};
struct ExtensionData {
uint32_t actor_spawn_index;
PTR(void) data_addr;
};
std::mutex actor_data_mutex{};
// The total size of actor-specific extension data for each actor type.
std::vector<uint32_t> actor_data_sizes{};
// The total size of all generic actor extension data.
uint32_t generic_data_size;
// The registered actor extensions.
std::vector<ExtensionInfo> actor_extensions{};
// The extension data for every actor.
using actor_data_map_t = dod::slot_map32<ExtensionData>;
actor_data_map_t actor_data{};
// The number of actors spawned since the last reset.
uint32_t actor_spawn_count = 0;
// Whether or not extensions can be registered at this time.
bool can_register = false;
// Debug counters.
size_t alloc_count = 0;
size_t free_count = 0;
void recomputil::init_extended_actor_data() {
std::lock_guard lock{ actor_data_mutex };
actor_data_sizes.clear();
generic_data_size = 0;
actor_extensions.clear();
actor_data.reset();
actor_spawn_count = 0;
can_register = true;
// Create a dummy extension so the first extension handle is nonzero, should help catch bugs.
actor_extensions.push_back({});
}
void recomputil::reset_actor_data() {
std::lock_guard lock{ actor_data_mutex };
actor_data.reset();
actor_spawn_count = 0;
assert(alloc_count == free_count);
alloc_count = 0;
free_count = 0;
}
constexpr uint32_t round_up_16(uint32_t value) {
return (value + 15) & (~15);
}
extern "C" void recomp_register_actor_extension(uint8_t* rdram, recomp_context* ctx) {
u32 actor_type = _arg<0, u32>(rdram, ctx);
u32 size = _arg<1, u32>(rdram, ctx);
if (!can_register) {
recompui::message_box("Fatal error in mod - attempted to register actor extension data after actors have been spawned.");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
if (actor_data_sizes.size() <= actor_type) {
actor_data_sizes.resize(2 * actor_type);
}
// Increase the actor type's extension data size by the provided size (rounded up to a multiple of 16).
uint32_t data_offset = actor_data_sizes[actor_type];
actor_data_sizes[actor_type] += round_up_16(size);
// Register the extension.
uint32_t ret = static_cast<uint32_t>(actor_extensions.size());
actor_extensions.emplace_back(ExtensionInfo{.actor_type = actor_type, .data_offset = data_offset});
// printf("Registered actor extension data for type %u (size 0x%08X, offset 0x%08X)\n", actor_type, size, data_offset);
_return<u32>(ctx, ret);
}
extern "C" void recomp_register_actor_extension_generic(uint8_t* rdram, recomp_context* ctx) {
u32 size = _arg<0, u32>(rdram, ctx);
// Increase the generic extension data size by the provided size (rounded up to a multiple of 16).
uint32_t data_offset = generic_data_size;
generic_data_size += round_up_16(size);
// Register the extension.
uint32_t ret = static_cast<uint32_t>(actor_extensions.size());
actor_extensions.emplace_back(ExtensionInfo{.actor_type = 0xFFFFFFFFU, .data_offset = data_offset});
// printf("Registered generic actor extension data (size 0x%08X, offset 0x%08X)\n", size, data_offset);
_return<u32>(ctx, ret);
}
extern "C" void recomp_clear_all_actor_data(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
(void)ctx;
recomputil::reset_actor_data();
}
extern "C" void recomp_create_actor_data(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
can_register = false;
// Determine the number of bytes to allocate based on the actor type's extensions and the generic extensions.
u32 actor_type = _arg<0, u32>(rdram, ctx);
u32 alloc_size = generic_data_size;
[[maybe_unused]] u32 type_data_size = 0;
if (actor_type < actor_data_sizes.size()) {
type_data_size = actor_data_sizes[actor_type];
alloc_size += type_data_size;
}
// Allocate the extension data if it's of nonzero size.
PTR(void) data_ptr = NULLPTR;
if (alloc_size != 0) {
void* data = recomp::alloc(rdram, alloc_size);
alloc_count++;
data_ptr = reinterpret_cast<uint8_t*>(data) - rdram + 0xFFFFFFFF80000000U;
// Zero the allocated memory.
// A memset should be fine here since this data is aligned, but use a byteswapped loop just to be safe.
for (size_t i = 0; i < alloc_size; i++) {
MEM_B(i, data_ptr) = 0;
}
}
// Add the actor's fields to the actor data slotmap.
u32 spawn_index = actor_spawn_count++;
dod::slot_map_key32<ExtensionData> key = actor_data.emplace(ExtensionData{.actor_spawn_index = spawn_index, .data_addr = data_ptr});
// printf("Allocated actor data: address 0x%08X with 0x%08X bytes total (0x%08X bytes generic and 0x%08X bytes specific), handle 0x%08X, spawn index %d\n",
// data_ptr, alloc_size, generic_data_size, type_data_size, key.raw, spawn_index);
_return<u32>(ctx, key.raw);
}
extern "C" void recomp_destroy_actor_data(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
u32 actor_handle = _arg<0, u32>(rdram, ctx);
actor_data_map_t::key actor_key{actor_handle};
ExtensionData* data = actor_data.get(actor_key);
if (data != nullptr) {
// printf("Freeing actor data: address 0x%08X handle 0x%08X\n", data->data_addr, actor_handle);
if (data->data_addr != NULLPTR) {
recomp::free(rdram, TO_PTR(void, data->data_addr));
free_count++;
}
actor_data.erase(actor_data_map_t::key{actor_handle});
}
else {
// Not an irrecoverable error, but catch it in debug mode with an assert to help find bugs.
assert(false);
}
}
extern "C" void recomp_get_actor_data(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
u32 actor_handle = _arg<0, u32>(rdram, ctx);
u32 extension_handle = _arg<1, u32>(rdram, ctx);
u32 actor_type = _arg<2, u32>(rdram, ctx);
// Check if the extension handle is valid.
if (extension_handle == 0 || extension_handle >= actor_extensions.size()) {
_return<PTR(void)>(ctx, NULLPTR);
return;
}
ExtensionInfo& extension = actor_extensions[extension_handle];
bool generic_extension = extension.actor_type == 0xFFFFFFFFU;
// Check if the extension is generic or for the provided actor type.
if (!generic_extension && extension.actor_type != actor_type) {
_return<PTR(void)>(ctx, NULLPTR);
return;
}
actor_data_map_t::key actor_key{actor_handle};
ExtensionData* data = actor_data.get(actor_key);
// Check if actor handle is valid.
if (data == nullptr) {
_return<PTR(void)>(ctx, NULLPTR);
return;
}
// Calculate the address for this specific extension's data.
PTR(void) base_address = data->data_addr;
u32 offset = extension.data_offset;
// Specific actor data is after generic actor data, so increase the offset by the total generic actor data if this isn't generic data.
if (!generic_extension) {
offset += generic_data_size;
}
PTR(void) ret = base_address + offset;
_return<PTR(void)>(ctx, ret);
}
extern "C" void recomp_get_actor_spawn_index(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
u32 actor_handle = _arg<0, u32>(rdram, ctx);
actor_data_map_t::key actor_key{actor_handle};
ExtensionData* data = actor_data.get(actor_key);
// Check if actor handle is valid.
if (data == nullptr) {
_return<u32>(ctx, 0xFFFFFFFFU);
return;
}
_return<u32>(ctx, data->actor_spawn_index);
}
+727
View File
@@ -0,0 +1,727 @@
#include <vector>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include "slot_map.h"
#include "recomp_data.h"
#include "recomp_ui.h"
#include "librecomp/helpers.hpp"
#include "librecomp/overlays.hpp"
#include "librecomp/addresses.hpp"
#include "ultramodern/error_handling.hpp"
template <typename KeyType, typename ValueType>
class LockedMap {
private:
std::mutex mutex{};
std::unordered_map<KeyType, ValueType> map{};
public:
bool get(const KeyType& key, ValueType& out) {
std::lock_guard lock{mutex};
auto find_it = map.find(key);
if (find_it == map.end()) {
return false;
}
out = find_it->second;
return true;
}
bool insert(const KeyType& key, ValueType val) {
std::lock_guard lock{mutex};
auto ret = map.insert_or_assign(key, val);
return ret.second;
}
bool erase(const KeyType& key) {
std::lock_guard lock{mutex};
size_t num_erased = map.erase(key);
return num_erased != 0;
}
void clear() {
std::lock_guard lock{mutex};
map.clear();
}
bool erase_first(ValueType& out) {
std::lock_guard lock{mutex};
auto it = map.begin();
if (it == map.end()) {
return false;
}
out = it->second;
map.erase(it);
return true;
}
bool contains(const KeyType& key) {
std::lock_guard lock{mutex};
return map.contains(key);
}
size_t size() {
std::lock_guard lock{mutex};
return map.size();
}
};
template <typename KeyType>
class LockedSet {
private:
std::mutex mutex{};
std::unordered_set<KeyType> set{};
public:
bool contains(const KeyType& key) {
std::lock_guard lock{mutex};
return set.contains(key);
}
bool insert(const KeyType& key) {
std::lock_guard lock{mutex};
auto it = set.insert(key);
return it.second;
}
bool erase(const KeyType& key) {
std::lock_guard lock{mutex};
size_t num_erased = set.erase(key);
return num_erased != 0;
}
void clear() {
std::lock_guard lock{mutex};
set.clear();
}
size_t size() {
std::lock_guard lock{mutex};
return set.size();
}
};
template <typename ValueType>
class LockedSlotmap {
private:
std::mutex mutex{};
dod::slot_map32<ValueType> map{};
using key_t = typename dod::slot_map32<ValueType>::key;
public:
bool get(uint32_t key, ValueType** out) {
std::lock_guard lock{mutex};
ValueType* ret = map.get(key_t{key});
*out = ret;
return ret != nullptr;
}
uint32_t create() {
std::lock_guard lock{mutex};
return map.emplace().raw;
}
bool erase(uint32_t key) {
std::lock_guard lock{mutex};
if (!map.has_key(key_t{ key })) {
return false;
}
map.erase(key_t{ key });
return true;
}
void clear() {
std::lock_guard lock{mutex};
map.clear();
}
bool erase_first(ValueType& out) {
std::lock_guard lock{mutex};
auto it = map.items().begin();
if (it == map.items().end()) {
return false;
}
out = it->second;
map.erase(it->first);
return true;
}
size_t size() {
std::lock_guard lock{mutex};
return map.size();
}
};
using U32ValueMap = LockedMap<uint32_t, uint32_t>;
using U32MemoryMap = std::pair<LockedMap<uint32_t, PTR(void)>, u32>;
using U32HashSet = LockedSet<uint32_t>;
using U32Slotmap = LockedSlotmap<uint32_t>;
using MemorySlotmap = std::pair<LockedSlotmap<PTR(void)>, u32>;
LockedSlotmap<U32ValueMap> u32_value_hashmaps{};
LockedSlotmap<U32MemoryMap> u32_memory_hashmaps{};
LockedSlotmap<U32HashSet> u32_hashsets{};
LockedSlotmap<U32Slotmap> u32_slotmaps{};
LockedSlotmap<MemorySlotmap> memory_slotmaps{};
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
static void show_fatal_error_message_box(const char* funcname, const char* errstr) {
std::string message = std::string{"Fatal error in mod - "} + funcname + " : " + errstr;
recompui::message_box(message.c_str());
}
#define HANDLE_INVALID_ERROR() \
show_fatal_error_message_box(__FUNCTION__, "handle is invalid"); \
assert(false); \
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
#define SLOTMAP_KEY_INVALID_ERROR() \
show_fatal_error_message_box(__FUNCTION__, "slotmap key is invalid"); \
assert(false); \
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
// u32 -> 32-bit value hashmap.
void recomputil_create_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_value_hashmaps.create());
}
void recomputil_destroy_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_value_hashmaps.erase(mapkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_value_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->contains(key));
}
void recomputil_u32_value_hashmap_insert(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->insert(key, value));
}
void recomputil_u32_value_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t ret;
if (map->get(key, ret)) {
MEM_W(0, val_out) = ret;
_return(ctx, 1);
return;
}
else {
_return(ctx, 0);
return;
}
}
void recomputil_u32_value_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->erase(key));
}
void recomputil_u32_value_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->size()));
}
// u32 -> memory hashmap.
void recomputil_create_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t element_size = _arg<0, uint32_t>(rdram, ctx);
// Create the map.
uint32_t map_key = u32_memory_hashmaps.create();
// Retrieve the map and set its element size to the provided value.
U32MemoryMap* map;
u32_memory_hashmaps.get(map_key, &map);
map->second = element_size;
// Return the created map's key.
_return(ctx, map_key);
}
void recomputil_destroy_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
// Retrieve the map.
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free all of the entries in the map.
PTR(void) cur_mem;
while (map->first.erase_first(cur_mem)) {
recomp::free(rdram, TO_PTR(void, cur_mem));
}
// Destroy the map itself.
u32_memory_hashmaps.erase(mapkey);
}
void recomputil_u32_memory_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->first.contains(key));
}
void recomputil_u32_memory_hashmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Check if the map contains the key already to prevent inserting it twice.
PTR(void) dummy;
if (map->first.get(key, dummy)) {
_return(ctx, 0);
return;
}
// Allocate the map's size and return the pointer.
void* mem = recomp::alloc(rdram, map->second);
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
// Zero the memory.
for (size_t i = 0; i < map->second; i++) {
MEM_B(i, addr) = 0;
}
PTR(void) ret = static_cast<PTR(void)>(addr);
map->first.insert(key, ret);
_return(ctx, 1);
}
void recomputil_u32_memory_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void) ret;
if (map->first.get(key, ret)) {
_return(ctx, ret);
return;
}
else {
_return(ctx, NULLPTR);
return;
}
}
void recomputil_u32_memory_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free the memory for this key if the key exists.
PTR(void) addr;
bool has_value = map->first.get(key, addr);
if (has_value) {
void* mem = TO_PTR(void, addr);
recomp::free(rdram, mem);
}
_return(ctx, map->first.erase(key));
}
void recomputil_u32_memory_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->first.size()));
}
// u32 hashset.
void recomputil_create_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_hashsets.create());
}
void recomputil_destroy_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_hashsets.erase(setkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_hashset_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->contains(key));
}
void recomputil_u32_hashset_insert(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->insert(key));
}
void recomputil_u32_hashset_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->erase(key));
}
void recomputil_u32_hashset_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(set->size()));
}
// u32 value slotmap.
void recomputil_create_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_slotmaps.create());
}
void recomputil_destroy_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_slotmaps.erase(mapkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* dummy_ptr;
_return(ctx, map->get(key, &dummy_ptr));
}
void recomputil_u32_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->create());
}
void recomputil_u32_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* ret;
if (!map->get(key, &ret)) {
_return(ctx, 0);
}
MEM_W(0, val_out) = *ret;
_return(ctx, 1);
}
void recomputil_u32_slotmap_set(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* value_ptr;
if (!map->get(key, &value_ptr)) {
_return(ctx, 0);
}
*value_ptr = value;
_return(ctx, 1);
}
void recomputil_u32_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
if (!map->erase(key)) {
_return(ctx, 0);
}
_return(ctx, 1);
}
void recomputil_u32_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->size()));
}
// memory slotmap.
void recomputil_create_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, memory_slotmaps.create());
}
void recomputil_destroy_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
// Retrieve the map.
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free all of the entries in the map.
PTR(void) cur_mem;
while (map->first.erase_first(cur_mem)) {
recomp::free(rdram, TO_PTR(void, cur_mem));
}
// Destroy the map itself.
memory_slotmaps.erase(mapkey);
}
void recomputil_memory_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void)* dummy_ptr;
_return(ctx, map->first.get(key, &dummy_ptr));
}
void recomputil_memory_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Create the slotmap element.
u32 key = map->first.create();
// Allocate the map's element size.
void* mem = recomp::alloc(rdram, map->second);
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
// Zero the memory.
for (size_t i = 0; i < map->second; i++) {
MEM_B(i, addr) = 0;
}
// Store the allocated pointer.
PTR(void)* value_ptr;
map->first.get(key, &value_ptr);
MEM_W(0, *value_ptr) = addr;
// Return the key.
_return(ctx, key);
}
void recomputil_memory_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void)* ret;
if (!map->first.get(key, &ret)) {
SLOTMAP_KEY_INVALID_ERROR();
}
MEM_W(0, val_out) = *ret;
}
void recomputil_memory_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free the memory for this key if the key exists.
PTR(void)* addr;
bool has_value = map->first.get(key, &addr);
if (has_value) {
void* mem = TO_PTR(void, addr);
recomp::free(rdram, mem);
}
_return(ctx, map->first.erase(key));
}
void recomputil_memory_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->first.size()));
}
// Exports.
void recomputil::register_data_api_exports() {
REGISTER_FUNC(recomputil_create_u32_value_hashmap);
REGISTER_FUNC(recomputil_destroy_u32_value_hashmap);
REGISTER_FUNC(recomputil_u32_value_hashmap_contains);
REGISTER_FUNC(recomputil_u32_value_hashmap_insert);
REGISTER_FUNC(recomputil_u32_value_hashmap_get);
REGISTER_FUNC(recomputil_u32_value_hashmap_erase);
REGISTER_FUNC(recomputil_u32_value_hashmap_size);
REGISTER_FUNC(recomputil_create_u32_memory_hashmap);
REGISTER_FUNC(recomputil_destroy_u32_memory_hashmap);
REGISTER_FUNC(recomputil_u32_memory_hashmap_contains);
REGISTER_FUNC(recomputil_u32_memory_hashmap_create);
REGISTER_FUNC(recomputil_u32_memory_hashmap_get);
REGISTER_FUNC(recomputil_u32_memory_hashmap_erase);
REGISTER_FUNC(recomputil_u32_memory_hashmap_size);
REGISTER_FUNC(recomputil_create_u32_hashset);
REGISTER_FUNC(recomputil_destroy_u32_hashset);
REGISTER_FUNC(recomputil_u32_hashset_contains);
REGISTER_FUNC(recomputil_u32_hashset_insert);
REGISTER_FUNC(recomputil_u32_hashset_erase);
REGISTER_FUNC(recomputil_u32_hashset_size);
REGISTER_FUNC(recomputil_create_u32_slotmap);
REGISTER_FUNC(recomputil_destroy_u32_slotmap);
REGISTER_FUNC(recomputil_u32_slotmap_contains);
REGISTER_FUNC(recomputil_u32_slotmap_create);
REGISTER_FUNC(recomputil_u32_slotmap_get);
REGISTER_FUNC(recomputil_u32_slotmap_set);
REGISTER_FUNC(recomputil_u32_slotmap_erase);
REGISTER_FUNC(recomputil_u32_slotmap_size);
REGISTER_FUNC(recomputil_create_memory_slotmap);
REGISTER_FUNC(recomputil_destroy_memory_slotmap);
REGISTER_FUNC(recomputil_memory_slotmap_contains);
REGISTER_FUNC(recomputil_memory_slotmap_create);
REGISTER_FUNC(recomputil_memory_slotmap_get);
REGISTER_FUNC(recomputil_memory_slotmap_erase);
REGISTER_FUNC(recomputil_memory_slotmap_size);
}
+15 -31
View File
@@ -27,6 +27,7 @@
#include "zelda_render.h"
#include "zelda_support.h"
#include "zelda_game.h"
#include "recomp_data.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
#include "librecomp/mods.hpp"
@@ -37,6 +38,8 @@
#include "../../patches/sound.h"
#include "../../patches/misc_funcs.h"
#include "mods/mm_recomp_dpad_builtin.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
@@ -544,15 +547,17 @@ void release_preload(PreloadContext& context) {
#endif
void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
zelda64::renderer::enable_texture_pack(mod);
zelda64::renderer::enable_texture_pack(context, mod);
}
void disable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
void disable_texture_pack(recomp::mods::ModContext&, const recomp::mods::ModHandle& mod) {
zelda64::renderer::disable_texture_pack(mod);
}
void reorder_texture_pack(recomp::mods::ModContext&) {
zelda64::renderer::trigger_texture_pack_update();
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
int main(int argc, char** argv) {
@@ -614,6 +619,8 @@ int main(int argc, char** argv) {
recomp::register_game(game);
}
recomp::mods::register_embedded_mod("mm_recomp_dpad_builtin", { (const uint8_t*)(mm_recomp_dpad_builtin), std::size(mm_recomp_dpad_builtin)});
REGISTER_FUNC(recomp_get_window_resolution);
REGISTER_FUNC(recomp_get_target_aspect_ratio);
REGISTER_FUNC(recomp_get_target_framerate);
@@ -627,9 +634,12 @@ int main(int argc, char** argv) {
REGISTER_FUNC(recomp_get_mouse_deltas);
REGISTER_FUNC(recomp_get_inverted_axes);
REGISTER_FUNC(recomp_get_analog_inverted_axes);
recompui::register_ui_exports();
recomputil::register_data_api_exports();
zelda64::register_overlays();
zelda64::register_patches();
recomputil::init_extended_actor_data();
zelda64::load_config();
recomp::rsp::callbacks_t rsp_callbacks{
@@ -678,39 +688,13 @@ int main(int argc, char** argv) {
.allow_runtime_toggle = true,
.on_enabled = enable_texture_pack,
.on_disabled = disable_texture_pack,
.on_reordered = reorder_texture_pack,
};
auto texture_pack_content_type_id = recomp::mods::register_mod_content_type(texture_pack_content_type);
// Register the .rtz texture pack file format with the previous content type as its only allowed content type.
recomp::mods::register_mod_container_type("rtz", std::vector{ texture_pack_content_type_id }, false);
recomp::mods::scan_mods();
printf("Found mods:\n");
for (const auto& mod : recomp::mods::get_mod_details("mm")) {
printf(" %s(%s)\n", mod.mod_id.c_str(), mod.version.to_string().c_str());
if (!mod.authors.empty()) {
printf(" Authors: %s", mod.authors[0].c_str());
for (size_t author_index = 1; author_index < mod.authors.size(); author_index++) {
const std::string& author = mod.authors[author_index];
printf(", %s", author.c_str());
}
printf("\n");
printf(" Runtime toggleable: %d\n", mod.runtime_toggleable);
}
if (!mod.dependencies.empty()) {
printf(" Dependencies: %s:%s", mod.dependencies[0].mod_id.c_str(), mod.dependencies[0].version.to_string().c_str());
for (size_t dep_index = 1; dep_index < mod.dependencies.size(); dep_index++) {
const recomp::mods::Dependency& dep = mod.dependencies[dep_index];
printf(", %s:%s", dep.mod_id.c_str(), dep.version.to_string().c_str());
}
printf("\n");
}
// TODO load all mods as a temporary solution to not having a UI yet.
recomp::mods::enable_mod(mod.mod_id, true);
}
printf("\n");
recomp::start(
project_version,
{},
+163 -40
View File
@@ -1,10 +1,12 @@
#include <memory>
#include <cstring>
#include <variant>
#include <algorithm>
#define HLSL_CPU
#include "hle/rt64_application.h"
#include "rt64_render_hooks.h"
#include "overloaded.h"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
@@ -13,12 +15,6 @@
#include "recomp_ui.h"
#include "concurrentqueue.h"
// Helper class for variant visiting.
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
static bool sample_positions_supported = false;
static bool high_precision_fb_enabled = false;
@@ -27,14 +23,25 @@ static uint8_t DMEM[0x1000];
static uint8_t IMEM[0x1000];
struct TexturePackEnableAction {
std::filesystem::path path;
std::string mod_id;
};
struct TexturePackDisableAction {
std::filesystem::path path;
std::string mod_id;
};
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction>;
struct TexturePackSecondaryEnableAction {
std::string mod_id;
};
struct TexturePackSecondaryDisableAction {
std::string mod_id;
};
struct TexturePackUpdateAction {
};
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction, TexturePackSecondaryEnableAction, TexturePackSecondaryDisableAction, TexturePackUpdateAction>;
static moodycamel::ConcurrentQueue<TexturePackAction> texture_pack_action_queue;
@@ -171,6 +178,7 @@ void set_application_user_config(RT64::Application* application, const ultramode
application->userConfig.refreshRate = to_rt64(config.rr_option);
application->userConfig.refreshRateTarget = config.rr_manual_value;
application->userConfig.internalColorFormat = to_rt64(config.hpfb_option);
application->userConfig.displayBuffering = RT64::UserConfiguration::DisplayBuffering::Triple;
}
ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
@@ -192,6 +200,23 @@ ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResu
std::exit(EXIT_FAILURE);
}
ultramodern::renderer::GraphicsApi map_graphics_api(RT64::UserConfiguration::GraphicsAPI api) {
switch (api) {
case RT64::UserConfiguration::GraphicsAPI::D3D12:
return ultramodern::renderer::GraphicsApi::D3D12;
case RT64::UserConfiguration::GraphicsAPI::Vulkan:
return ultramodern::renderer::GraphicsApi::Vulkan;
case RT64::UserConfiguration::GraphicsAPI::Metal:
return ultramodern::renderer::GraphicsApi::Metal;
case RT64::UserConfiguration::GraphicsAPI::Automatic:
return ultramodern::renderer::GraphicsApi::Auto;
}
fprintf(stderr, "Unhandled `RT64::UserConfiguration::GraphicsAPI` ?\n");
assert(false);
std::exit(EXIT_FAILURE);
}
zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool debug) {
static unsigned char dummy_rom_header[0x40];
recompui::set_render_hooks();
@@ -266,9 +291,8 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
case ultramodern::renderer::GraphicsApi::Metal:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Metal;
break;
default:
case ultramodern::renderer::GraphicsApi::Auto:
// Don't override if auto is selected.
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Automatic;
break;
}
@@ -278,6 +302,8 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
thread_id = window_handle.thread_id;
#endif
setup_result = map_setup_result(app->setup(thread_id));
// Get the API that RT64 chose.
chosen_api = map_graphics_api(app->chosenGraphicsAPI);
if (setup_result != ultramodern::renderer::SetupResult::Success) {
app = nullptr;
return;
@@ -306,32 +332,7 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
zelda64::renderer::RT64Context::~RT64Context() = default;
void zelda64::renderer::RT64Context::send_dl(const OSTask* task) {
bool packs_disabled = false;
TexturePackAction cur_action;
while (texture_pack_action_queue.try_dequeue(cur_action)) {
std::visit(overloaded{
[&](TexturePackDisableAction& to_disable) {
enabled_texture_packs.erase(to_disable.path);
packs_disabled = true;
},
[&](TexturePackEnableAction& to_enable) {
enabled_texture_packs.insert(to_enable.path);
// Load the pack now if no packs have been disabled.
if (!packs_disabled) {
app->textureCache->loadReplacementDirectory(to_enable.path);
}
}
}, cur_action);
}
// If any packs were disabled, unload all packs and load all the active ones.
if (packs_disabled) {
app->textureCache->clearReplacementDirectories();
for (const std::filesystem::path& cur_pack_path : enabled_texture_packs) {
app->textureCache->loadReplacementDirectory(cur_pack_path);
}
}
check_texture_pack_actions();
app->state->rsp->reset();
app->interpreter->loadUCodeGBI(task->t.ucode & 0x3FFFFFF, task->t.ucode_data & 0x3FFFFFF, true);
app->processDisplayLists(app->core.RDRAM, task->t.data_ptr & 0x3FFFFFF, 0, true);
@@ -397,6 +398,66 @@ float zelda64::renderer::RT64Context::get_resolution_scale() const {
}
}
void zelda64::renderer::RT64Context::check_texture_pack_actions() {
bool packs_changed = false;
TexturePackAction cur_action;
while (texture_pack_action_queue.try_dequeue(cur_action)) {
std::visit(overloaded{
[&](TexturePackDisableAction &to_disable) {
enabled_texture_packs.erase(to_disable.mod_id);
packs_changed = true;
},
[&](TexturePackEnableAction &to_enable) {
enabled_texture_packs.insert(to_enable.mod_id);
packs_changed = true;
},
[&](TexturePackSecondaryDisableAction &to_override_disable) {
secondary_disabled_texture_packs.insert(to_override_disable.mod_id);
packs_changed = true;
},
[&](TexturePackSecondaryEnableAction &to_override_enable) {
secondary_disabled_texture_packs.erase(to_override_enable.mod_id);
packs_changed = true;
},
[&](TexturePackUpdateAction &) {
packs_changed = true;
}
}, cur_action);
}
// If any packs were disabled, unload all packs and load all the active ones.
if (packs_changed) {
// Sort the enabled texture packs in reverse order so that earlier ones override later ones.
std::vector<std::string> sorted_texture_packs{};
sorted_texture_packs.reserve(enabled_texture_packs.size());
for (const std::string& mod : enabled_texture_packs) {
if (!secondary_disabled_texture_packs.contains(mod)) {
sorted_texture_packs.emplace_back(mod);
}
}
std::sort(sorted_texture_packs.begin(), sorted_texture_packs.end(),
[](const std::string& lhs, const std::string& rhs) {
return recomp::mods::get_mod_order_index(lhs) > recomp::mods::get_mod_order_index(rhs);
}
);
// Build the path list from the sorted mod list.
std::vector<RT64::ReplacementDirectory> replacement_directories;
replacement_directories.reserve(enabled_texture_packs.size());
for (const std::string &mod_id : sorted_texture_packs) {
replacement_directories.emplace_back(RT64::ReplacementDirectory(recomp::mods::get_mod_filename(mod_id)));
}
if (!replacement_directories.empty()) {
app->textureCache->loadReplacementDirectories(replacement_directories);
}
else {
app->textureCache->clearReplacementDirectories();
}
}
}
RT64::UserConfiguration::Antialiasing zelda64::renderer::RT64MaxMSAA() {
return device_max_msaa;
}
@@ -413,10 +474,72 @@ bool zelda64::renderer::RT64HighPrecisionFBEnabled() {
return high_precision_fb_enabled;
}
void zelda64::renderer::enable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_root_path});
void zelda64::renderer::trigger_texture_pack_update() {
texture_pack_action_queue.enqueue(TexturePackUpdateAction{});
}
void zelda64::renderer::enable_texture_pack(const recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_id});
// Check for the texture pack enabled config option.
const recomp::mods::ConfigSchema& config_schema = context.get_mod_config_schema(mod.manifest.mod_id);
auto find_it = config_schema.options_by_id.find(zelda64::renderer::special_option_texture_pack_enabled);
if (find_it != config_schema.options_by_id.end()) {
const recomp::mods::ConfigOption& config_option = config_schema.options[find_it->second];
if (is_texture_pack_enable_config_option(config_option, false)) {
recomp::mods::ConfigValueVariant value_variant = context.get_mod_config_value(mod.manifest.mod_id, config_option.id);
uint32_t value;
if (uint32_t* value_ptr = std::get_if<uint32_t>(&value_variant)) {
value = *value_ptr;
}
else {
value = 0;
}
if (value) {
zelda64::renderer::secondary_enable_texture_pack(mod.manifest.mod_id);
}
else {
zelda64::renderer::secondary_disable_texture_pack(mod.manifest.mod_id);
}
}
}
}
void zelda64::renderer::disable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_root_path});
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_id});
}
void zelda64::renderer::secondary_enable_texture_pack(const std::string& mod_id) {
texture_pack_action_queue.enqueue(TexturePackSecondaryEnableAction{mod_id});
}
void zelda64::renderer::secondary_disable_texture_pack(const std::string& mod_id) {
texture_pack_action_queue.enqueue(TexturePackSecondaryDisableAction{mod_id});
}
// HD texture enable option. Must be an enum with two options.
// The first option is treated as disabled and the second option is treated as enabled.
bool zelda64::renderer::is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors) {
if (option.id == zelda64::renderer::special_option_texture_pack_enabled) {
if (option.type != recomp::mods::ConfigOptionType::Enum) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + zelda64::renderer::special_option_texture_pack_enabled + "\"), but the config option is not an enum.").c_str());
}
return false;
}
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
if (option_enum.options.size() != 2) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + zelda64::renderer::special_option_texture_pack_enabled + "\"), but the config option doesn't have exactly 2 values.").c_str());
}
return false;
}
return true;
}
return false;
}
+33
View File
@@ -20,6 +20,29 @@ namespace zelda64 {
callback(success, path);
}
void perform_file_dialog_operation_multiple(const std::function<void(bool, const std::list<std::filesystem::path>&)>& callback) {
const nfdpathset_t* native_paths = nullptr;
nfdresult_t result = NFD_OpenDialogMultipleN(&native_paths, nullptr, 0, nullptr);
bool success = (result == NFD_OKAY);
std::list<std::filesystem::path> paths;
nfdpathsetsize_t count = 0;
if (success) {
NFD_PathSet_GetCount(native_paths, &count);
for (nfdpathsetsize_t i = 0; i < count; i++) {
nfdnchar_t* cur_path = nullptr;
nfdresult_t cur_result = NFD_PathSet_GetPathN(native_paths, i, &cur_path);
if (cur_result == NFD_OKAY) {
paths.emplace_back(std::filesystem::path{cur_path});
}
}
NFD_PathSet_Free(native_paths);
}
callback(success, paths);
}
// MARK: - Public API
std::filesystem::path get_asset_path(const char* asset) {
@@ -41,6 +64,16 @@ namespace zelda64 {
#endif
}
void open_file_dialog_multiple(std::function<void(bool success, const std::list<std::filesystem::path>& paths)> callback) {
#ifdef __APPLE__
dispatch_on_ui_thread([callback]() {
perform_file_dialog_operation_multiple(callback);
});
#else
perform_file_dialog_operation_multiple(callback);
#endif
}
void show_error_message_box(const char *title, const char *message) {
#ifdef __APPLE__
std::string title_copy(title);
+668
View File
@@ -0,0 +1,668 @@
#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"
#include "ui_context.h"
#include "../elements/ui_element.h"
// Hash implementations for ContextId and ResourceId.
template <>
struct std::hash<recompui::ContextId> {
std::size_t operator()(const recompui::ContextId& id) const {
return std::hash<uint32_t>()(id.slot_id);
}
};
template <>
struct std::hash<recompui::ResourceId> {
std::size_t operator()(const recompui::ResourceId& id) const {
return std::hash<uint32_t>()(id.slot_id);
}
};
using resource_slotmap = dod::slot_map32<std::unique_ptr<recompui::Style>>;
namespace recompui {
struct Context {
std::mutex context_lock;
resource_slotmap resources;
Rml::ElementDocument* document;
Element root_element;
Element* autofocus_element = nullptr;
std::vector<Element*> loose_elements;
std::unordered_set<ResourceId> to_update;
bool captures_input = true;
bool captures_mouse = true;
Context(Rml::ElementDocument* document) : document(document), root_element(document) {}
};
} // namespace recompui
using context_slotmap = dod::slot_map32<recompui::Context>;
static struct {
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;
thread_local recompui::ContextId opened_context_id = recompui::ContextId::null();
enum class ContextErrorType {
OpenWithoutClose,
OpenInvalidContext,
CloseWithoutOpen,
CloseWrongContext,
DestroyInvalidContext,
GetContextWithoutOpen,
AddResourceWithoutOpen,
AddResourceToWrongContext,
UpdateElementWithoutContext,
UpdateElementInWrongContext,
GetResourceWithoutOpen,
GetResourceFailed,
DestroyResourceWithoutOpen,
DestroyResourceInWrongContext,
DestroyResourceNotFound,
GetDocumentInvalidContext,
GetAutofocusInvalidContext,
SetAutofocusInvalidContext,
InternalError,
};
enum class SlotTag : uint8_t {
Style = 0,
Element = 1,
};
void context_error(recompui::ContextId id, ContextErrorType type) {
(void)id;
const char* error_message = "";
switch (type) {
case ContextErrorType::OpenWithoutClose:
error_message = "Attempted to open a UI context without closing another UI context";
break;
case ContextErrorType::OpenInvalidContext:
error_message = "Attempted to open an invalid UI context";
break;
case ContextErrorType::CloseWithoutOpen:
error_message = "Attempted to close a UI context without one being open";
break;
case ContextErrorType::CloseWrongContext:
error_message = "Attempted to close a different UI context than the one that's open";
break;
case ContextErrorType::DestroyInvalidContext:
error_message = "Attempted to destroy an invalid UI element";
break;
case ContextErrorType::GetContextWithoutOpen:
error_message = "Attempted to get the current UI context with no UI context open";
break;
case ContextErrorType::AddResourceWithoutOpen:
error_message = "Attempted to create a UI resource with no open UI context";
break;
case ContextErrorType::AddResourceToWrongContext:
error_message = "Attempted to create a UI resource in a different UI context than the one that's open";
break;
case ContextErrorType::UpdateElementWithoutContext:
error_message = "Attempted to update a UI element with no open UI context";
break;
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::GetResourceWithoutOpen:
error_message = "Attempted to get a UI resource with no open UI context";
break;
case ContextErrorType::GetResourceFailed:
error_message = "Failed to get a UI resource from the current open UI context";
break;
case ContextErrorType::DestroyResourceWithoutOpen:
error_message = "Attempted to destroy a UI resource with no open UI context";
break;
case ContextErrorType::DestroyResourceInWrongContext:
error_message = "Attempted to destroy a UI resource in a different UI context than the one that's open";
break;
case ContextErrorType::DestroyResourceNotFound:
error_message = "Attempted to destroy a UI resource that doesn't exist in the current context";
break;
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;
default:
error_message = "Unknown UI context error";
break;
}
// This assumes the error is coming from a mod, as it's unlikely that an end user will see a UI context error
// in the base recomp.
recompui::message_box((std::string{"Fatal error in mod - "} + error_message + ".").c_str());
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
recompui::ContextId create_context_impl(Rml::ElementDocument* document) {
static Rml::ElementDocument dummy_document{""};
bool add_to_dict = true;
if (document == nullptr) {
document = &dummy_document;
add_to_dict = false;
}
recompui::ContextId ret;
{
std::lock_guard lock{ context_state.all_contexts_lock };
ret = { context_state.all_contexts.emplace(document).raw };
if (add_to_dict) {
context_state.documents_to_contexts.emplace(document, ret);
}
}
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);
auto workingdir = std::filesystem::current_path();
new_context.open();
Rml::ElementDocument* doc = recompui::load_document(path.string());
opened_context->document = doc;
opened_context->root_element.base = doc;
new_context.close();
{
std::lock_guard lock{ context_state.all_contexts_lock };
context_state.documents_to_contexts.emplace(doc, new_context);
}
return new_context;
}
recompui::ContextId recompui::create_context(Rml::ElementDocument* document) {
assert(document != nullptr);
return create_context_impl(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;
ret.open();
root->set_width(100.0f, Unit::Percent);
root->set_height(100.0f, Unit::Percent);
root->set_display(Display::Flex);
ret.close();
doc->Hide();
return ret;
}
void recompui::destroy_context(ContextId id) {
bool existed = false;
// TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping
// from the slotmap?
// Check if the provided id exists.
{
std::lock_guard lock{ context_state.all_contexts_lock };
// Check if the target context is currently open.
existed = context_state.all_contexts.has_key(context_slotmap::key{ id.slot_id });
}
// Raise an error if the context didn't exist.
if (!existed) {
context_error(id, ContextErrorType::DestroyInvalidContext);
}
id.open();
id.clear_children();
id.close();
// Delete the provided id.
{
std::lock_guard lock{ context_state.all_contexts_lock };
context_state.all_contexts.erase(context_slotmap::key{ id.slot_id });
}
}
void recompui::destroy_all_contexts() {
recompui::hide_all_contexts();
std::lock_guard lock{ context_state.all_contexts_lock };
// TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping
// from the slotmap
std::vector<context_slotmap::key> keys{};
for (const auto& [key, item] : context_state.all_contexts.items()) {
keys.push_back(key);
}
for (auto key : keys) {
Context* ctx = context_state.all_contexts.get(key);
std::lock_guard context_lock{ ctx->context_lock };
opened_context = ctx;
opened_context_id = ContextId{ key };
opened_context_id.clear_children();
opened_context = nullptr;
opened_context_id = ContextId::null();
}
context_state.all_contexts.reset();
context_state.documents_to_contexts.clear();
}
void recompui::ContextId::open() {
// Ensure no other context is opened by this thread already.
if (opened_context_id != ContextId::null()) {
context_error(*this, ContextErrorType::OpenWithoutClose);
}
// Get the context with this id.
Context* ctx;
{
std::lock_guard lock{ context_state.all_contexts_lock };
ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
// If the context was found, add it to the opened contexts.
if (ctx != nullptr) {
context_state.opened_contexts.emplace(*this);
}
}
// Check if the context exists.
if (ctx == nullptr) {
context_error(*this, ContextErrorType::OpenInvalidContext);
}
// Take ownership of the target context.
ctx->context_lock.lock();
opened_context = ctx;
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()) {
context_error(*this, ContextErrorType::CloseWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::CloseWrongContext);
}
// Release ownership of the target context.
opened_context->context_lock.unlock();
opened_context = nullptr;
opened_context_id = ContextId::null();
// Remove this context from the opened contexts.
{
std::lock_guard lock{ context_state.all_contexts_lock };
context_state.opened_contexts.erase(*this);
}
}
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()) {
context_error(*this, ContextErrorType::InternalError);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::InternalError);
}
// Move the current update set into a local variable. This clears the update set
// and allows it to be used to queue updates from any element callbacks.
std::unordered_set<ResourceId> to_update = std::move(opened_context->to_update);
Event update_event = Event::update_event();
for (auto cur_resource_id : to_update) {
resource_slotmap::key cur_key{ cur_resource_id.slot_id };
// Ignore any resources that aren't elements.
if (cur_key.get_tag() != static_cast<uint8_t>(SlotTag::Element)) {
// Assert to catch errors of queueing other resource types for update.
// This isn't an actual error, so there's no issue with continuing in release builds.
assert(false);
continue;
}
// Get the resource being updaten from the context.
std::unique_ptr<Style>* cur_resource = opened_context->resources.get(cur_key);
// Make sure the resource exists before dispatching the event. It may have been deleted
// after being queued for a update, so just continue to the next element if it doesn't exist.
if (cur_resource == nullptr) {
continue;
}
static_cast<Element*>(cur_resource->get())->handle_event(update_event);
}
}
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) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::AddResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::AddResourceToWrongContext);
}
bool is_element = resource->is_element();
Style* resource_ptr = resource.get();
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 });
}
else {
key.set_tag(static_cast<uint8_t>(SlotTag::Style));
}
resource_ptr->resource_id = { key.raw };
return resource_ptr;
}
void recompui::ContextId::add_loose_element(Element* element) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::AddResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::AddResourceToWrongContext);
}
opened_context->loose_elements.emplace_back(element);
}
void recompui::ContextId::queue_element_update(ResourceId element) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::UpdateElementWithoutContext);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::UpdateElementInWrongContext);
}
// Check that the element that was specified is in the open context.
auto* elementPtr = opened_context->resources.get(resource_slotmap::key{ element.slot_id });
if (elementPtr == nullptr) {
context_error(*this, ContextErrorType::UpdateElementInWrongContext);
}
opened_context->to_update.emplace(element);
}
recompui::Style* recompui::ContextId::create_style() {
return add_resource_impl(std::make_unique<Style>());
}
void recompui::ContextId::destroy_resource(Style* resource) {
destroy_resource(resource->resource_id);
}
void recompui::ContextId::destroy_resource(ResourceId resource) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::DestroyResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::DestroyResourceInWrongContext);
}
// Try to remove the resource from the current context.
auto pop_result = opened_context->resources.pop(resource_slotmap::key{ resource.slot_id });
if (!pop_result.has_value()) {
context_error(*this, ContextErrorType::DestroyResourceNotFound);
}
}
void recompui::ContextId::clear_children() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::DestroyResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::DestroyResourceInWrongContext);
}
// Remove the root element's children.
opened_context->root_element.clear_children();
// Remove any loose resources.
for (Element* e : opened_context->loose_elements) {
destroy_resource(e->resource_id);
}
opened_context->loose_elements.clear();
}
Rml::ElementDocument* recompui::ContextId::get_document() {
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::GetDocumentInvalidContext);
}
return ctx->document;
}
recompui::Element* recompui::ContextId::get_root_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::GetDocumentInvalidContext);
}
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()) {
context_error(ContextId::null(), ContextErrorType::GetContextWithoutOpen);
}
return opened_context_id;
}
recompui::Style* get_resource_from_current_context(resource_slotmap::key key) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == recompui::ContextId::null()) {
context_error(recompui::ContextId::null(), ContextErrorType::GetResourceWithoutOpen);
}
auto* value = opened_context->resources.get(key);
if (value == nullptr) {
context_error(opened_context_id, ContextErrorType::GetResourceFailed);
}
return value->get();
}
const recompui::Style* recompui::ResourceId::operator*() const {
resource_slotmap::key key{ slot_id };
return get_resource_from_current_context(key);
}
recompui::Style* recompui::ResourceId::operator*() {
resource_slotmap::key key{ slot_id };
return get_resource_from_current_context(key);
}
const recompui::Element* recompui::ResourceId::as_element() const {
resource_slotmap::key key{ slot_id };
uint8_t tag = key.get_tag();
assert(tag == static_cast<uint8_t>(SlotTag::Element));
return static_cast<Element*>(get_resource_from_current_context(key));
}
recompui::Element* recompui::ResourceId::as_element() {
resource_slotmap::key key{ slot_id };
uint8_t tag = key.get_tag();
assert(tag == static_cast<uint8_t>(SlotTag::Element));
return static_cast<Element*>(get_resource_from_current_context(key));
}
recompui::ContextId recompui::get_context_from_document(Rml::ElementDocument* document) {
std::lock_guard lock{ context_state.all_contexts_lock };
auto find_it = context_state.documents_to_contexts.find(document);
if (find_it == context_state.documents_to_contexts.end()) {
return ContextId::null();
}
return find_it->second;
}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <cstdint>
#include <memory>
#include <utility>
#include <filesystem>
#include <functional>
#include "RmlUi/Core.h"
#include "ui_resource.h"
namespace recompui {
class Style;
class Element;
class ContextId {
Style* add_resource_impl(std::unique_ptr<Style>&& resource);
public:
uint32_t slot_id;
auto operator<=>(const ContextId& rhs) const = default;
template <typename T, typename... Args>
T* create_element(Args... args) {
return static_cast<T*>(add_resource_impl(std::make_unique<T>(std::forward<Args>(args)...)));
}
template <typename T>
T* create_element(T&& element) {
return static_cast<T*>(add_resource_impl(std::make_unique<T>(std::move(element))));
}
void add_loose_element(Element* element);
void queue_element_update(ResourceId element);
Style* create_style();
void destroy_resource(Style* resource);
void destroy_resource(ResourceId resource);
void clear_children();
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) }; }
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);
ContextId create_context(Rml::ElementDocument* document);
ContextId create_context();
void destroy_context(ContextId id);
ContextId get_current_context();
ContextId get_context_from_document(Rml::ElementDocument* document);
void destroy_all_contexts();
void register_ui_exports();
} // namespace recompui
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
namespace recompui {
class Style;
class Element;
struct ResourceId {
uint32_t slot_id;
bool operator==(const ResourceId& rhs) const = default;
const Style* operator*() const;
Style* operator*();
const Style* operator->() const { return *(*this); }
Style* operator->() { return *(*this); }
const Element* as_element() const;
Element* as_element();
static constexpr ResourceId null() { return ResourceId{ uint32_t(-1) }; }
};
} // namespace recompui
+114
View File
@@ -0,0 +1,114 @@
#include "ui_button.h"
#include <cassert>
namespace recompui {
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);
set_border_width(1.1f);
set_border_radius(12.0f);
set_font_size(28.0f);
set_letter_spacing(3.08f);
set_line_height(28.0f);
set_font_style(FontStyle::Normal);
set_font_weight(700);
set_cursor(Cursor::Pointer);
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 });
const uint8_t border_opacity = 204;
const uint8_t background_opacity = 13;
const uint8_t border_hover_opacity = 255;
const uint8_t background_hover_opacity = 76;
switch (style) {
case ButtonStyle::Primary: {
set_border_color({ 185, 125, 242, border_opacity });
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 });
hover_disabled_style.set_background_color({ 185, 125, 242, background_hover_opacity / 4 });
break;
}
case ButtonStyle::Secondary: {
set_border_color({ 23, 214, 232, border_opacity });
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 });
hover_disabled_style.set_background_color({ 23, 214, 232, background_hover_opacity / 4 });
break;
}
default:
assert(false && "Unknown button style.");
break;
}
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 });
// transition: color 0.05s linear-in-out, background-color 0.05s linear-in-out;
}
void Button::process_event(const Event &e) {
switch (e.type) {
case EventType::Click:
if (is_enabled()) {
for (const auto &function : pressed_callbacks) {
function();
}
}
break;
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
{
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;
default:
assert(false && "Unknown event type.");
break;
}
}
void Button::add_pressed_callback(std::function<void()> callback) {
pressed_callbacks.emplace_back(callback);
}
};
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class ButtonStyle {
Primary,
Secondary
};
class Button : public Element {
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
+62
View File
@@ -0,0 +1,62 @@
#include "ui_clickable.h"
namespace recompui {
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
set_cursor(Cursor::Pointer);
if (draggable) {
set_drag(Drag::Drag);
}
}
void Clickable::process_event(const Event &e) {
switch (e.type) {
case EventType::Click: {
if (is_enabled()) {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : pressed_callbacks) {
function(click.x, click.y);
}
break;
}
}
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
{
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_pressed_callback(std::function<void(float, float)> callback) {
pressed_callbacks.emplace_back(callback);
}
void Clickable::add_dragged_callback(std::function<void(float, float, DragPhase)> callback) {
dragged_callbacks.emplace_back(callback);
}
};
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Clickable : public Element {
protected:
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_pressed_callback(std::function<void(float, float)> callback);
void add_dragged_callback(std::function<void(float, float, DragPhase)> callback);
};
} // namespace recompui
+13
View File
@@ -0,0 +1,13 @@
#include "ui_container.h"
#include <cassert>
namespace recompui {
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled) : Element(parent, events_enabled) {
set_display(Display::Flex);
set_flex_direction(direction);
set_justify_content(justify_content);
}
};
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "ui_element.h"
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, uint32_t events_enabled = 0);
};
} // namespace recompui
+478
View File
@@ -0,0 +1,478 @@
#include "RmlUi/Core/StringUtilities.h"
#include "overloaded.h"
#include "recomp_ui.h"
#include "ui_element.h"
#include "../core/ui_context.h"
#include <cassert>
namespace recompui {
Element::Element(Rml::Element *base) {
assert(base != nullptr);
this->base = base;
this->base_owning = {};
this->shim = true;
}
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);
if (parent != nullptr) {
base = parent->base->AppendChild(std::move(base_owning));
parent->add_child(this);
}
else {
base = base_owning.get();
}
set_display(Display::Block);
set_property(Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox);
register_event_listeners(events_enabled);
}
Element::~Element() {
if (!shim) {
clear_children();
if (!base_owning) {
base->GetParentNode()->RemoveChild(base);
}
}
}
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);
if (shim) {
ContextId context = get_current_context();
context.add_loose_element(child);
}
}
void Element::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
assert(base != nullptr);
base->SetProperty(property_id, property);
Style::set_property(property_id, property);
}
void Element::register_event_listeners(uint32_t events_enabled) {
assert(base != nullptr);
this->events_enabled = events_enabled;
if (events_enabled & Events(EventType::Click)) {
base->AddEventListener(Rml::EventId::Click, this);
}
if (events_enabled & Events(EventType::Focus)) {
base->AddEventListener(Rml::EventId::Focus, this);
base->AddEventListener(Rml::EventId::Blur, this);
}
if (events_enabled & Events(EventType::Hover)) {
base->AddEventListener(Rml::EventId::Mouseover, this);
base->AddEventListener(Rml::EventId::Mouseout, this);
}
if (events_enabled & Events(EventType::Drag)) {
base->AddEventListener(Rml::EventId::Drag, this);
base->AddEventListener(Rml::EventId::Dragstart, this);
base->AddEventListener(Rml::EventId::Dragend, this);
}
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) {
// 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);
}
}
}
void Element::apply_styles() {
apply_style(this);
for (size_t i = 0; i < styles_counter.size(); i++) {
if (styles_counter[i] == 0) {
apply_style(styles[i]);
}
}
}
void Element::propagate_disabled(bool disabled) {
disabled_from_parent = disabled;
bool attribute_state = disabled_from_parent || !enabled;
if (disabled_attribute != attribute_state) {
disabled_attribute = attribute_state;
base->SetAttribute("disabled", attribute_state);
if (events_enabled & Events(EventType::Enable)) {
handle_event(Event::enable_event(!attribute_state));
}
for (auto &child : children) {
child->propagate_disabled(attribute_state);
}
}
}
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);
}
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()) {
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::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:
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
break;
default:
break;
}
// Events that are only processed during the Target phase.
if (event.GetPhase() == Rml::EventPhase::Target) {
switch (event.GetId()) {
case Rml::EventId::Mouseover:
handle_event(Event::hover_event(true));
break;
case Rml::EventId::Mouseout:
handle_event(Event::hover_event(false));
break;
case Rml::EventId::Focus:
handle_event(Event::focus_event(true));
break;
case Rml::EventId::Blur:
handle_event(Event::focus_event(false));
break;
case Rml::EventId::Dragstart:
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
break;
case Rml::EventId::Dragend:
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) {
handle_event(Event::text_event(value_variant->Get<std::string>()));
}
}
break;
}
default:
break;
}
}
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) {
base->SetAttribute(attribute_key, attribute_value);
}
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;
}
ContextId context = get_current_context();
// Remove the children from the context.
for (Element* child : children) {
context.destroy_resource(child);
}
// Clear the child list.
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 });
}
void Element::add_style(Style *style, const std::initializer_list<std::string_view> &style_names) {
for (const std::string_view &style_name : style_names) {
style_name_index_map.emplace(style_name, styles.size());
}
styles.emplace_back(style);
uint32_t initial_style_counter = style_names.size();
for (const std::string_view &style_name : style_names) {
if (style_active_set.find(style_name) != style_active_set.end()) {
initial_style_counter--;
}
}
styles_counter.push_back(initial_style_counter);
}
void Element::set_enabled(bool enabled) {
this->enabled = enabled;
propagate_disabled(disabled_from_parent);
}
bool Element::is_enabled() const {
return enabled && !disabled_from_parent;
}
// Adapted from RmlUi's `EncodeRml`.
std::string escape_rml(std::string_view string)
{
std::string result;
result.reserve(string.size());
for (char c : string)
{
switch (c)
{
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '&': result += "&amp;"; break;
case '"': result += "&quot;"; break;
case '\n': result += "<br/>"; break;
default: result += c; break;
}
}
return result;
}
void Element::set_text(std::string_view text) {
if (can_set_text) {
// Escape the string into Rml to prevent element injection.
base->SetInnerRML(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) {
base->SetAttribute("src", std::string(src));
}
void Element::set_style_enabled(std::string_view style_name, bool enable) {
if (enable && style_active_set.find(style_name) == style_active_set.end()) {
// Style was disabled and will be enabled.
style_active_set.emplace(style_name);
}
else if (!enable && style_active_set.find(style_name) != style_active_set.end()) {
// Style was enabled and will be disabled.
style_active_set.erase(style_name);
}
else {
// Do nothing.
return;
}
auto range = style_name_index_map.equal_range(style_name);
for (auto it = range.first; it != range.second; it++) {
if (enable) {
styles_counter[it->second]--;
}
else {
styles_counter[it->second]++;
}
}
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();
}
float Element::get_absolute_top() {
return base->GetAbsoluteTop();
}
float Element::get_client_left() {
return base->GetClientLeft();
}
float Element::get_client_top() {
return base->GetClientTop();
}
float Element::get_client_width() {
return base->GetClientWidth();
}
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();
// TODO disallow null contexts once the entire UI system has been migrated.
if (cur_context == ContextId::null()) {
return;
}
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});
}
}
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#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 handle_event method directly.
private:
Rml::Element *base = nullptr;
Rml::ElementPtr base_owning = {};
uint32_t events_enabled = 0;
std::vector<Style *> styles;
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 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;
// Rml::EventListener overrides.
void ProcessEvent(Rml::Event &event) override final;
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", 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();
float get_client_left();
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
+11
View File
@@ -0,0 +1,11 @@
#include "ui_image.h"
#include <cassert>
namespace recompui {
Image::Image(Element *parent, std::string_view src) : Element(parent, 0, "img") {
set_src(src);
}
};
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Image : public Element {
protected:
std::string_view get_type_name() override { return "ImageView"; }
public:
Image(Element *parent, std::string_view src);
};
} // namespace recompui
+43
View File
@@ -0,0 +1,43 @@
#include "ui_label.h"
#include <cassert>
namespace recompui {
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 });
set_font_size(18.0f);
set_letter_spacing(2.52f);
set_line_height(18.0f);
set_font_weight(400);
break;
case LabelStyle::Small:
set_font_size(20.0f);
set_letter_spacing(0.0f);
set_line_height(20.0f);
set_font_weight(400);
break;
case LabelStyle::Normal:
set_font_size(28.0f);
set_letter_spacing(3.08f);
set_line_height(28.0f);
set_font_weight(700);
break;
case LabelStyle::Large:
set_font_size(36.0f);
set_letter_spacing(2.52f);
set_line_height(36.0f);
set_font_weight(700);
break;
}
set_font_style(FontStyle::Normal);
}
Label::Label(Element *parent, const std::string &text, LabelStyle label_style) : Label(parent, label_style) {
set_text(text);
}
};
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class LabelStyle {
Annotation,
Small,
Normal,
Large
};
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);
};
} // namespace recompui
+240
View File
@@ -0,0 +1,240 @@
#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, EventType::Update), "label", true) {
this->index = index;
enable_focus();
set_text(name);
set_cursor(Cursor::Pointer);
set_font_size(20.0f);
set_letter_spacing(2.8f);
set_line_height(20.0f);
set_font_weight(400);
set_font_style(FontStyle::Normal);
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_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_selected_state(bool enable) {
set_style_enabled(checked_state, enable);
}
void RadioOption::process_event(const Event &e) {
switch (e.type) {
case EventType::Click:
pressed_callback(index);
break;
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
break;
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();
}
}
break;
case EventType::Update:
if (is_style_enabled(focus_state)) {
pulsing_style.set_color(recompui::get_pulse_color(750));
apply_styles();
queue_update();
}
if (focus_queued) {
focus_queued = false;
focus();
}
break;
default:
break;
}
}
// Radio
void Radio::set_index_internal(uint32_t index, bool setup, bool trigger_callbacks) {
if (this->index != index || setup) {
options[this->index]->set_selected_state(false);
this->index = index;
options[index]->set_selected_state(true);
if (trigger_callbacks) {
for (const auto &function : index_changed_callbacks) {
function(index);
}
}
}
}
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, Events(EventType::Focus)) {
set_gap(24.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();
options[index]->queue_focus();
}
}
break;
}
}
Radio::~Radio() {
}
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); });
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) {
set_index_internal(index, false, false);
}
uint32_t Radio::get_index() const {
return index;
}
void Radio::add_index_changed_callback(std::function<void(uint32_t)> callback) {
index_changed_callbacks.emplace_back(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;
}
}
}
};
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "ui_container.h"
#include "ui_label.h"
namespace recompui {
class RadioOption : public Element {
private:
Style hover_style;
Style checked_style;
Style pulsing_style;
std::function<void(uint32_t)> pressed_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_selected_state(bool enable);
void queue_focus() { focus_queued = true; queue_update(); }
};
class Radio : public Container {
private:
std::vector<RadioOption *> options;
uint32_t index = 0;
std::vector<std::function<void(uint32_t)>> index_changed_callbacks;
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"; }
public:
Radio(Element *parent);
virtual ~Radio();
void add_option(std::string_view name);
void set_index(uint32_t index);
uint32_t get_index() const;
void add_index_changed_callback(std::function<void(uint32_t)> 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
+27
View File
@@ -0,0 +1,27 @@
#include "ui_scroll_container.h"
#include <cassert>
namespace recompui {
ScrollContainer::ScrollContainer(Element *parent, ScrollDirection direction) : Element(parent) {
set_flex(1.0f, 1.0f, 100.0f);
set_width(100.0f, Unit::Percent);
set_height(100.0f, Unit::Percent);
switch (direction) {
case ScrollDirection::Horizontal:
set_max_width(100.0f, Unit::Percent);
set_overflow_x(Overflow::Auto);
break;
case ScrollDirection::Vertical:
set_max_height(100.0f, Unit::Percent);
set_overflow_y(Overflow::Auto);
break;
default:
assert(false && "Unknown scroll direction.");
break;
}
}
};
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class ScrollDirection {
Horizontal,
Vertical
};
class ScrollContainer : public Element {
protected:
std::string_view get_type_name() override { return "ScrollContainer"; }
public:
ScrollContainer(Element *parent, ScrollDirection direction);
};
} // namespace recompui
+236
View File
@@ -0,0 +1,236 @@
#include "overloaded.h"
#include "ui_slider.h"
#include "../ui_utils.h"
#include <cmath>
#include <charconv>
namespace recompui {
void Slider::set_value_internal(double v, bool setup, bool trigger_callbacks) {
if (step_value != 0.0) {
v = std::lround(v / step_value) * step_value;
}
if (value != v || setup) {
value = v;
update_circle_position();
update_label_text();
if (trigger_callbacks) {
for (auto callback : value_changed_callbacks) {
callback(v);
}
}
}
}
void Slider::bar_clicked(float x, float) {
update_value_from_mouse(x);
}
void Slider::bar_dragged(float x, float, DragPhase) {
update_value_from_mouse(x);
}
void Slider::circle_dragged(float x, float, DragPhase) {
update_value_from_mouse(x);
}
void Slider::update_value_from_mouse(float x) {
double left = slider_element->get_absolute_left();
double width = slider_element->get_client_width();
double ratio = std::clamp((x - left) / width, 0.0, 1.0);
set_value_internal(min_value + ratio * (max_value - min_value), false, true);
}
void Slider::update_circle_position() {
double ratio = std::clamp((value - min_value) / (max_value - min_value), 0.0, 1.0);
circle_element->set_left(ratio * 100.0, Unit::Percent);
}
void Slider::update_label_text() {
char text_buffer[32];
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();
}
}
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, Events(EventType::Focus, EventType::Update, EventType::Navigate, EventType::Enable)) {
this->type = type;
set_cursor(Cursor::Pointer);
set_display(Display::Flex);
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();
value_label = context.create_element<Label>(this, "0", LabelStyle::Small);
value_label->set_margin_right(20.0f);
value_label->set_min_width(60.0f);
value_label->set_max_width(60.0f);
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_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);
bar_element->set_width(100.0f, Unit::Percent);
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_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);
circle_element->set_position(Position::Relative);
circle_element->set_width(16.0f);
circle_element->set_height(16.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_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);
}
set_value_internal(value, true, false);
}
Slider::~Slider() {
}
void Slider::set_value(double v) {
set_value_internal(v, false, false);
}
double Slider::get_value() const {
return value;
}
void Slider::set_min_value(double v) {
min_value = v;
}
double Slider::get_min_value() const {
return min_value;
}
void Slider::set_max_value(double v) {
max_value = v;
}
double Slider::get_max_value() const {
return max_value;
}
void Slider::set_step_value(double v) {
step_value = v;
}
double Slider::get_step_value() const {
return step_value;
}
void Slider::add_value_changed_callback(std::function<void(double)> callback) {
value_changed_callbacks.emplace_back(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
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include "ui_clickable.h"
#include "ui_label.h"
namespace recompui {
enum SliderType {
Double,
Percent,
Integer
};
class Slider : public Element {
private:
SliderType type = SliderType::Percent;
Label *value_label = 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;
std::vector<std::function<void(double)>> value_changed_callbacks;
void set_value_internal(double v, bool setup, bool trigger_callbacks);
void bar_clicked(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);
virtual ~Slider();
void set_value(double v);
double get_value() const;
void set_min_value(double v);
double get_min_value() const;
void set_max_value(double v);
double get_max_value() const;
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);
};
} // namespace recompui
+15
View File
@@ -0,0 +1,15 @@
#include "ui_span.h"
#include <cassert>
namespace recompui {
Span::Span(Element *parent) : Element(parent, 0, "span", true) {
set_font_style(FontStyle::Normal);
}
Span::Span(Element *parent, const std::string &text) : Span(parent) {
set_text(text);
}
};
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "ui_element.h"
#include "ui_label.h"
namespace recompui {
class Span : public Element {
protected:
std::string_view get_type_name() override { return "Span"; }
public:
Span(Element *parent);
Span(Element *parent, const std::string &text);
};
} // namespace recompui
+612
View File
@@ -0,0 +1,612 @@
#include "ui_style.h"
#include "ui_element.h"
#include <cassert>
namespace recompui {
static Rml::Unit to_rml(Unit unit) {
switch (unit) {
case Unit::Px:
return Rml::Unit::PX;
case Unit::Dp:
return Rml::Unit::DP;
case Unit::Percent:
return Rml::Unit::PERCENT;
default:
return Rml::Unit::UNKNOWN;
}
}
static Rml::Style::AlignItems to_rml(AlignItems align_items) {
switch (align_items) {
case AlignItems::FlexStart:
return Rml::Style::AlignItems::FlexStart;
case AlignItems::FlexEnd:
return Rml::Style::AlignItems::FlexEnd;
case AlignItems::Center:
return Rml::Style::AlignItems::Center;
case AlignItems::Baseline:
return Rml::Style::AlignItems::Baseline;
case AlignItems::Stretch:
return Rml::Style::AlignItems::Stretch;
default:
assert(false && "Unknown align items.");
return Rml::Style::AlignItems::FlexStart;
}
}
static Rml::Style::Overflow to_rml(Overflow overflow) {
switch (overflow) {
case Overflow::Visible:
return Rml::Style::Overflow::Visible;
case Overflow::Hidden:
return Rml::Style::Overflow::Hidden;
case Overflow::Auto:
return Rml::Style::Overflow::Auto;
case Overflow::Scroll:
return Rml::Style::Overflow::Scroll;
default:
assert(false && "Unknown overflow.");
return Rml::Style::Overflow::Visible;
}
}
static Rml::Style::TextAlign to_rml(TextAlign text_align) {
switch (text_align) {
case TextAlign::Left:
return Rml::Style::TextAlign::Left;
case TextAlign::Right:
return Rml::Style::TextAlign::Right;
case TextAlign::Center:
return Rml::Style::TextAlign::Center;
case TextAlign::Justify:
return Rml::Style::TextAlign::Justify;
default:
assert(false && "Unknown text align.");
return Rml::Style::TextAlign::Left;
}
}
static Rml::Style::TextTransform to_rml(TextTransform text_transform) {
switch (text_transform) {
case TextTransform::None:
return Rml::Style::TextTransform::None;
case TextTransform::Capitalize:
return Rml::Style::TextTransform::Capitalize;
case TextTransform::Uppercase:
return Rml::Style::TextTransform::Uppercase;
case TextTransform::Lowercase:
return Rml::Style::TextTransform::Lowercase;
default:
assert(false && "Unknown text transform.");
return Rml::Style::TextTransform::None;
}
}
static Rml::Style::Drag to_rml(Drag drag) {
switch (drag) {
case Drag::None:
return Rml::Style::Drag::None;
case Drag::Drag:
return Rml::Style::Drag::Drag;
case Drag::DragDrop:
return Rml::Style::Drag::DragDrop;
case Drag::Block:
return Rml::Style::Drag::Block;
case Drag::Clone:
return Rml::Style::Drag::Clone;
default:
assert(false && "Unknown drag.");
return Rml::Style::Drag::None;
}
}
static Rml::Style::TabIndex to_rml(TabIndex tab_index) {
switch (tab_index) {
case TabIndex::None:
return Rml::Style::TabIndex::None;
case TabIndex::Auto:
return Rml::Style::TabIndex::Auto;
default:
assert(false && "Unknown tab index.");
return Rml::Style::TabIndex::None;
}
}
static Rml::Style::Display to_rml(Display display) {
switch (display) {
case Display::None:
return Rml::Style::Display::None;
case Display::Block:
return Rml::Style::Display::Block;
case Display::Inline:
return Rml::Style::Display::Inline;
case Display::InlineBlock:
return Rml::Style::Display::InlineBlock;
case Display::FlowRoot:
return Rml::Style::Display::FlowRoot;
case Display::Flex:
return Rml::Style::Display::Flex;
case Display::InlineFlex:
return Rml::Style::Display::InlineFlex;
case Display::Table:
return Rml::Style::Display::Table;
case Display::InlineTable:
return Rml::Style::Display::InlineTable;
case Display::TableRow:
return Rml::Style::Display::TableRow;
case Display::TableRowGroup:
return Rml::Style::Display::TableRowGroup;
case Display::TableColumn:
return Rml::Style::Display::TableColumn;
case Display::TableColumnGroup:
return Rml::Style::Display::TableColumnGroup;
case Display::TableCell:
return Rml::Style::Display::TableCell;
default:
assert(false && "Unknown display.");
return Rml::Style::Display::Block;
}
}
static Rml::Style::JustifyContent to_rml(JustifyContent justify_content) {
switch (justify_content) {
case JustifyContent::FlexStart:
return Rml::Style::JustifyContent::FlexStart;
case JustifyContent::FlexEnd:
return Rml::Style::JustifyContent::FlexEnd;
case JustifyContent::Center:
return Rml::Style::JustifyContent::Center;
case JustifyContent::SpaceBetween:
return Rml::Style::JustifyContent::SpaceBetween;
case JustifyContent::SpaceAround:
return Rml::Style::JustifyContent::SpaceAround;
case JustifyContent::SpaceEvenly:
return Rml::Style::JustifyContent::SpaceEvenly;
default:
assert(false && "Unknown justify content.");
return Rml::Style::JustifyContent::FlexStart;
}
}
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;
}
Style::Style() {
}
Style::~Style() {
}
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:
set_property(Rml::PropertyId::Position, Rml::Style::Position::Absolute);
break;
case Position::Relative:
set_property(Rml::PropertyId::Position, Rml::Style::Position::Relative);
break;
default:
assert(false && "Unknown position.");
break;
}
}
void Style::set_left(float left, Unit unit) {
set_property(Rml::PropertyId::Left, Rml::Property(left, to_rml(unit)));
}
void Style::set_top(float top, Unit unit) {
set_property(Rml::PropertyId::Top, Rml::Property(top, to_rml(unit)));
}
void Style::set_right(float right, Unit unit) {
set_property(Rml::PropertyId::Right, Rml::Property(right, to_rml(unit)));
}
void Style::set_bottom(float bottom, Unit unit) {
set_property(Rml::PropertyId::Bottom, Rml::Property(bottom, to_rml(unit)));
}
void Style::set_width(float width, Unit unit) {
set_property(Rml::PropertyId::Width, Rml::Property(width, to_rml(unit)));
}
void Style::set_width_auto() {
set_property(Rml::PropertyId::Width, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_height(float height, Unit unit) {
set_property(Rml::PropertyId::Height, Rml::Property(height, to_rml(unit)));
}
void Style::set_height_auto() {
set_property(Rml::PropertyId::Height, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_min_width(float width, Unit unit) {
set_property(Rml::PropertyId::MinWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_min_height(float height, Unit unit) {
set_property(Rml::PropertyId::MinHeight, Rml::Property(height, to_rml(unit)));
}
void Style::set_max_width(float width, Unit unit) {
set_property(Rml::PropertyId::MaxWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_max_height(float height, Unit unit) {
set_property(Rml::PropertyId::MaxHeight, Rml::Property(height, to_rml(unit)));
}
void Style::set_padding(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingLeft, Rml::Property(padding, to_rml(unit)));
set_property(Rml::PropertyId::PaddingTop, Rml::Property(padding, to_rml(unit)));
set_property(Rml::PropertyId::PaddingRight, Rml::Property(padding, to_rml(unit)));
set_property(Rml::PropertyId::PaddingBottom, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_left(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingLeft, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_top(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingTop, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_right(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingRight, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_bottom(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingBottom, Rml::Property(padding, to_rml(unit)));
}
void Style::set_margin(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(margin, to_rml(unit)));
set_property(Rml::PropertyId::MarginTop, Rml::Property(margin, to_rml(unit)));
set_property(Rml::PropertyId::MarginRight, Rml::Property(margin, to_rml(unit)));
set_property(Rml::PropertyId::MarginBottom, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_left(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_top(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginTop, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_right(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginRight, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_bottom(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginBottom, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_auto() {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
set_property(Rml::PropertyId::MarginTop, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
set_property(Rml::PropertyId::MarginRight, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
set_property(Rml::PropertyId::MarginBottom, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_left_auto() {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_top_auto() {
set_property(Rml::PropertyId::MarginTop, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_right_auto() {
set_property(Rml::PropertyId::MarginRight, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_bottom_auto() {
set_property(Rml::PropertyId::MarginBottom, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_border_width(float width, Unit unit) {
Rml::Property property(width, to_rml(unit));
set_property(Rml::PropertyId::BorderTopWidth, property);
set_property(Rml::PropertyId::BorderBottomWidth, property);
set_property(Rml::PropertyId::BorderLeftWidth, property);
set_property(Rml::PropertyId::BorderRightWidth, property);
}
void Style::set_border_left_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderLeftWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_top_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderTopWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_right_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderRightWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_bottom_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderBottomWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_radius(float radius, Unit unit) {
Rml::Property property(radius, to_rml(unit));
set_property(Rml::PropertyId::BorderTopLeftRadius, property);
set_property(Rml::PropertyId::BorderTopRightRadius, property);
set_property(Rml::PropertyId::BorderBottomLeftRadius, property);
set_property(Rml::PropertyId::BorderBottomRightRadius, property);
}
void Style::set_border_top_left_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderTopLeftRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_border_top_right_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderTopRightRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_border_bottom_left_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderBottomLeftRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_border_bottom_right_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderBottomRightRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_background_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BackgroundColor, property);
}
void Style::set_border_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderTopColor, property);
set_property(Rml::PropertyId::BorderBottomColor, property);
set_property(Rml::PropertyId::BorderLeftColor, property);
set_property(Rml::PropertyId::BorderRightColor, property);
}
void Style::set_border_left_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderLeftColor, property);
}
void Style::set_border_top_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderTopColor, property);
}
void Style::set_border_right_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderRightColor, property);
}
void Style::set_border_bottom_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderBottomColor, property);
}
void Style::set_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::Color, property);
}
void Style::set_cursor(Cursor cursor) {
switch (cursor) {
case Cursor::None:
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));
break;
default:
assert(false && "Unknown cursor.");
break;
}
}
void Style::set_opacity(float opacity) {
set_property(Rml::PropertyId::Opacity, Rml::Property(opacity, Rml::Unit::NUMBER));
}
void Style::set_display(Display display) {
set_property(Rml::PropertyId::Display, to_rml(display));
}
void Style::set_justify_content(JustifyContent justify_content) {
set_property(Rml::PropertyId::JustifyContent, to_rml(justify_content));
}
void Style::set_flex_grow(float grow) {
set_property(Rml::PropertyId::FlexGrow, Rml::Property(grow, Rml::Unit::NUMBER));
}
void Style::set_flex_shrink(float shrink) {
set_property(Rml::PropertyId::FlexShrink, Rml::Property(shrink, Rml::Unit::NUMBER));
}
void Style::set_flex_basis_auto() {
set_property(Rml::PropertyId::FlexBasis, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_flex_basis(float basis, Unit unit) {
set_property(Rml::PropertyId::FlexBasis, Rml::Property(basis, to_rml(unit)));
}
void Style::set_flex(float grow, float shrink) {
set_flex_grow(grow);
set_flex_shrink(shrink);
set_flex_basis_auto();
}
void Style::set_flex(float grow, float shrink, float basis, Unit basis_unit) {
set_flex_grow(grow);
set_flex_shrink(shrink);
set_flex_basis(basis, basis_unit);
}
void Style::set_flex_direction(FlexDirection flex_direction) {
switch (flex_direction) {
case FlexDirection::Row:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row);
break;
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;
}
}
void Style::set_align_items(AlignItems align_items) {
set_property(Rml::PropertyId::AlignItems, to_rml(align_items));
}
void Style::set_overflow(Overflow overflow) {
set_property(Rml::PropertyId::OverflowX, to_rml(overflow));
set_property(Rml::PropertyId::OverflowY, to_rml(overflow));
}
void Style::set_overflow_x(Overflow overflow) {
set_property(Rml::PropertyId::OverflowX, to_rml(overflow));
}
void Style::set_overflow_y(Overflow overflow) {
set_property(Rml::PropertyId::OverflowY, to_rml(overflow));
}
void Style::set_font_size(float size, Unit unit) {
set_property(Rml::PropertyId::FontSize, Rml::Property(size, to_rml(unit)));
}
void Style::set_letter_spacing(float spacing, Unit unit) {
set_property(Rml::PropertyId::LetterSpacing, Rml::Property(spacing, to_rml(unit)));
}
void Style::set_line_height(float height, Unit unit) {
set_property(Rml::PropertyId::LineHeight, Rml::Property(height, to_rml(unit)));
}
void Style::set_font_style(FontStyle style) {
switch (style) {
case FontStyle::Normal:
set_property(Rml::PropertyId::FontStyle, Rml::Style::FontStyle::Normal);
break;
case FontStyle::Italic:
set_property(Rml::PropertyId::FontStyle, Rml::Style::FontStyle::Italic);
break;
default:
assert(false && "Unknown font style.");
break;
}
}
void Style::set_font_weight(uint32_t weight) {
set_property(Rml::PropertyId::FontWeight, Rml::Style::FontWeight(weight));
}
void Style::set_text_align(TextAlign text_align) {
set_property(Rml::PropertyId::TextAlign, to_rml(text_align));
}
void Style::set_text_transform(TextTransform text_transform) {
set_property(Rml::PropertyId::TextTransform, to_rml(text_transform));
}
void Style::set_gap(float size, Unit unit) {
set_row_gap(size, unit);
set_column_gap(size, unit);
}
void Style::set_row_gap(float size, Unit unit) {
set_property(Rml::PropertyId::RowGap, Rml::Property(size, to_rml(unit)));
}
void Style::set_column_gap(float size, Unit unit) {
set_property(Rml::PropertyId::ColumnGap, Rml::Property(size, to_rml(unit)));
}
void Style::set_drag(Drag drag) {
set_property(Rml::PropertyId::Drag, to_rml(drag));
}
void Style::set_tab_index(TabIndex tab_index) {
set_property(Rml::PropertyId::TabIndex, to_rml(tab_index));
}
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
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#include <string_view>
#include "RmlUi/Core.h"
#include "../core/ui_resource.h"
#include "ui_types.h"
namespace recompui {
class ContextId;
class Style {
friend class Element; // For access to property_map without making it visible to element subclasses.
friend class ContextId;
private:
std::map<Rml::PropertyId, Rml::Property> property_map;
protected:
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property);
ResourceId resource_id = ResourceId::null();
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);
void set_right(float right, Unit unit = Unit::Dp);
void set_bottom(float bottom, Unit unit = Unit::Dp);
void set_width(float width, Unit unit = Unit::Dp);
void set_width_auto();
void set_height(float height, Unit unit = Unit::Dp);
void set_height_auto();
void set_min_width(float width, Unit unit = Unit::Dp);
void set_min_height(float height, Unit unit = Unit::Dp);
void set_max_width(float width, Unit unit = Unit::Dp);
void set_max_height(float height, Unit unit = Unit::Dp);
void set_padding(float padding, Unit unit = Unit::Dp);
void set_padding_left(float padding, Unit unit = Unit::Dp);
void set_padding_top(float padding, Unit unit = Unit::Dp);
void set_padding_right(float padding, Unit unit = Unit::Dp);
void set_padding_bottom(float padding, Unit unit = Unit::Dp);
void set_margin(float margin, Unit unit = Unit::Dp);
void set_margin_left(float margin, Unit unit = Unit::Dp);
void set_margin_top(float margin, Unit unit = Unit::Dp);
void set_margin_right(float margin, Unit unit = Unit::Dp);
void set_margin_bottom(float margin, Unit unit = Unit::Dp);
void set_margin_auto();
void set_margin_left_auto();
void set_margin_top_auto();
void set_margin_right_auto();
void set_margin_bottom_auto();
void set_border_width(float width, Unit unit = Unit::Dp);
void set_border_left_width(float width, Unit unit = Unit::Dp);
void set_border_top_width(float width, Unit unit = Unit::Dp);
void set_border_right_width(float width, Unit unit = Unit::Dp);
void set_border_bottom_width(float width, Unit unit = Unit::Dp);
void set_border_radius(float radius, Unit unit = Unit::Dp);
void set_border_top_left_radius(float radius, Unit unit = Unit::Dp);
void set_border_top_right_radius(float radius, Unit unit = Unit::Dp);
void set_border_bottom_left_radius(float radius, Unit unit = Unit::Dp);
void set_border_bottom_right_radius(float radius, Unit unit = Unit::Dp);
void set_background_color(const Color &color);
void set_border_color(const Color &color);
void set_border_left_color(const Color &color);
void set_border_top_color(const Color &color);
void set_border_right_color(const Color &color);
void set_border_bottom_color(const Color &color);
void set_color(const Color &color);
void set_cursor(Cursor cursor);
void set_opacity(float opacity);
void set_display(Display display);
void set_justify_content(JustifyContent justify_content);
void set_flex_grow(float grow);
void set_flex_shrink(float shrink);
void set_flex_basis_auto();
void set_flex_basis(float basis, Unit unit = Unit::Percent);
void set_flex(float grow, float shrink);
void set_flex(float grow, float shrink, float basis, Unit basis_unit = Unit::Percent);
void set_flex_direction(FlexDirection flex_direction);
void set_align_items(AlignItems align_items);
void set_overflow(Overflow overflow);
void set_overflow_x(Overflow overflow);
void set_overflow_y(Overflow overflow);
void set_font_size(float size, Unit unit = Unit::Dp);
void set_letter_spacing(float spacing, Unit unit = Unit::Dp);
void set_line_height(float height, Unit unit = Unit::Dp);
void set_font_style(FontStyle style);
void set_font_weight(uint32_t weight);
void set_text_align(TextAlign text_align);
void set_text_transform(TextTransform text_transform);
void set_gap(float size, Unit unit = Unit::Dp);
void set_row_gap(float size, Unit unit = Unit::Dp);
void set_column_gap(float size, Unit unit = Unit::Dp);
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; }
};
} // namespace recompui
+51
View File
@@ -0,0 +1,51 @@
#include "ui_text_input.h"
#include <cassert>
namespace recompui {
void TextInput::process_event(const Event &e) {
switch (e.type) {
case EventType::Text: {
const EventText &event = std::get<EventText>(e.variant);
text = event.text;
for (const auto &function : text_changed_callbacks) {
function(text);
}
break;
}
default:
break;
}
}
TextInput::TextInput(Element *parent, bool text_visible) : Element(parent, Events(EventType::Text), "input") {
if (!text_visible) {
set_attribute("type", "password");
}
set_min_width(60.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) {
this->text = std::string(text);
set_attribute("value", this->text);
}
const std::string &TextInput::get_text() {
return text;
}
void TextInput::add_text_changed_callback(std::function<void(const std::string &)> callback) {
text_changed_callbacks.emplace_back(callback);
}
};
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class TextInput : public Element {
private:
std::string text;
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "TextInput"; }
public:
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);
};
} // namespace recompui
+161
View File
@@ -0,0 +1,161 @@
#include "ui_toggle.h"
#include <cassert>
#include <ultramodern/ultramodern.hpp>
namespace recompui {
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);
set_opacity(0.9f);
set_cursor(Cursor::Pointer);
set_border_width(2.0f);
set_border_color(Color{ 177, 76, 34, 255 });
set_background_color(Color{ 0, 0, 0, 0 });
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 });
ContextId context = get_current_context();
floater = context.create_element<Element>(this);
floater->set_position(Position::Relative);
floater->set_top(2.0f);
floater->set_width(80.0f);
floater->set_height(64.0f);
floater->set_border_radius(32.0f);
floater->set_background_color(Color{ 177, 76, 34, 255 });
floater_checked_style.set_background_color(Color{ 34, 177, 76, 255 });
floater_disabled_style.set_background_color(Color{ 177, 76, 34, 128 });
floater_disabled_checked_style.set_background_color(Color{ 34, 177, 76, 128 });
floater->add_style(&floater_checked_style, checked_state);
floater->add_style(&floater_disabled_style, disabled_state);
floater->add_style(&floater_disabled_checked_style, { checked_state, disabled_state });
set_checked_internal(false, false, true, false);
}
void Toggle::set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks) {
if (this->checked != checked || setup) {
this->checked = checked;
if (animate) {
last_time = ultramodern::time_since_start();
queue_update();
}
else {
floater_left = floater_left_target();
}
floater->set_left(floater_left, Unit::Dp);
if (trigger_callbacks) {
for (const auto &function : checked_callbacks) {
function(checked);
}
}
set_style_enabled(checked_state, checked);
floater->set_style_enabled(checked_state, checked);
}
}
float Toggle::floater_left_target() const {
return checked ? 78.0f : 4.0f;
}
void Toggle::process_event(const Event &e) {
switch (e.type) {
case EventType::Click:
if (is_enabled()) {
set_checked_internal(!checked, true, false, true);
}
break;
case EventType::Hover: {
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: {
std::chrono::high_resolution_clock::duration now = ultramodern::time_since_start();
float delta_time = std::max(std::chrono::duration<float>(now - last_time).count(), 0.0f);
last_time = now;
constexpr float dp_speed = 740.0f;
const float target = floater_left_target();
if (target < floater_left) {
floater_left += std::max(-dp_speed * delta_time, target - floater_left);
}
else {
floater_left += std::min(dp_speed * delta_time, target - floater_left);
}
if (abs(target - floater_left) < 1e-4f) {
floater_left = target;
}
else {
queue_update();
}
floater->set_left(floater_left, Unit::Dp);
break;
}
default:
break;
}
}
void Toggle::set_checked(bool checked) {
set_checked_internal(checked, false, false, false);
}
bool Toggle::is_checked() const {
return checked;
}
void Toggle::add_checked_callback(std::function<void(bool)> callback) {
checked_callbacks.emplace_back(callback);
}
};
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Toggle : public Element {
protected:
Element *floater;
float floater_left = 0.0f;
std::chrono::high_resolution_clock::duration last_time;
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;
Style floater_disabled_style;
Style floater_disabled_checked_style;
bool checked = false;
void set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks);
float floater_left_target() const;
// 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);
bool is_checked() const;
void add_checked_callback(std::function<void(bool)> callback);
};
} // namespace recompui
+260
View File
@@ -0,0 +1,260 @@
#pragma once
#include <stdint.h>
#include <variant>
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 {
uint8_t r = 255;
uint8_t g = 255;
uint8_t b = 255;
uint8_t a = 255;
};
enum class Cursor {
None,
Pointer
};
// These two enums must be kept in sync with patches/recompui_event_structs.h!
enum class EventType {
None,
Click,
Focus,
Hover,
Enable,
Drag,
Text,
Update,
Navigate,
Count
};
enum class DragPhase {
None,
Start,
Move,
End
};
enum class NavDirection {
Up,
Right,
Down,
Left
};
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);
}
template <typename Enum, typename... Enums, typename = std::enable_if_t<std::is_enum_v<Enum>>>
constexpr uint32_t Events(Enum first, Enums... rest) {
return Events(first) | Events(rest...);
}
struct EventClick {
float x;
float y;
};
struct EventFocus {
bool active;
};
struct EventHover {
bool active;
};
struct EventEnable {
bool active;
};
struct EventDrag {
float x;
float y;
DragPhase phase;
};
struct EventText {
std::string text;
};
struct EventNavigate {
NavDirection direction;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, std::monostate>;
struct Event {
EventType type;
EventVariant variant;
// Factory methods for creating specific events
static Event click_event(float x, float y) {
Event e;
e.type = EventType::Click;
e.variant = EventClick{ x, y };
return e;
}
static Event focus_event(bool active) {
Event e;
e.type = EventType::Focus;
e.variant = EventFocus{ active };
return e;
}
static Event hover_event(bool active) {
Event e;
e.type = EventType::Hover;
e.variant = EventHover{ active };
return e;
}
static Event enable_event(bool enable) {
Event e;
e.type = EventType::Enable;
e.variant = EventEnable{ enable };
return e;
}
static Event drag_event(float x, float y, DragPhase phase) {
Event e;
e.type = EventType::Drag;
e.variant = EventDrag{ x, y, phase };
return e;
}
static Event text_event(const std::string &text) {
Event e;
e.type = EventType::Text;
e.variant = EventText{ text };
return e;
}
static Event update_event() {
Event e;
e.type = EventType::Update;
e.variant = std::monostate{};
return e;
}
static Event navigate_event(NavDirection direction) {
Event e;
e.type = EventType::Navigate;
e.variant = EventNavigate{ direction };
return e;
}
};
enum class Display {
None,
Block,
Inline,
InlineBlock,
FlowRoot,
Flex,
InlineFlex,
Table,
InlineTable,
TableRow,
TableRowGroup,
TableColumn,
TableColumnGroup,
TableCell
};
enum class Visibility {
Visible,
Hidden
};
enum class Position {
Absolute,
Relative
};
enum class JustifyContent {
FlexStart,
FlexEnd,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly
};
enum class FlexDirection {
Row,
Column,
RowReverse,
ColumnReverse
};
enum class AlignItems {
FlexStart,
FlexEnd,
Center,
Baseline,
Stretch
};
enum class Overflow {
Visible,
Hidden,
Auto,
Scroll
};
enum class Unit {
Px,
Dp,
Percent
};
enum class AnimationType : uint32_t {
None,
Set,
Tween
};
enum class FontStyle {
Normal,
Italic
};
enum class TextAlign {
Left,
Right,
Center,
Justify
};
enum class TextTransform {
None,
Capitalize,
Uppercase,
Lowercase
};
enum class Drag {
None,
Drag,
DragDrop,
Block,
Clone
};
enum class TabIndex {
None,
Auto
};
} // namespace recompui
+1031
View File
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
#include "concurrentqueue.h"
#include "overloaded.h"
#include "recomp_ui.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
#include "elements/ui_element.h"
#include "elements/ui_button.h"
#include "elements/ui_clickable.h"
#include "elements/ui_container.h"
#include "elements/ui_image.h"
#include "elements/ui_label.h"
#include "elements/ui_radio.h"
#include "elements/ui_scroll_container.h"
#include "elements/ui_slider.h"
#include "elements/ui_style.h"
#include "elements/ui_text_input.h"
#include "elements/ui_toggle.h"
#include "elements/ui_types.h"
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "../patches/ui_funcs.h"
struct QueuedCallback {
recompui::ResourceId resource;
recompui::Event event;
recompui::UICallback callback;
};
moodycamel::ConcurrentQueue<QueuedCallback> queued_callbacks{};
void recompui::queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback) {
queued_callbacks.enqueue(QueuedCallback{ .resource = resource, .event = e, .callback = callback });
}
bool convert_event(const recompui::Event& in, RecompuiEventData& out) {
bool skip = false;
out = {};
out.type = static_cast<RecompuiEventType>(in.type);
switch (in.type) {
default:
case recompui::EventType::None:
case recompui::EventType::Count:
skip = true;
break;
case recompui::EventType::Click:
{
const recompui::EventClick &click = std::get<recompui::EventClick>(in.variant);
out.data.click.x = click.x;
out.data.click.y = click.y;
}
break;
case recompui::EventType::Focus:
{
const recompui::EventFocus &focus = std::get<recompui::EventFocus>(in.variant);
out.data.focus.active = focus.active;
}
break;
case recompui::EventType::Hover:
{
const recompui::EventHover &hover = std::get<recompui::EventHover>(in.variant);
out.data.hover.active = hover.active;
}
break;
case recompui::EventType::Enable:
{
const recompui::EventEnable &enable = std::get<recompui::EventEnable>(in.variant);
out.data.enable.active = enable.active;
}
break;
case recompui::EventType::Drag:
{
const recompui::EventDrag &drag = std::get<recompui::EventDrag>(in.variant);
out.data.drag.phase = static_cast<RecompuiDragPhase>(drag.phase);
out.data.drag.x = drag.x;
out.data.drag.y = drag.y;
}
break;
case recompui::EventType::Text:
skip = true; // Text events aren't supported in the UI mod API.
break;
case recompui::EventType::Update:
// No data for an update event.
break;
}
return !skip;
}
extern "C" void recomp_run_ui_callbacks(uint8_t* rdram, recomp_context* ctx) {
// Allocate the event on the stack.
gpr stack_frame = ctx->r29;
ctx->r29 -= sizeof(RecompuiEventData);
RecompuiEventData* event_data = TO_PTR(RecompuiEventData, stack_frame);
QueuedCallback cur_callback;
while (queued_callbacks.try_dequeue(cur_callback)) {
if (convert_event(cur_callback.event, *event_data)) {
recompui::ContextId cur_context = cur_callback.callback.context;
cur_context.open();
ctx->r4 = static_cast<int32_t>(cur_callback.resource.slot_id);
ctx->r5 = stack_frame;
ctx->r6 = cur_callback.callback.userdata;
LOOKUP_FUNC(cur_callback.callback.callback)(rdram, ctx);
cur_context.close();
}
}
ctx->r29 += sizeof(RecompuiEventData);
}
+131
View File
@@ -0,0 +1,131 @@
#include <mutex>
#include <unordered_set>
#include "recomp_ui.h"
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "ultramodern/error_handling.hpp"
#include "ui_helpers.h"
#include "ui_api_images.h"
#include "elements/ui_image.h"
using namespace recompui;
struct {
std::mutex mutex;
std::unordered_set<uint32_t> textures{};
uint32_t textures_created = 0;
} TextureState;
const std::string mod_texture_prefix = "?/mod_api/";
static std::string get_texture_name(uint32_t texture_id) {
return mod_texture_prefix + std::to_string(texture_id);
}
static uint32_t get_new_texture_id() {
std::lock_guard lock{TextureState.mutex};
uint32_t cur_id = TextureState.textures_created++;
TextureState.textures.emplace(cur_id);
return cur_id;
}
static void release_texture(uint32_t texture_id) {
std::string texture_name = get_texture_name(texture_id);
std::lock_guard lock{TextureState.mutex};
if (TextureState.textures.erase(texture_id) == 0) {
recompui::message_box("Fatal error in mod - attempted to destroy texture that doesn't exist!");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
recompui::release_image(texture_name);
}
thread_local std::vector<char> swapped_image_bytes;
void recompui_create_texture_rgba32(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t width = _arg<1, uint32_t>(rdram, ctx);
uint32_t height = _arg<2, uint32_t>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's pixel data.
size_t size_bytes = width * height * 4 * sizeof(uint8_t);
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the pixel data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_rgba32(texture_name, swapped_image_bytes, width, height);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_create_texture_image_bytes(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t size_bytes = _arg<1, u32>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's data.
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the image's data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_file(texture_name, swapped_image_bytes);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_destroy_texture(uint8_t* rdram, recomp_context* ctx) {
uint32_t texture_id = _arg<0, uint32_t>(rdram, ctx);
release_texture(texture_id);
}
void recompui_create_imageview(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
uint32_t texture_id = _arg<2, uint32_t>(rdram, ctx);
Element* ret = ui_context.create_element<Image>(parent, get_texture_name(texture_id));
return_resource(ctx, ret->get_resource_id());
}
void recompui_set_imageview_texture(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t texture_id = _arg<1, uint32_t>(rdram, ctx);
if (!resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set texture of non-element");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_src(get_texture_name(texture_id));
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_image_exports() {
REGISTER_FUNC(recompui_create_texture_rgba32);
REGISTER_FUNC(recompui_create_texture_image_bytes);
REGISTER_FUNC(recompui_destroy_texture);
REGISTER_FUNC(recompui_create_imageview);
REGISTER_FUNC(recompui_set_imageview_texture);
}
+10
View File
@@ -0,0 +1,10 @@
#ifndef __UI_API_IMAGES_H__
#define __UI_API_IMAGES_H__
#include <cstdint>
namespace recompui {
void register_ui_image_exports();
}
#endif
+139 -139
View File
@@ -7,166 +7,166 @@
using ColourMap = Rml::UnorderedMap<Rml::String, Rml::Colourb>;
namespace recompui {
class PropertyParserColorHack : public Rml::PropertyParser {
public:
PropertyParserColorHack();
virtual ~PropertyParserColorHack();
bool ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const override;
private:
static ColourMap html_colours;
};
static_assert(sizeof(PropertyParserColorHack) == sizeof(Rml::PropertyParserColour));
PropertyParserColorHack::PropertyParserColorHack() {
html_colours["black"] = Rml::Colourb(0, 0, 0);
html_colours["silver"] = Rml::Colourb(192, 192, 192);
html_colours["gray"] = Rml::Colourb(128, 128, 128);
html_colours["grey"] = Rml::Colourb(128, 128, 128);
html_colours["white"] = Rml::Colourb(255, 255, 255);
html_colours["maroon"] = Rml::Colourb(128, 0, 0);
html_colours["red"] = Rml::Colourb(255, 0, 0);
html_colours["orange"] = Rml::Colourb(255, 165, 0);
html_colours["purple"] = Rml::Colourb(128, 0, 128);
html_colours["fuchsia"] = Rml::Colourb(255, 0, 255);
html_colours["green"] = Rml::Colourb(0, 128, 0);
html_colours["lime"] = Rml::Colourb(0, 255, 0);
html_colours["olive"] = Rml::Colourb(128, 128, 0);
html_colours["yellow"] = Rml::Colourb(255, 255, 0);
html_colours["navy"] = Rml::Colourb(0, 0, 128);
html_colours["blue"] = Rml::Colourb(0, 0, 255);
html_colours["teal"] = Rml::Colourb(0, 128, 128);
html_colours["aqua"] = Rml::Colourb(0, 255, 255);
html_colours["transparent"] = Rml::Colourb(0, 0, 0, 0);
html_colours["whitesmoke"] = Rml::Colourb(245, 245, 245);
}
class PropertyParserColorHack : public Rml::PropertyParser {
public:
PropertyParserColorHack();
virtual ~PropertyParserColorHack();
bool ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const override;
private:
static ColourMap html_colours;
};
static_assert(sizeof(PropertyParserColorHack) == sizeof(Rml::PropertyParserColour));
PropertyParserColorHack::PropertyParserColorHack() {
html_colours["black"] = Rml::Colourb(0, 0, 0);
html_colours["silver"] = Rml::Colourb(192, 192, 192);
html_colours["gray"] = Rml::Colourb(128, 128, 128);
html_colours["grey"] = Rml::Colourb(128, 128, 128);
html_colours["white"] = Rml::Colourb(255, 255, 255);
html_colours["maroon"] = Rml::Colourb(128, 0, 0);
html_colours["red"] = Rml::Colourb(255, 0, 0);
html_colours["orange"] = Rml::Colourb(255, 165, 0);
html_colours["purple"] = Rml::Colourb(128, 0, 128);
html_colours["fuchsia"] = Rml::Colourb(255, 0, 255);
html_colours["green"] = Rml::Colourb(0, 128, 0);
html_colours["lime"] = Rml::Colourb(0, 255, 0);
html_colours["olive"] = Rml::Colourb(128, 128, 0);
html_colours["yellow"] = Rml::Colourb(255, 255, 0);
html_colours["navy"] = Rml::Colourb(0, 0, 128);
html_colours["blue"] = Rml::Colourb(0, 0, 255);
html_colours["teal"] = Rml::Colourb(0, 128, 128);
html_colours["aqua"] = Rml::Colourb(0, 255, 255);
html_colours["transparent"] = Rml::Colourb(0, 0, 0, 0);
html_colours["whitesmoke"] = Rml::Colourb(245, 245, 245);
}
PropertyParserColorHack::~PropertyParserColorHack() {}
PropertyParserColorHack::~PropertyParserColorHack() {}
bool PropertyParserColorHack::ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const {
if (value.empty())
return false;
bool PropertyParserColorHack::ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const {
if (value.empty())
return false;
Rml::Colourb colour;
Rml::Colourb colour;
// Check for a hex colour.
if (value[0] == '#')
{
char hex_values[4][2] = { {'f', 'f'}, {'f', 'f'}, {'f', 'f'}, {'f', 'f'} };
// Check for a hex colour.
if (value[0] == '#')
{
char hex_values[4][2] = { {'f', 'f'}, {'f', 'f'}, {'f', 'f'}, {'f', 'f'} };
switch (value.size())
{
// Single hex digit per channel, RGB and alpha.
case 5:
hex_values[3][0] = hex_values[3][1] = value[4];
//-fallthrough
// Single hex digit per channel, RGB only.
case 4:
hex_values[0][0] = hex_values[0][1] = value[1];
hex_values[1][0] = hex_values[1][1] = value[2];
hex_values[2][0] = hex_values[2][1] = value[3];
break;
switch (value.size())
{
// Single hex digit per channel, RGB and alpha.
case 5:
hex_values[3][0] = hex_values[3][1] = value[4];
//-fallthrough
// Single hex digit per channel, RGB only.
case 4:
hex_values[0][0] = hex_values[0][1] = value[1];
hex_values[1][0] = hex_values[1][1] = value[2];
hex_values[2][0] = hex_values[2][1] = value[3];
break;
// Two hex digits per channel, RGB and alpha.
case 9:
hex_values[3][0] = value[7];
hex_values[3][1] = value[8];
//-fallthrough
// Two hex digits per channel, RGB only.
case 7: memcpy(hex_values, &value.c_str()[1], sizeof(char) * 6); break;
// Two hex digits per channel, RGB and alpha.
case 9:
hex_values[3][0] = value[7];
hex_values[3][1] = value[8];
//-fallthrough
// Two hex digits per channel, RGB only.
case 7: memcpy(hex_values, &value.c_str()[1], sizeof(char) * 6); break;
default: return false;
}
default: return false;
}
// Parse each of the colour elements.
for (int i = 0; i < 4; i++)
{
int tens = Rml::Math::HexToDecimal(hex_values[i][0]);
int ones = Rml::Math::HexToDecimal(hex_values[i][1]);
if (tens == -1 || ones == -1)
return false;
// Parse each of the colour elements.
for (size_t i = 0; i < 4; i++)
{
int tens = Rml::Math::HexToDecimal(hex_values[i][0]);
int ones = Rml::Math::HexToDecimal(hex_values[i][1]);
if (tens == -1 || ones == -1)
return false;
colour[i] = (Rml::byte)(tens * 16 + ones);
}
}
else if (value.substr(0, 3) == "rgb")
{
Rml::StringList values;
values.reserve(4);
colour[i] = (Rml::byte)(tens * 16 + ones);
}
}
else if (value.substr(0, 3) == "rgb")
{
Rml::StringList values;
values.reserve(4);
size_t find = value.find('(');
if (find == Rml::String::npos)
return false;
size_t find = value.find('(');
if (find == Rml::String::npos)
return false;
size_t begin_values = find + 1;
size_t begin_values = find + 1;
Rml::StringUtilities::ExpandString(values, value.substr(begin_values, value.rfind(')') - begin_values), ',');
Rml::StringUtilities::ExpandString(values, value.substr(begin_values, value.rfind(')') - begin_values), ',');
// Check if we're parsing an 'rgba' or 'rgb' colour declaration.
if (value.size() > 3 && value[3] == 'a')
{
if (values.size() != 4)
return false;
}
else
{
if (values.size() != 3)
return false;
// Check if we're parsing an 'rgba' or 'rgb' colour declaration.
if (value.size() > 3 && value[3] == 'a')
{
if (values.size() != 4)
return false;
}
else
{
if (values.size() != 3)
return false;
values.push_back("255");
}
values.push_back("255");
}
// Parse the three RGB values.
for (int i = 0; i < 3; ++i)
{
int component;
// Parse the three RGB values.
for (size_t i = 0; i < 3; ++i)
{
int component;
// We're parsing a percentage value.
if (values[i].size() > 0 && values[i][values[i].size() - 1] == '%')
component = int((float)atof(values[i].substr(0, values[i].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 255 integer value.
else
component = atoi(values[i].c_str());
// We're parsing a percentage value.
if (values[i].size() > 0 && values[i][values[i].size() - 1] == '%')
component = int((float)atof(values[i].substr(0, values[i].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 255 integer value.
else
component = atoi(values[i].c_str());
colour[i] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
// Parse the alpha value. Modified from the original RmlUi implementation to use 0-1 instead of 0-255.
{
int component;
colour[i] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
// Parse the alpha value. Modified from the original RmlUi implementation to use 0-1 instead of 0-255.
{
int component;
// We're parsing a percentage value.
if (values[3].size() > 0 && values[3][values[3].size() - 1] == '%')
component = ((float)atof(values[3].substr(0, values[3].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 1 float value.
else
component = atof(values[3].c_str()) * 255.0f;
// We're parsing a percentage value.
if (values[3].size() > 0 && values[3][values[3].size() - 1] == '%')
component = ((float)atof(values[3].substr(0, values[3].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 1 float value.
else
component = atof(values[3].c_str()) * 255.0f;
colour[3] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
}
else
{
// Check for the specification of an HTML colour.
ColourMap::const_iterator iterator = html_colours.find(Rml::StringUtilities::ToLower(value));
if (iterator == html_colours.end())
return false;
else
colour = (*iterator).second;
}
colour[3] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
}
else
{
// Check for the specification of an HTML colour.
ColourMap::const_iterator iterator = html_colours.find(Rml::StringUtilities::ToLower(value));
if (iterator == html_colours.end())
return false;
else
colour = (*iterator).second;
}
property.value = Rml::Variant(colour);
property.unit = Rml::Unit::COLOUR;
property.value = Rml::Variant(colour);
property.unit = Rml::Unit::COLOUR;
return true;
}
return true;
}
// This hack overwrites the contents of a property parser pointer for "color" (which is known to point to a valid Rml::PropertyParserColour) with the contents of a PropertyParserColorHack.
// This overwrites the object's vtable, allowing us to override color parsing behavior to use 0-1 alpha instead of 0-255.
// Ideally we'd just replace the pointer itself, but RmlUi doesn't provide a way to do that currently.
void apply_color_hack() {
// Allocate and leak a parser to act as a vtable source.
PropertyParserColorHack* new_parser = new PropertyParserColorHack();
// Copy the allocated object into the color parser pointer to overwrite its vtable.
memcpy((void*)Rml::StyleSheetSpecification::GetParser("color"), (void*)new_parser, sizeof(*new_parser));
}
// This hack overwrites the contents of a property parser pointer for "color" (which is known to point to a valid Rml::PropertyParserColour) with the contents of a PropertyParserColorHack.
// This overwrites the object's vtable, allowing us to override color parsing behavior to use 0-1 alpha instead of 0-255.
// Ideally we'd just replace the pointer itself, but RmlUi doesn't provide a way to do that currently.
void apply_color_hack() {
// Allocate and leak a parser to act as a vtable source.
PropertyParserColorHack* new_parser = new PropertyParserColorHack();
// Copy the allocated object into the color parser pointer to overwrite its vtable.
memcpy((void*)Rml::StyleSheetSpecification::GetParser("color"), (void*)new_parser, sizeof(*new_parser));
}
ColourMap PropertyParserColorHack::html_colours{};
ColourMap PropertyParserColorHack::html_colours{};
}
+767 -761
View File
File diff suppressed because it is too large Load Diff
+250
View File
@@ -0,0 +1,250 @@
#include "ui_config_sub_menu.h"
#include <cassert>
#include <string_view>
#include "recomp_ui.h"
namespace recompui {
// ConfigOptionElement
void ConfigOptionElement::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover:
hover_callback(this, std::get<EventHover>(e.variant).active);
break;
case EventType::Update:
break;
default:
assert(false && "Unknown event type.");
break;
}
}
ConfigOptionElement::ConfigOptionElement(Element *parent) : Element(parent, Events(EventType::Hover)) {
set_display(Display::Flex);
set_flex_direction(FlexDirection::Column);
set_gap(16.0f);
set_height(100.0f);
name_label = get_current_context().create_element<Label>(this, LabelStyle::Normal);
}
ConfigOptionElement::~ConfigOptionElement() {
}
void ConfigOptionElement::set_option_id(std::string_view id) {
this->option_id = id;
}
void ConfigOptionElement::set_name(std::string_view name) {
this->name = name;
name_label->set_text(name);
}
void ConfigOptionElement::set_description(std::string_view description) {
this->description = description;
}
void ConfigOptionElement::set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback) {
hover_callback = callback;
}
const std::string &ConfigOptionElement::get_description() const {
return description;
}
// ConfigOptionSlider
void ConfigOptionSlider::slider_value_changed(double 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([this](double v){ slider_value_changed(v); });
}
// ConfigOptionTextInput
void ConfigOptionTextInput::text_changed(const std::string &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([this](const std::string &text){ text_changed(text); });
}
// ConfigOptionRadio
void ConfigOptionRadio::index_changed(uint32_t 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([this](uint32_t index){ index_changed(index); });
for (std::string_view option : options) {
radio->add_option(option);
}
if (value < options.size()) {
radio->set_index(value);
}
}
// ConfigSubMenu
void ConfigSubMenu::back_button_pressed() {
// Hide the config sub menu and show the config menu.
ContextId config_context = recompui::get_config_context_id();
ContextId sub_menu_context = recompui::get_config_sub_menu_context_id();
recompui::hide_context(sub_menu_context);
recompui::show_context(config_context, "");
recompui::focus_mod_configure_button();
}
void ConfigSubMenu::option_hovered(ConfigOptionElement *option, bool active) {
if (active) {
hover_option_elements.emplace(option);
}
else {
hover_option_elements.erase(option);
}
if (hover_option_elements.empty()) {
description_label->set_text("");
}
else {
description_label->set_text((*hover_option_elements.begin())->get_description());
}
}
ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
using namespace std::string_view_literals;
set_display(Display::Flex);
set_flex(1, 1, 100.0f, Unit::Percent);
set_flex_direction(FlexDirection::Column);
set_height(100.0f, Unit::Percent);
recompui::ContextId context = get_current_context();
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(12.0f);
header_container->set_gap(24.0f);
{
back_button = context.create_element<Button>(header_container, "Back", ButtonStyle::Secondary);
back_button->add_pressed_callback([this](){ back_button_pressed(); });
title_label = context.create_element<Label>(header_container, "Title", LabelStyle::Large);
}
body_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceEvenly);
body_container->set_padding(32.0f);
{
config_container = context.create_element<Container>(body_container, FlexDirection::Column, JustifyContent::Center);
config_container->set_display(Display::Block);
config_container->set_flex_basis(100.0f);
config_container->set_align_items(AlignItems::Center);
{
config_scroll_container = context.create_element<ScrollContainer>(config_container, ScrollDirection::Vertical);
}
description_label = context.create_element<Label>(body_container, "Description", LabelStyle::Small);
description_label->set_min_width(800.0f);
}
recompui::get_current_context().set_autofocus_element(back_button);
}
ConfigSubMenu::~ConfigSubMenu() {
}
void ConfigSubMenu::enter(std::string_view title) {
title_label->set_text(title);
}
void ConfigSubMenu::clear_options() {
config_scroll_container->clear_children();
config_option_elements.clear();
hover_option_elements.clear();
}
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); });
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);
}
void ConfigSubMenu::add_slider_option(std::string_view id, std::string_view name, std::string_view description, double value, double min, double max, double step, bool percent, std::function<void(const std::string &, double)> callback) {
ConfigOptionSlider *option_slider = get_current_context().create_element<ConfigOptionSlider>(config_scroll_container, value, min, max, step, percent, callback);
add_option(option_slider, id, name, description);
}
void ConfigSubMenu::add_text_option(std::string_view id, std::string_view name, std::string_view description, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) {
ConfigOptionTextInput *option_text_input = get_current_context().create_element<ConfigOptionTextInput>(config_scroll_container, value, callback);
add_option(option_text_input, id, name, description);
}
void ConfigSubMenu::add_radio_option(std::string_view id, std::string_view name, std::string_view description, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) {
ConfigOptionRadio *option_radio = get_current_context().create_element<ConfigOptionRadio>(config_scroll_container, value, options, callback);
add_option(option_radio, id, name, description);
}
// ElementConfigSubMenu
ElementConfigSubMenu::ElementConfigSubMenu(const Rml::String &tag) : Rml::Element(tag) {
SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Flex);
SetProperty("width", "100%");
SetProperty("height", "100%");
recompui::Element this_compat(this);
recompui::ContextId context = get_current_context();
config_sub_menu = context.create_element<ConfigSubMenu>(&this_compat);
}
ElementConfigSubMenu::~ElementConfigSubMenu() {
}
void ElementConfigSubMenu::set_display(bool display) {
SetProperty(Rml::PropertyId::Display, display ? Rml::Style::Display::Block : Rml::Style::Display::None);
}
ConfigSubMenu *ElementConfigSubMenu::get_config_sub_menu_element() const {
return config_sub_menu;
}
}
+115
View File
@@ -0,0 +1,115 @@
#ifndef RECOMPUI_CONFIG_SUB_MENU_H
#define RECOMPUI_CONFIG_SUB_MENU_H
#include <span>
#include "elements/ui_button.h"
#include "elements/ui_container.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_text_input.h"
namespace recompui {
class ConfigOptionElement : public Element {
protected:
Label *name_label = nullptr;
std::string option_id;
std::string name;
std::string description;
std::function<void(ConfigOptionElement *, bool)> hover_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_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);
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 {
protected:
Slider *slider = nullptr;
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 {
protected:
TextInput *text_input = nullptr;
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 {
protected:
Radio *radio = nullptr;
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 {
private:
Container *header_container = nullptr;
Button *back_button = nullptr;
Label *title_label = nullptr;
Container *body_container = nullptr;
Label *description_label = nullptr;
Container *config_container = nullptr;
ScrollContainer *config_scroll_container = nullptr;
std::vector<ConfigOptionElement *> config_option_elements;
std::unordered_set<ConfigOptionElement *> hover_option_elements;
void back_button_pressed();
void option_hovered(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();
void enter(std::string_view title);
void clear_options();
void add_slider_option(std::string_view id, std::string_view name, std::string_view description, double value, double min, double max, double step, bool percent, std::function<void(const std::string &, double)> callback);
void add_text_option(std::string_view id, std::string_view name, std::string_view description, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
void add_radio_option(std::string_view id, std::string_view name, std::string_view description, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
};
class ElementConfigSubMenu : public Rml::Element {
public:
ElementConfigSubMenu(const Rml::String &tag);
virtual ~ElementConfigSubMenu();
void set_display(bool display);
ConfigSubMenu *get_config_sub_menu_element() const;
private:
ConfigSubMenu *config_sub_menu;
};
}
#endif
+42
View File
@@ -0,0 +1,42 @@
#include "ui_elements.h"
struct RecompCustomElement {
Rml::String tag;
std::unique_ptr<Rml::ElementInstancer> instancer;
};
#define CUSTOM_ELEMENT(s, e) { s, std::make_unique< Rml::ElementInstancerGeneric< e > >() }
static RecompCustomElement custom_elements[] = {
CUSTOM_ELEMENT("recomp-mod-menu", recompui::ElementModMenu),
CUSTOM_ELEMENT("recomp-config-sub-menu", recompui::ElementConfigSubMenu),
};
void recompui::register_custom_elements() {
for (auto& element_config : custom_elements) {
Rml::Factory::RegisterElementInstancer(element_config.tag, element_config.instancer.get());
}
}
Rml::ElementInstancer* recompui::get_custom_element_instancer(std::string tag) {
for (auto& element_config : custom_elements) {
if (tag == element_config.tag) {
return element_config.instancer.get();
}
}
return nullptr;
}
Rml::ElementPtr recompui::create_custom_element(Rml::Element* parent, std::string tag) {
auto instancer = recompui::get_custom_element_instancer(tag);
const Rml::XMLAttributes attributes = {};
if (Rml::ElementPtr element = instancer->InstanceElement(parent, tag, attributes))
{
element->SetInstancer(instancer);
element->SetAttributes(attributes);
return element;
}
return nullptr;
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef RECOMPUI_ELEMENTS_H
#define RECOMPUI_ELEMENTS_H
#include "recomp_ui.h"
#include "RmlUi/Core/Element.h"
#include "ui_mod_menu.h"
#include "ui_config_sub_menu.h"
namespace recompui {
void register_custom_elements();
Rml::ElementInstancer* get_custom_element_instancer(std::string tag);
}
#endif
+154
View File
@@ -0,0 +1,154 @@
#ifndef __UI_HELPERS_H__
#define __UI_HELPERS_H__
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "elements/ui_element.h"
#include "elements/ui_types.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
namespace recompui {
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
inline ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
return ContextId{ .slot_id = context_id };
}
inline float arg_float2(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<2, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float3(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<3, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float4(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x10, ctx->r29);
return val.f32;
}
inline float arg_float5(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x14, ctx->r29);
return val.f32;
}
inline float arg_float6(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x18, ctx->r29);
return val.f32;
}
template <int arg_index>
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
return ResourceId{ .slot_id = slot_id };
}
template <int arg_index>
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
return ui_context.get_root_element();
}
return resource.as_element();
}
template <int arg_index>
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
ContextId ui_context = recompui::get_current_context();
return ui_context.get_root_element();
}
return *resource;
}
template <int arg_index>
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
Color ret{};
ret.r = MEM_B(0, color_arg);
ret.g = MEM_B(1, color_arg);
ret.b = MEM_B(2, color_arg);
ret.a = MEM_B(3, color_arg);
return ret;
}
inline void return_resource(recomp_context* ctx, ResourceId resource) {
_return<uint32_t>(ctx, resource.slot_id);
}
inline void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) {
gpr addr = (reinterpret_cast<uint8_t*>(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL;
for (size_t i = 0; i < ret.size(); i++) {
MEM_B(i, addr) = ret[i];
}
MEM_B(ret.size(), addr) = '\x00';
_return<PTR(char)>(ctx, addr);
}
inline std::string decode_string(uint8_t* rdram, PTR(char) str) {
// Get the length of the byteswapped string.
size_t len = 0;
while (MEM_B(str, len) != 0x00) {
len++;
}
std::string ret{};
ret.reserve(len + 1);
for (size_t i = 0; i < len; i++) {
ret += (char)MEM_B(str, i);
}
return ret;
}
}
#endif
+92 -78
View File
@@ -15,97 +15,111 @@ bool mm_rom_valid = false;
extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
zelda64::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:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_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;
}
}
});
nfdnchar_t* native_path = nullptr;
zelda64::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:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_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;
recompui::ContextId recompui::get_launcher_context_id() {
return launcher_context;
}
class LauncherMenu : public recompui::MenuController {
public:
LauncherMenu() {
mm_rom_valid = recomp::is_rom_valid(supported_games[0].game_id);
mm_rom_valid = recomp::is_rom_valid(supported_games[0].game_id);
}
~LauncherMenu() override {
~LauncherMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
const std::filesystem::path asset = zelda64::get_asset_path("launcher.rml");
return context->LoadDocument(asset.string());
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
[](const std::string& param, Rml::Event& event) {
select_rom();
}
);
recompui::register_event(listener, "rom_selected",
[](const std::string& param, Rml::Event& event) {
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
}
);
recompui::register_event(listener, "start_game",
[](const std::string& param, Rml::Event& event) {
recomp::start_game(supported_games[0].game_id);
recompui::set_current_menu(recompui::Menu::None);
}
);
}
void load_document() override {
launcher_context = recompui::create_context(zelda64::get_asset_path("launcher.rml"));
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
[](const std::string& param, Rml::Event& event) {
select_rom();
}
);
recompui::register_event(listener, "rom_selected",
[](const std::string& param, Rml::Event& event) {
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
}
);
recompui::register_event(listener, "start_game",
[](const std::string& param, Rml::Event& event) {
recomp::start_game(supported_games[0].game_id);
recompui::hide_all_contexts();
}
);
recompui::register_event(listener, "open_controls",
[](const std::string& param, Rml::Event& event) {
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::Controls);
}
);
[](const std::string& param, Rml::Event& event) {
recompui::set_config_tab(recompui::ConfigTab::Controls);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
);
recompui::register_event(listener, "open_settings",
[](const std::string& param, Rml::Event& event) {
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::General);
}
);
[](const std::string& param, Rml::Event& event) {
recompui::set_config_tab(recompui::ConfigTab::General);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
);
recompui::register_event(listener, "open_mods",
[](const std::string &param, Rml::Event &event) {
recompui::set_config_tab(recompui::ConfigTab::Mods);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
);
recompui::register_event(listener, "exit_game",
[](const std::string& param, Rml::Event& event) {
ultramodern::quit();
}
);
}
void make_bindings(Rml::Context* context) override {
Rml::DataModelConstructor constructor = context->CreateDataModel("launcher_model");
[](const std::string& param, Rml::Event& event) {
ultramodern::quit();
}
);
}
void make_bindings(Rml::Context* context) override {
Rml::DataModelConstructor constructor = context->CreateDataModel("launcher_model");
constructor.Bind("mm_rom_valid", &mm_rom_valid);
constructor.Bind("mm_rom_valid", &mm_rom_valid);
version_string = recomp::get_project_version().to_string();
constructor.Bind("version_number", &version_string);
version_string = recomp::get_project_version().to_string();
constructor.Bind("version_number", &version_string);
model_handle = constructor.GetModelHandle();
}
model_handle = constructor.GetModelHandle();
}
};
std::unique_ptr<recompui::MenuController> recompui::create_launcher_menu() {
+153
View File
@@ -0,0 +1,153 @@
#include "ui_mod_details_panel.h"
#include "librecomp/mods.hpp"
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_background_color(Color{ 190, 184, 219, 25 });
ContextId context = get_current_context();
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
header_container->set_flex(0.0f, 0.0f);
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);
{
thumbnail_image = context.create_element<Image>(thumbnail_container, "");
thumbnail_image->set_width(100.0f);
thumbnail_image->set_height(100.0f);
thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 });
}
header_details_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
header_details_container->set_flex(1.0f, 1.0f);
header_details_container->set_flex_basis(100.0f, Unit::Percent);
header_details_container->set_text_align(TextAlign::Left);
{
title_label = context.create_element<Label>(header_details_container, LabelStyle::Large);
version_label = context.create_element<Label>(header_details_container, LabelStyle::Normal);
}
}
body_container = context.create_element<ScrollContainer>(this, ScrollDirection::Vertical);
body_container->set_text_align(TextAlign::Left);
body_container->set_padding(16.0f);
{
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);
}
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([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([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;
thumbnail_image->set_src(thumbnail);
title_label->set_text(cur_details.display_name);
version_label->set_text(cur_details.version.to_string());
std::string authors_str = "Authors:";
bool first = true;
for (const std::string& author : details.authors) {
authors_str += (first ? " " : ", ") + author;
first = false;
}
authors_label->set_text(authors_str);
description_label->set_text(cur_details.description);
enable_toggle->set_checked(toggle_checked);
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) {
mod_toggled_callback = callback;
}
void ModDetailsPanel::set_mod_configure_pressed_callback(std::function<void()> callback) {
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);
}
}
void ModDetailsPanel::configure_button_pressed() {
if (mod_configure_pressed_callback != nullptr) {
mod_configure_pressed_callback();
}
}
} // namespace recompui
+52
View File
@@ -0,0 +1,52 @@
#ifndef RECOMPUI_ELEMENT_MOD_DETAILS_PANEL_H
#define RECOMPUI_ELEMENT_MOD_DETAILS_PANEL_H
#include "librecomp/mods.hpp"
#include "elements/ui_button.h"
#include "elements/ui_container.h"
#include "elements/ui_image.h"
#include "elements/ui_label.h"
#include "elements/ui_toggle.h"
#include "elements/ui_scroll_container.h"
namespace recompui {
class ModDetailsPanel : public Element {
public:
ModDetailsPanel(Element *parent);
virtual ~ModDetailsPanel();
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;
Image *thumbnail_image = nullptr;
Container *header_container = nullptr;
Container *header_details_container = nullptr;
Label *title_label = nullptr;
Label *version_label = nullptr;
ScrollContainer *body_container = nullptr;
Label *description_label = nullptr;
Label *authors_label = nullptr;
Container *buttons_container = nullptr;
Container *enable_container = nullptr;
Toggle *enable_toggle = nullptr;
Label *enable_label = nullptr;
Button *configure_button = nullptr;
std::function<void(bool)> mod_toggled_callback = nullptr;
std::function<void()> mod_configure_pressed_callback = nullptr;
void enable_toggle_checked(bool checked);
void configure_button_pressed();
};
} // namespace recompui
#endif
+331
View File
@@ -0,0 +1,331 @@
#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";
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 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
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) {
recomp::mods::ModOpenError open_error;
recomp::mods::ZipModFileHandle file_handle(path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back(path.filename().string() + " is not a valid zip or mod.");
continue;
}
// First we verify if the container itself isn't a mod already.
// TODO hook into the runtime's container registration to check the extension instead of using hardcoded values.
if ((path.extension() == ".rtz") || (path.extension() == ".nrm")) {
start_single_mod_installation(path, file_handle, progress_callback, result);
}
else {
// Scan the container for compatible mods instead. This is the case for packages made by users or how they're tipically uploaded to Thunderstore.
start_package_mod_installation(path, file_handle, progress_callback, result);
}
}
}
void ModInstaller::cancel_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
// Delete all the files that were extracted for all mods.
for (const Installation &installation : result.pending_installations) {
std::filesystem::path new_path(installation.mod_file.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
for (const std::filesystem::path &path : installation.additional_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
}
}
void ModInstaller::finish_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
for (const Installation &installation : result.pending_installations) {
// Overwrite the mod files.
remove_and_rename(error_messages, installation.mod_file);
for (const std::filesystem::path &path : installation.additional_files) {
remove_and_rename(error_messages, path);
}
}
}
};
+42
View File
@@ -0,0 +1,42 @@
#ifndef RECOMPUI_MOD_INSTALLER_H
#define RECOMPUI_MOD_INSTALLER_H
#include <librecomp/game.hpp>
#include <unordered_set>
#include <vector>
#include <string>
#include <list>
namespace recompui {
struct ModInstaller {
struct Installation {
std::string mod_id;
std::string display_name;
recomp::Version mod_version;
std::filesystem::path mod_file;
std::list<std::filesystem::path> additional_files;
bool needs_overwrite_confirmation = false;
};
struct Confirmation {
std::string old_display_name;
std::string new_display_name;
std::string old_mod_id;
std::string new_mod_id;
recomp::Version old_version;
recomp::Version new_version;
};
struct Result {
std::list<std::string> error_messages;
std::list<Installation> pending_installations;
};
static void start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result);
static void cancel_mod_installation(const Result& result, std::vector<std::string>& errors);
static void finish_mod_installation(const Result &result, std::vector<std::string>& errors);
};
};
#endif
+796
View File
@@ -0,0 +1,796 @@
#include "ui_mod_menu.h"
#include "ui_utils.h"
#include "recomp_ui.h"
#include "zelda_support.h"
#include "zelda_render.h"
#include "librecomp/mods.hpp"
#include <string>
#ifdef WIN32
#include <shellapi.h>
#endif
// TODO:
// - Set up navigation.
// - Add hover and active state for mod entries.
namespace recompui {
static std::string generate_thumbnail_src_for_mod(const std::string &mod_id) {
return "?/mods/" + mod_id + "/thumb";
}
static bool is_mod_enabled_or_auto(const std::string &mod_id) {
return recomp::mods::is_mod_enabled(mod_id) || recomp::mods::is_mod_auto_enabled(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;
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(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{ 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(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<Element>(this);
body_container->set_width_auto();
body_container->set_margin_left(16.0f);
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() {
}
void ModEntryView::set_mod_details(const recomp::mods::ModDetails &details) {
name_label->set_text(details.display_name);
description_label->set_text(details.short_description);
}
void ModEntryView::set_mod_thumbnail(const std::string &thumbnail) {
thumbnail_image->set_src(thumbnail);
}
void ModEntryView::set_mod_enabled(bool enabled) {
set_opacity(enabled ? 1.0f : 0.5f);
}
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);
}
ModEntryButton::~ModEntryButton() {
}
void ModEntryButton::set_mod_selected_callback(std::function<void(uint32_t)> callback) {
selected_callback = callback;
}
void ModEntryButton::set_mod_drag_callback(std::function<void(uint32_t, EventDrag)> callback) {
drag_callback = callback;
}
void ModEntryButton::set_mod_details(const recomp::mods::ModDetails &details) {
view->set_mod_details(details);
}
void ModEntryButton::set_mod_thumbnail(const std::string &thumbnail) {
view->set_mod_thumbnail(thumbnail);
}
void ModEntryButton::set_mod_enabled(bool enabled) {
view->set_mod_enabled(enabled);
}
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::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::Drag:
drag_callback(mod_index, std::get<EventDrag>(e.variant));
break;
default:
break;
}
}
// ModEntrySpacer
void ModEntrySpacer::check_height_distance() {
constexpr float tolerance = 0.01f;
if (abs(target_height - height) < tolerance) {
height = target_height;
set_height(height, Unit::Dp);
}
else {
queue_update();
}
}
void ModEntrySpacer::process_event(const Event &e) {
switch (e.type) {
case EventType::Update: {
std::chrono::high_resolution_clock::duration now = ultramodern::time_since_start();
float delta_time = std::max(std::chrono::duration<float>(now - last_time).count(), 0.0f);
constexpr float dp_speed = 1000.0f;
last_time = now;
if (target_height < height) {
height += std::max(-dp_speed * delta_time, target_height - height);
}
else {
height += std::min(dp_speed * delta_time, target_height - height);
}
set_height(height, Unit::Dp);
check_height_distance();
break;
}
default:
break;
}
}
ModEntrySpacer::ModEntrySpacer(Element *parent) : Element(parent) {
// Do nothing.
}
void ModEntrySpacer::set_target_height(float target_height, bool animate_to_target) {
this->target_height = target_height;
if (animate_to_target) {
last_time = ultramodern::time_since_start();
check_height_distance();
}
else {
height = target_height;
set_height(target_height, Unit::Dp);
}
}
// ModMenu
void ModMenu::refresh_mods(bool scan_mods) {
for (const std::string &thumbnail : loaded_thumbnails) {
recompui::release_image(thumbnail);
}
if (scan_mods) {
recomp::mods::scan_mods();
}
mod_details = recomp::mods::get_all_mod_details(game_mod_id);
create_mod_list();
}
void ModMenu::open_mods_folder() {
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
#if defined(WIN32)
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::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() {
zelda64::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);
// Refresh enabled status for all mods in case one of them got auto-enabled due to being a dependency.
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
mod_entry_buttons[i]->set_mod_enabled(is_mod_enabled_or_auto(mod_details[i].mod_id));
}
}
}
void ModMenu::mod_selected(uint32_t mod_index) {
if (active_mod_index >= 0) {
mod_entry_buttons[active_mod_index]->set_selected(false);
}
active_mod_index = mod_index;
if (active_mod_index >= 0) {
std::string thumbnail_src = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id);
const recomp::mods::ConfigSchema &config_schema = recomp::mods::get_mod_config_schema(mod_details[active_mod_index].mod_id);
bool toggle_checked = is_mod_enabled_or_auto(mod_details[mod_index].mod_id);
bool auto_enabled = recomp::mods::is_mod_auto_enabled(mod_details[mod_index].mod_id);
bool toggle_enabled = !auto_enabled && (mod_details[mod_index].runtime_toggleable || !ultramodern::is_game_started());
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 = modEntryHeight + modEntryPadding * 2.0f;
switch (drag.phase) {
case DragPhase::Start: {
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
mod_entry_middles[i] = mod_entry_buttons[i]->get_absolute_top() + mod_entry_buttons[i]->get_client_height() / 2.0f;
}
// When the drag phase starts, we make the floating mod details visible and store the relative coordinate of the
// mouse cursor. Instantly hide the real element and use a spacer in its place that will stay on the same size as
// long as the cursor is hovering over this slot.
float width = mod_entry_buttons[mod_index]->get_client_width();
float height = mod_entry_buttons[mod_index]->get_client_height();
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));
mod_entry_floating_view->set_mod_enabled(is_mod_enabled_or_auto(mod_details[mod_index].mod_id));
mod_entry_floating_view->set_left(left, Unit::Px);
mod_entry_floating_view->set_top(top, Unit::Px);
mod_entry_floating_view->set_width(width, Unit::Px);
mod_entry_floating_view->set_height(height, Unit::Px);
mod_drag_start_coordinates[0] = drag.x;
mod_drag_start_coordinates[1] = drag.y;
mod_drag_view_coordinates[0] = left;
mod_drag_view_coordinates[1] = top;
mod_drag_target_index = mod_index;
mod_entry_spacers[mod_drag_target_index]->set_target_height(spacer_height, false);
break;
}
case DragPhase::Move: {
// Binary search for the drag area.
uint32_t low = 0;
uint32_t high = mod_entry_buttons.size();
while (low < high) {
uint32_t mid = low + (high - low) / 2;
if (drag.y < mod_entry_middles[mid]) {
high = mid;
}
else {
low = mid + 1;
}
}
uint32_t new_index = low;
float delta_x = drag.x - mod_drag_start_coordinates[0];
float delta_y = drag.y - mod_drag_start_coordinates[1];
mod_entry_floating_view->set_left(mod_drag_view_coordinates[0] + delta_x, Unit::Px);
mod_entry_floating_view->set_top(mod_drag_view_coordinates[1] + delta_y, Unit::Px);
if (mod_drag_target_index != new_index) {
mod_entry_spacers[mod_drag_target_index]->set_target_height(0.0f, true);
mod_entry_spacers[new_index]->set_target_height(spacer_height, true);
mod_drag_target_index = new_index;
}
break;
}
case DragPhase::End: {
// Dragging has ended, hide the floating view.
mod_entry_buttons[mod_index]->set_display(Display::Block);
mod_entry_buttons[mod_index]->set_selected(false);
mod_entry_spacers[mod_drag_target_index]->set_target_height(0.0f, false);
mod_entry_floating_view->set_display(Display::None);
// Result needs a small substraction when dragging downwards.
if (mod_drag_target_index > mod_index) {
mod_drag_target_index--;
}
// 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_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));
mod_entry_buttons[i]->set_mod_enabled(is_mod_enabled_or_auto(mod_details[i].mod_id));
}
mod_entry_buttons[mod_drag_target_index]->set_selected(true);
active_mod_index = mod_drag_target_index;
break;
}
default:
break;
}
}
// TODO remove this once this is migrated to the new system.
ContextId sub_menu_context;
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 (zelda64::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.
ContextId prev_context = recompui::get_current_context();
prev_context.close();
// Open the sub menu context and set up the element.
sub_menu_context.open();
config_sub_menu->clear_options();
const recomp::mods::ConfigSchema &config_schema = recomp::mods::get_mod_config_schema(mod_details[active_mod_index].mod_id);
for (const recomp::mods::ConfigOption &option : config_schema.options) {
recomp::mods::ConfigValueVariant config_value = recomp::mods::get_mod_config_value(mod_details[active_mod_index].mod_id, option.id);
if (std::holds_alternative<std::monostate>(config_value)) {
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,
[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,
[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),
[this](const std::string &id, const std::string &value){ mod_string_option_changed(id, value); });
break;
}
default:
assert(false && "Unknown config option type.");
break;
}
}
config_sub_menu->enter(mod_details[active_mod_index].display_name);
sub_menu_context.close();
// Reopen the context that was open when this function was called.
prev_context.open();
// Hide the config menu and show the sub menu.
recompui::hide_context(recompui::get_config_context_id());
recompui::show_context(sub_menu_context, "");
}
}
void ModMenu::mod_enum_option_changed(const std::string &id, uint32_t value) {
if (active_mod_index >= 0) {
recomp::mods::set_mod_config_value(mod_details[active_mod_index].mod_id, id, value);
}
}
void ModMenu::mod_string_option_changed(const std::string &id, const std::string &value) {
if (active_mod_index >= 0) {
recomp::mods::set_mod_config_value(mod_details[active_mod_index].mod_id, id, value);
}
}
void ModMenu::mod_number_option_changed(const std::string &id, double value) {
if (active_mod_index >= 0) {
recomp::mods::set_mod_config_value(mod_details[active_mod_index].mod_id, id, value);
}
}
void ModMenu::mod_hd_textures_enabled_changed(uint32_t value) {
if (active_mod_index >= 0) {
if (value) {
zelda64::renderer::secondary_enable_texture_pack(mod_details[active_mod_index].mod_id);
}
else {
zelda64::renderer::secondary_disable_texture_pack(mod_details[active_mod_index].mod_id);
}
}
}
void ModMenu::create_mod_list() {
ContextId context = get_current_context();
// Clear the contents of the list scroll.
list_scroll_container->clear_children();
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_file(thumbnail_name, thumbnail);
loaded_thumbnails.emplace(thumbnail_name);
}
ModEntrySpacer *spacer = context.create_element<ModEntrySpacer>(list_scroll_container);
mod_entry_spacers.emplace_back(spacer);
ModEntryButton *mod_entry = context.create_element<ModEntryButton>(list_scroll_container, mod_index);
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);
}
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);
mod_entry_middles.resize(mod_entry_buttons.size());
bool mods_available = !mod_details.empty();
body_container->set_display(mods_available ? Display::Flex : Display::None);
body_empty_container->set_display(mods_available ? Display::None : Display::Flex);
if (mods_available) {
mod_selected(0);
}
}
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 = "mm";
ContextId context = get_current_context();
set_display(Display::Flex);
set_flex(1.0f, 1.0f, 100.0f);
set_flex_direction(FlexDirection::Column);
set_align_items(AlignItems::Center);
set_justify_content(JustifyContent::FlexStart);
set_width(100.0f, Unit::Percent);
set_height(100.0f, Unit::Percent);
{
body_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
body_container->set_flex(1.0f, 1.0f, 100.0f);
body_container->set_width(100.0f, Unit::Percent);
body_container->set_height(100.0f, Unit::Percent);
{
list_container = context.create_element<Container>(body_container, FlexDirection::Column, JustifyContent::Center);
list_container->set_display(Display::Block);
list_container->set_flex_basis(100.0f);
list_container->set_align_items(AlignItems::Center);
list_container->set_height(100.0f, Unit::Percent);
list_container->set_background_color(Color{ 0, 0, 0, 89 });
list_container->set_border_bottom_left_radius(16.0f);
{
list_scroll_container = context.create_element<ScrollContainer>(list_container, ScrollDirection::Vertical);
} // list_container
mod_details_panel = context.create_element<ModDetailsPanel>(body_container);
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);
body_empty_container->set_flex(1.0f, 1.0f, 100.0f);
body_empty_container->set_display(Display::None);
{
context.create_element<Element>(body_empty_container);
context.create_element<Label>(body_empty_container, "You have no mods. Go get some!", LabelStyle::Large);
context.create_element<Element>(body_empty_container);
} // body_empty_container
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);
{
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(); });
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([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
mod_entry_floating_view = context.create_element<ModEntryView>(this);
mod_entry_floating_view->set_display(Display::None);
mod_entry_floating_view->set_position(Position::Absolute);
mod_entry_floating_view->set_selected(true);
context.close();
sub_menu_context = recompui::create_context(zelda64::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");
ElementConfigSubMenu* config_sub_menu_element = rmlui_dynamic_cast<ElementConfigSubMenu*>(config_sub_menu_generic);
config_sub_menu = config_sub_menu_element->get_config_sub_menu_element();
sub_menu_context.close();
context.open();
}
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%");
recompui::Element this_compat(this);
recompui::ContextId context = get_current_context();
mod_menu = context.create_element<ModMenu>(&this_compat);
}
ElementModMenu::~ElementModMenu() {
}
} // namespace recompui
+130
View File
@@ -0,0 +1,130 @@
#ifndef RECOMPUI_ELEMENT_MOD_MENU_H
#define RECOMPUI_ELEMENT_MOD_MENU_H
#include "librecomp/mods.hpp"
#include "elements/ui_scroll_container.h"
#include "ui_config_sub_menu.h"
#include "ui_mod_details_panel.h"
namespace recompui {
class ModMenu;
class ModEntryView : public Element {
public:
ModEntryView(Element *parent);
virtual ~ModEntryView();
void set_mod_details(const recomp::mods::ModDetails &details);
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;
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 {
public:
ModEntryButton(Element *parent, uint32_t mod_index);
virtual ~ModEntryButton();
void set_mod_selected_callback(std::function<void(uint32_t)> callback);
void set_mod_drag_callback(std::function<void(uint32_t, EventDrag)> callback);
void set_mod_details(const recomp::mods::ModDetails &details);
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;
std::function<void(uint32_t)> selected_callback = nullptr;
std::function<void(uint32_t, EventDrag)> drag_callback = nullptr;
};
class ModEntrySpacer : public Element {
private:
float height = 0.0f;
float target_height = 0.0f;
std::chrono::high_resolution_clock::duration last_time;
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);
};
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(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;
ScrollContainer *list_scroll_container = nullptr;
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;
std::vector<ModEntryButton *> mod_entry_buttons;
std::vector<ModEntrySpacer *> mod_entry_spacers;
std::vector<float> mod_entry_middles;
ModEntryView *mod_entry_floating_view = nullptr;
float mod_drag_start_coordinates[2] = {};
float mod_drag_view_coordinates[2] = {};
uint32_t mod_drag_target_index = 0;
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;
};
class ElementModMenu : public Rml::Element {
public:
ElementModMenu(const Rml::String& tag);
virtual ~ElementModMenu();
};
} // namespace recompui
#endif
+360
View File
@@ -0,0 +1,360 @@
#include <mutex>
#include "recomp_ui.h"
#include "elements/ui_element.h"
#include "elements/ui_label.h"
#include "elements/ui_button.h"
struct {
recompui::ContextId ui_context;
recompui::Label* prompt_header;
recompui::Label* prompt_label;
recompui::Element* prompt_controls;
recompui::Button* confirm_button;
recompui::Button* cancel_button;
std::function<void()> confirm_action;
std::function<void()> cancel_action;
std::string return_element_id;
std::mutex mutex;
} prompt_state;
void run_confirm_callback() {
std::function<void()> confirm_action;
{
std::lock_guard lock{ prompt_state.mutex };
confirm_action = std::move(prompt_state.confirm_action);
}
if (confirm_action) {
confirm_action();
}
recompui::hide_context(prompt_state.ui_context);
// TODO nav: focus on return_element_id
// or just remove it as the usage of the prompt can change now
}
void run_cancel_callback() {
std::function<void()> cancel_action;
{
std::lock_guard lock{ prompt_state.mutex };
cancel_action = std::move(prompt_state.cancel_action);
}
if (cancel_action) {
cancel_action();
}
recompui::hide_context(prompt_state.ui_context);
// TODO nav: focus on return_element_id
// or just remove it as the usage of the prompt can change now
}
void recompui::init_prompt_context() {
ContextId context = create_context();
std::lock_guard lock{ prompt_state.mutex };
context.open();
prompt_state.ui_context = context;
Element* window = context.create_element<Element>(context.get_root_element());
window->set_display(Display::Flex);
window->set_flex_direction(FlexDirection::Column);
window->set_background_color({0, 0, 0, 0});
Element* prompt_overlay = context.create_element<Element>(window);
prompt_overlay->set_background_color(Color{ 190, 184, 219, 25 });
prompt_overlay->set_position(Position::Absolute);
prompt_overlay->set_top(0);
prompt_overlay->set_right(0);
prompt_overlay->set_bottom(0);
prompt_overlay->set_left(0);
Element* prompt_content_wrapper = context.create_element<Element>(window);
prompt_content_wrapper->set_display(Display::Flex);
prompt_content_wrapper->set_position(Position::Absolute);
prompt_content_wrapper->set_top(0);
prompt_content_wrapper->set_right(0);
prompt_content_wrapper->set_bottom(0);
prompt_content_wrapper->set_left(0);
prompt_content_wrapper->set_align_items(AlignItems::Center);
prompt_content_wrapper->set_justify_content(JustifyContent::Center);
Element* prompt_content = context.create_element<Element>(prompt_content_wrapper);
prompt_content->set_display(Display::Flex);
prompt_content->set_position(Position::Relative);
prompt_content->set_flex(1.0f, 1.0f);
prompt_content->set_flex_basis(100, Unit::Percent);
prompt_content->set_flex_direction(FlexDirection::Column);
prompt_content->set_width(100, Unit::Percent);
prompt_content->set_max_width(700, Unit::Dp);
prompt_content->set_height_auto();
prompt_content->set_margin_auto();
prompt_content->set_border_width(1.1, Unit::Dp);
prompt_content->set_border_radius(16, Unit::Dp);
prompt_content->set_border_color(Color{ 255, 255, 255, 51 });
prompt_content->set_background_color(Color{ 8, 7, 13, 229 });
prompt_state.prompt_header = context.create_element<Label>(prompt_content, "", LabelStyle::Large);
prompt_state.prompt_header->set_margin(24, Unit::Dp);
prompt_state.prompt_label = context.create_element<Label>(prompt_content, "", LabelStyle::Small);
prompt_state.prompt_label->set_margin(24, Unit::Dp);
prompt_state.prompt_label->set_margin_top(0);
prompt_state.prompt_controls = context.create_element<Element>(prompt_content);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.prompt_controls->set_flex_direction(FlexDirection::Row);
prompt_state.prompt_controls->set_justify_content(JustifyContent::Center);
prompt_state.prompt_controls->set_padding_top(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_bottom(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_left(12, Unit::Dp);
prompt_state.prompt_controls->set_padding_right(12, Unit::Dp);
prompt_state.prompt_controls->set_border_top_width(1.1, Unit::Dp);
prompt_state.prompt_controls->set_border_top_color({ 255, 255, 255, 25 });
prompt_state.confirm_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.confirm_button->set_min_width(185.0f, Unit::Dp);
prompt_state.confirm_button->set_margin_top(0);
prompt_state.confirm_button->set_margin_bottom(0);
prompt_state.confirm_button->set_margin_left(12, Unit::Dp);
prompt_state.confirm_button->set_margin_right(12, Unit::Dp);
prompt_state.confirm_button->set_text_align(TextAlign::Center);
prompt_state.confirm_button->set_color(Color{ 204, 204, 204, 255 });
prompt_state.confirm_button->add_pressed_callback(run_confirm_callback);
Style* confirm_hover_style = prompt_state.confirm_button->get_hover_style();
confirm_hover_style->set_border_color(Color{ 69, 208, 67, 255 });
confirm_hover_style->set_background_color(Color{ 69, 208, 67, 76 });
confirm_hover_style->set_color(Color{ 242, 242, 242, 255 });
Style* confirm_focus_style = prompt_state.confirm_button->get_focus_style();
confirm_focus_style->set_border_color(Color{ 69, 208, 67, 255 });
confirm_focus_style->set_background_color(Color{ 69, 208, 67, 76 });
confirm_focus_style->set_color(Color{ 242, 242, 242, 255 });
prompt_state.cancel_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.cancel_button->set_min_width(185.0f, Unit::Dp);
prompt_state.cancel_button->set_margin_top(0);
prompt_state.cancel_button->set_margin_bottom(0);
prompt_state.cancel_button->set_margin_left(12, Unit::Dp);
prompt_state.cancel_button->set_margin_right(12, Unit::Dp);
prompt_state.cancel_button->set_text_align(TextAlign::Center);
prompt_state.cancel_button->set_color(Color{ 204, 204, 204, 255 });
prompt_state.cancel_button->add_pressed_callback(run_cancel_callback);
Style* cancel_hover_style = prompt_state.cancel_button->get_hover_style();
cancel_hover_style->set_border_color(Color{ 248, 96, 57, 255 });
cancel_hover_style->set_background_color(Color{ 248, 96, 57, 76 });
cancel_hover_style->set_color(Color{ 242, 242, 242, 255 });
Style* cancel_focus_style = prompt_state.cancel_button->get_focus_style();
cancel_focus_style->set_border_color(Color{ 248, 96, 57, 255 });
cancel_focus_style->set_background_color(Color{ 248, 96, 57, 76 });
cancel_focus_style->set_color(Color{ 242, 242, 242, 255 });
context.close();
}
void style_button(recompui::Button* button, recompui::ButtonVariant variant) {
recompui::Color button_color{};
switch (variant) {
case recompui::ButtonVariant::Primary:
button_color = { 185, 125, 242, 255 };
break;
case recompui::ButtonVariant::Secondary:
button_color = { 23, 214, 232, 255 };
break;
case recompui::ButtonVariant::Tertiary:
button_color = { 242, 242, 242, 255 };
break;
case recompui::ButtonVariant::Success:
button_color = { 69, 208, 67, 255 };
break;
case recompui::ButtonVariant::Error:
button_color = { 248, 96, 57, 255 };
break;
case recompui::ButtonVariant::Warning:
button_color = { 233, 205, 53, 255 };
break;
default:
assert(false);
break;
}
recompui::Color border_color = button_color;
recompui::Color background_color = button_color;
border_color.a = 0.8f * 255;
background_color.a = 0.05f * 255;
button->set_border_color(border_color);
button->set_background_color(background_color);
recompui::Color hover_border_color = button_color;
recompui::Color hover_background_color = button_color;
hover_border_color.a = 255;
hover_background_color.a = 0.3f * 255;
recompui::Style* hover_style = button->get_hover_style();
hover_style->set_border_color(hover_border_color);
hover_style->set_background_color(hover_background_color);
recompui::Style* focus_style = button->get_focus_style();
focus_style->set_border_color(hover_border_color);
focus_style->set_background_color(hover_background_color);
recompui::Color disabled_color { 255, 255, 255, 0.6f * 255 };
recompui::Style* disabled_style = button->get_disabled_style();
disabled_style->set_color(disabled_color);
}
// Must be called while prompt_state.mutex is locked.
void show_prompt(std::function<void()>& prev_cancel_action, bool focus_on_cancel) {
if (focus_on_cancel) {
prompt_state.ui_context.set_autofocus_element(prompt_state.cancel_button);
}
else {
prompt_state.ui_context.set_autofocus_element(prompt_state.confirm_button);
}
if (!recompui::is_context_shown(prompt_state.ui_context)) {
recompui::show_context(prompt_state.ui_context, "");
}
else {
// Call the previous cancel action to effectively close the previous prompt.
if (prev_cancel_action) {
prev_cancel_action();
}
}
}
void recompui::open_choice_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& confirm_label_text,
const std::string& cancel_label_text,
std::function<void()> confirm_action,
std::function<void()> cancel_action,
ButtonVariant confirm_variant,
ButtonVariant cancel_variant,
bool focus_on_cancel,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::Block);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.confirm_button->set_text(confirm_label_text);
prompt_state.cancel_button->set_text(cancel_label_text);
prompt_state.confirm_action = confirm_action;
prompt_state.cancel_action = cancel_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.confirm_button, confirm_variant);
style_button(prompt_state.cancel_button, cancel_variant);
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, focus_on_cancel);
}
void recompui::open_info_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& okay_label_text,
std::function<void()> okay_action,
ButtonVariant okay_variant,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.cancel_button->set_text(okay_label_text);
prompt_state.confirm_action = {};
prompt_state.cancel_action = okay_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.cancel_button, okay_variant);
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, true);
}
void recompui::open_notification(
const std::string& header_text,
const std::string& content_text,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
ContextId prev_context = try_close_current_context();
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::None);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::None);
prompt_state.confirm_action = {};
prompt_state.cancel_action = {};
prompt_state.return_element_id = return_element_id;
prompt_state.ui_context.close();
if (prev_context != ContextId::null()) {
prev_context.open();
}
show_prompt(prev_cancel_action, false);
}
void recompui::close_prompt() {
std::lock_guard lock{ prompt_state.mutex };
if (recompui::is_context_shown(prompt_state.ui_context)) {
if (prompt_state.cancel_action) {
prompt_state.cancel_action();
}
recompui::hide_context(prompt_state.ui_context);
}
}
bool recompui::is_prompt_open() {
std::lock_guard lock{ prompt_state.mutex };
return recompui::is_context_shown(prompt_state.ui_context);
}
+275 -1075
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
#ifndef __UI_RENDERER_H__
#define __UI_RENDERER_H__
#include <memory>
#include "recomp_ui.h"
namespace RT64 {
struct RenderInterface;
struct RenderDevice;
struct RenderCommandList;
struct RenderFramebuffer;
};
namespace Rml {
class RenderInterface;
}
namespace recompui {
class RmlRenderInterface_RT64_impl;
class RmlRenderInterface_RT64 {
private:
std::unique_ptr<RmlRenderInterface_RT64_impl> impl;
public:
RmlRenderInterface_RT64();
~RmlRenderInterface_RT64();
void reset();
void init(RT64::RenderInterface* interface, RT64::RenderDevice* device);
Rml::RenderInterface* get_rml_interface();
void start(RT64::RenderCommandList* list, int image_width, int image_height);
void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer);
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
#endif
+1010
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
#include "ultramodern/ultramodern.hpp"
#include "ui_utils.h"
recompui::Color recompui::lerp_color(const recompui::Color& a, const recompui::Color& b, float factor) {
return recompui::Color{
static_cast<uint8_t>(std::lerp(float(a.r), float(b.r), factor)),
static_cast<uint8_t>(std::lerp(float(a.g), float(b.g), factor)),
static_cast<uint8_t>(std::lerp(float(a.b), float(b.b), factor)),
static_cast<uint8_t>(std::lerp(float(a.a), float(b.a), factor)),
};
}
recompui::Color recompui::get_pulse_color(uint32_t pulse_milliseconds) {
uint64_t millis = std::chrono::duration_cast<std::chrono::milliseconds>(ultramodern::time_since_start()).count();
uint32_t anim_offset = millis % pulse_milliseconds;
float factor = std::abs((2.0f * anim_offset / pulse_milliseconds) - 1.0f);
return lerp_color(Color{ 23, 214, 232, 255 }, Color{ 162, 239, 246, 255 }, factor);
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef __UI_UTILS_H__
#define __UI_UTILS_H__
#include "elements/ui_types.h"
namespace recompui {
Color lerp_color(const Color& a, const Color& b, float factor);
Color get_pulse_color(uint32_t millisecond_period);
}
#endif
+12
View File
@@ -0,0 +1,12 @@
#ifndef RECOMPUI_ELEMENTS_BEM
#define RECOMPUI_ELEMENTS_BEM
#include <string>
#define EL(bem, element) bem "__" element
#define EL_DYN(bem, element) bem "__" + element
#define MOD(bem, modifier) bem "--" modifier
#define MOD_DYN(bem, modifier) bem "--" + modifier
#define BLOCK(bem) bem
#endif
+145
View File
@@ -0,0 +1,145 @@
#include "hsv.h"
#include <algorithm> // std::min, std::max and std::clamp
#include <math.h>
namespace recompui {
void HsvToRgb(HsvColor& hsv, RgbColor& rgb)
{
unsigned char region, remainder, p, q, t;
if (hsv.s == 0)
{
rgb.r = hsv.v;
rgb.g = hsv.v;
rgb.b = hsv.v;
return;
}
region = hsv.h / 43;
remainder = (hsv.h - (region * 43)) * 6;
p = (hsv.v * (255 - hsv.s)) >> 8;
q = (hsv.v * (255 - ((hsv.s * remainder) >> 8))) >> 8;
t = (hsv.v * (255 - ((hsv.s * (255 - remainder)) >> 8))) >> 8;
switch (region)
{
case 0:
rgb.r = hsv.v; rgb.g = t; rgb.b = p;
break;
case 1:
rgb.r = q; rgb.g = hsv.v; rgb.b = p;
break;
case 2:
rgb.r = p; rgb.g = hsv.v; rgb.b = t;
break;
case 3:
rgb.r = p; rgb.g = q; rgb.b = hsv.v;
break;
case 4:
rgb.r = t; rgb.g = p; rgb.b = hsv.v;
break;
default:
rgb.r = hsv.v; rgb.g = p; rgb.b = q;
break;
}
}
void RgbToHsv(RgbColor& rgb, HsvColor& hsv)
{
unsigned char rgbMin, rgbMax;
rgbMin = rgb.r < rgb.g ? (rgb.r < rgb.b ? rgb.r : rgb.b) : (rgb.g < rgb.b ? rgb.g : rgb.b);
rgbMax = rgb.r > rgb.g ? (rgb.r > rgb.b ? rgb.r : rgb.b) : (rgb.g > rgb.b ? rgb.g : rgb.b);
hsv.v = rgbMax;
if (hsv.v == 0)
{
hsv.h = 0;
hsv.s = 0;
return;
}
hsv.s = 255 * long(rgbMax - rgbMin) / hsv.v;
if (hsv.s == 0)
{
hsv.h = 0;
return;
}
if (rgbMax == rgb.r)
hsv.h = 0 + 43 * (rgb.g - rgb.b) / (rgbMax - rgbMin);
else if (rgbMax == rgb.g)
hsv.h = 85 + 43 * (rgb.b - rgb.r) / (rgbMax - rgbMin);
else
hsv.h = 171 + 43 * (rgb.r - rgb.g) / (rgbMax - rgbMin);
}
static unsigned char clamp_255(float f) {
return std::clamp((int)round(f), 0, 255);
}
void HsvFToRgb(HsvColorF in, RgbColor& out)
{
float hh, p, q, t, ff;
long i;
unsigned char val = clamp_255(in.v * 255.0f);
if (in.s <= 0.0) {
out.r = val;
out.g = val;
out.b = val;
return;
}
hh = in.h;
if (hh >= 360.0f) hh = 0.0f;
hh /= 60.0f;
i = (float)hh;
ff = hh - i;
p = in.v * (1.0f - in.s);
q = in.v * (1.0f - (in.s * ff));
t = in.v * (1.0f - (in.s * (1.0f - ff)));
unsigned char up = clamp_255(p * 255.0f);
unsigned char uq = clamp_255(q * 255.0f);
unsigned char ut = clamp_255(t * 255.0f);
switch (i) {
case 0:
out.r = val;
out.g = ut;
out.b = up;
return;
case 1:
out.r = uq;
out.g = val;
out.b = up;
return;
case 2:
out.r = up;
out.g = val;
out.b = ut;
return;
case 3:
out.r = up;
out.g = uq;
out.b = val;
return;
case 4:
out.r = ut;
out.g = up;
out.b = val;
return;
case 5:
default:
out.r = val;
out.g = up;
out.b = uq;
return;
}
}
}
+77
View File
@@ -0,0 +1,77 @@
#ifndef RECOMP_UI_HSV
#define RECOMP_UI_HSV
namespace recompui {
typedef struct RgbColor
{
union {
struct {
unsigned char r;
unsigned char g;
unsigned char b;
};
unsigned char data[3]; // Array access
};
// Operator[] to access members by index
unsigned char& operator[](int index) {
return data[index];
}
// Const version for read-only access
const unsigned char& operator[](int index) const {
return data[index];
}
} RgbColor;
typedef struct HsvColor
{
union {
struct {
unsigned char h;
unsigned char s;
unsigned char v;
};
unsigned char data[3]; // Array access
};
// Operator[] to access members by index
unsigned char& operator[](int index) {
return data[index];
}
// Const version for read-only access
const unsigned char& operator[](int index) const {
return data[index];
}
} HsvColor;
typedef struct HsvColorF
{
union {
struct {
float h; // 0-360
float s; // 0-1
float v; // 0-1
};
float data[3]; // Array access
};
// Operator[] to access members by index
float& operator[](int index) {
return data[index];
}
// Const version for read-only access
const float& operator[](int index) const {
return data[index];
}
} HsvColorF;
void HsvToRgb(HsvColor& hsv, RgbColor& rgb);
void HsvFToRgb(HsvColorF in, RgbColor& out);
void RgbToHsv(RgbColor& rgb, HsvColor& hsv);
}
#endif