mirror of
https://github.com/BanjoRecomp/BanjoRecomp
synced 2026-05-25 23:15:11 -04:00
Update codebase with changes for Zelda 64: Recompiled 1.2
This commit is contained in:
+82
-18
@@ -1,5 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(BanjoRecompiled)
|
||||
|
||||
if (APPLE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version")
|
||||
endif()
|
||||
|
||||
project(BanjoRecompiled LANGUAGES C CXX)
|
||||
set(CMAKE_C_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -16,6 +21,14 @@ if (WIN32)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR")
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
enable_language(OBJC OBJCXX)
|
||||
endif()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
option(RECOMP_FLATPAK "Configure the build for Flatpak compatibility." OFF)
|
||||
endif()
|
||||
|
||||
# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24:
|
||||
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
|
||||
cmake_policy(SET CMP0135 NEW)
|
||||
@@ -32,6 +45,10 @@ set(RT64_STATIC TRUE)
|
||||
set(RT64_SDL_WINDOW_VULKAN TRUE)
|
||||
add_compile_definitions(HLSL_CPU)
|
||||
|
||||
if (RECOMP_FLATPAK)
|
||||
add_compile_definitions(RECOMP_FLATPAK)
|
||||
endif()
|
||||
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/rt64 ${CMAKE_BINARY_DIR}/rt64)
|
||||
|
||||
# set(BUILD_SHARED_LIBS_SAVED "${BUILD_SHARED_LIBS}")
|
||||
@@ -42,6 +59,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/lib/lunasvg)
|
||||
SET(RMLUI_SVG_PLUGIN ON CACHE BOOL "" FORCE)
|
||||
SET(RMLUI_TESTS_ENABLED OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/RmlUi)
|
||||
target_compile_definitions(rmlui_core PRIVATE LUNASVG_BUILD_STATIC)
|
||||
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime)
|
||||
|
||||
@@ -110,7 +128,7 @@ add_custom_target(PatchesBin
|
||||
|
||||
# Generate patches_bin.c from patches.bin
|
||||
add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c
|
||||
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/patches/patches.bin bk_patches_bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
|
||||
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/patches/patches.bin bk_patches_bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.bin
|
||||
)
|
||||
|
||||
@@ -126,22 +144,12 @@ add_custom_command(OUTPUT
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.elf
|
||||
)
|
||||
|
||||
# Download controller db file for controller support via SDL2
|
||||
set(GAMECONTROLLERDB_COMMIT "b1e4090b3d4266e55feb0793efa35792e05faf66")
|
||||
set(GAMECONTROLLERDB_URL "https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/${GAMECONTROLLERDB_COMMIT}/gamecontrollerdb.txt")
|
||||
|
||||
file(DOWNLOAD ${GAMECONTROLLERDB_URL} ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt
|
||||
TLS_VERIFY ON)
|
||||
|
||||
add_custom_target(DownloadGameControllerDB
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt)
|
||||
|
||||
# Main executable
|
||||
add_executable(BanjoRecompiled)
|
||||
add_dependencies(BanjoRecompiled DownloadGameControllerDB)
|
||||
|
||||
set (SOURCES
|
||||
${CMAKE_SOURCE_DIR}/src/main/main.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/main/support.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/main/register_patches.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/main/rt64_render_context.cpp
|
||||
@@ -151,20 +159,26 @@ set (SOURCES
|
||||
${CMAKE_SOURCE_DIR}/src/game/config.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/debug.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/recomp_api.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/recomp_mem_api.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/recomp_actor_api.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/recomp_data_api.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/rom_decompression.cpp
|
||||
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_renderer.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_state.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_launcher.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_config.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_prompt.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_config_sub_menu.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_color_hack.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_rml_hacks.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_elements.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_details_panel.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_installer.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_menu.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_api.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_api_events.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_api_images.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/ui_utils.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/util/hsv.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/core/ui_context.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp
|
||||
@@ -176,6 +190,7 @@ set (SOURCES
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_radio.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_scroll_container.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_slider.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_span.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_style.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_text_input.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_toggle.cpp
|
||||
@@ -185,6 +200,10 @@ set (SOURCES
|
||||
${CMAKE_SOURCE_DIR}/lib/RmlUi/Backends/RmlUi_Platform_SDL.cpp
|
||||
)
|
||||
|
||||
if (APPLE)
|
||||
list(APPEND SOURCES ${CMAKE_SOURCE_DIR}/src/main/support_apple.mm)
|
||||
endif()
|
||||
|
||||
target_include_directories(BanjoRecompiled PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
|
||||
@@ -221,6 +240,12 @@ endif()
|
||||
if (MSVC)
|
||||
# Disable identical code folding, since this breaks mod function patching as multiple functions can get merged into one.
|
||||
target_link_options(BanjoRecompiled PRIVATE /OPT:NOICF)
|
||||
elseif (APPLE)
|
||||
# Use a wrapper around ld64 that respects segprot's `max_prot` value in order
|
||||
# to make our executable memory writable (required for mod function patching)
|
||||
target_link_options(BanjoRecompiled PRIVATE
|
||||
"-fuse-ld=${CMAKE_SOURCE_DIR}/.github/macos/ld64"
|
||||
)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
@@ -263,6 +288,20 @@ if (WIN32)
|
||||
)
|
||||
|
||||
# target_sources(BanjoRecompiled PRIVATE ${CMAKE_SOURCE_DIR}/icons/app.rc)
|
||||
target_link_libraries(BanjoRecompiled PRIVATE SDL2)
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
find_package(SDL2 REQUIRED)
|
||||
target_include_directories(BanjoRecompiled PRIVATE ${SDL2_INCLUDE_DIRS})
|
||||
|
||||
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
|
||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
target_link_libraries(BanjoRecompiled PRIVATE ${CMAKE_DL_LIBS} Threads::Threads SDL2::SDL2)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/.github/macos/apple_bundle.cmake)
|
||||
endif()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
@@ -273,7 +312,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
|
||||
# Generate icon_bytes.c from the app icon PNG.
|
||||
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
|
||||
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
|
||||
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/icons/512.png
|
||||
)
|
||||
target_sources(BanjoRecompiled PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c)
|
||||
@@ -298,7 +337,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
message(STATUS "FREETYPE_LIBRARIES = ${FREETYPE_LIBRARIES}")
|
||||
|
||||
include_directories(${FREETYPE_LIBRARIES})
|
||||
target_link_libraries(BanjoRecompiled PRIVATE ${FREETYPE_LIBRARIES})
|
||||
target_link_libraries(BanjoRecompiled PRIVATE ${FREETYPE_LIBRARIES} SDL2::SDL2)
|
||||
|
||||
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
|
||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||
@@ -310,7 +349,6 @@ endif()
|
||||
target_link_libraries(BanjoRecompiled PRIVATE
|
||||
PatchesLib
|
||||
RecompiledFuncs
|
||||
SDL2
|
||||
librecomp
|
||||
ultramodern
|
||||
rt64
|
||||
@@ -338,9 +376,14 @@ else()
|
||||
if (APPLE)
|
||||
# Apple's binary is universal, so it'll work on both x86_64 and arm64
|
||||
set (DXC "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-macos")
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
|
||||
set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
|
||||
else()
|
||||
set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
|
||||
endif()
|
||||
else()
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
|
||||
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc")
|
||||
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc-linux")
|
||||
else()
|
||||
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-linux")
|
||||
endif()
|
||||
@@ -350,6 +393,27 @@ endif()
|
||||
build_vertex_shader(BanjoRecompiled "shaders/InterfaceVS.hlsl" "shaders/InterfaceVS.hlsl")
|
||||
build_pixel_shader(BanjoRecompiled "shaders/InterfacePS.hlsl" "shaders/InterfacePS.hlsl")
|
||||
|
||||
# Embed all .nrm files in the "mods" directory
|
||||
file(GLOB NRM_FILES "${CMAKE_SOURCE_DIR}/mods/*.nrm")
|
||||
|
||||
set(GENERATED_NRM_SOURCES "")
|
||||
|
||||
foreach(NRM_FILE ${NRM_FILES})
|
||||
get_filename_component(NRM_NAME ${NRM_FILE} NAME_WE)
|
||||
set(OUT_C "${CMAKE_CURRENT_BINARY_DIR}/mods/${NRM_NAME}.c")
|
||||
set(OUT_H "${CMAKE_CURRENT_BINARY_DIR}/mods/${NRM_NAME}.h")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${OUT_C} ${OUT_H}
|
||||
COMMAND file_to_c ${NRM_FILE} ${NRM_NAME} ${OUT_C} ${OUT_H}
|
||||
DEPENDS ${NRM_FILE}
|
||||
)
|
||||
|
||||
list(APPEND GENERATED_NRM_SOURCES ${OUT_C})
|
||||
endforeach()
|
||||
|
||||
target_sources(BanjoRecompiled PRIVATE ${GENERATED_NRM_SOURCES})
|
||||
|
||||
target_sources(BanjoRecompiled PRIVATE ${SOURCES})
|
||||
|
||||
set_property(TARGET BanjoRecompiled PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
+15
-3
@@ -14,6 +14,8 @@ namespace RT64 {
|
||||
|
||||
namespace banjo {
|
||||
namespace renderer {
|
||||
inline const std::string special_option_texture_pack_enabled = "_recomp_texture_pack_enabled";
|
||||
|
||||
class RT64Context final : public ultramodern::renderer::RendererContext {
|
||||
public:
|
||||
~RT64Context() override;
|
||||
@@ -30,9 +32,12 @@ namespace banjo {
|
||||
uint32_t get_display_framerate() const override;
|
||||
float get_resolution_scale() const override;
|
||||
|
||||
protected:
|
||||
private:
|
||||
std::unique_ptr<RT64::Application> app;
|
||||
std::unordered_set<std::filesystem::path> enabled_texture_packs;
|
||||
std::unordered_set<std::string> enabled_texture_packs;
|
||||
std::unordered_set<std::string> secondary_disabled_texture_packs;
|
||||
|
||||
void check_texture_pack_actions();
|
||||
};
|
||||
|
||||
std::unique_ptr<ultramodern::renderer::RendererContext> create_render_context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
|
||||
@@ -41,8 +46,15 @@ namespace banjo {
|
||||
bool RT64SamplePositionsSupported();
|
||||
bool RT64HighPrecisionFBEnabled();
|
||||
|
||||
void enable_texture_pack(const recomp::mods::ModHandle& mod);
|
||||
void trigger_texture_pack_update();
|
||||
void enable_texture_pack(const recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod);
|
||||
void disable_texture_pack(const recomp::mods::ModHandle& mod);
|
||||
void secondary_enable_texture_pack(const std::string& mod_id);
|
||||
void secondary_disable_texture_pack(const std::string& mod_id);
|
||||
|
||||
// Texture pack 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 is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef __BANJO_SUPPORT_H__
|
||||
#define __BANJO_SUPPORT_H__
|
||||
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <list>
|
||||
|
||||
namespace banjo {
|
||||
std::filesystem::path get_program_path();
|
||||
std::filesystem::path get_asset_path(const char* asset);
|
||||
void open_file_dialog(std::function<void(bool success, const std::filesystem::path& path)> callback);
|
||||
void open_file_dialog_multiple(std::function<void(bool success, const std::list<std::filesystem::path>& paths)> callback);
|
||||
void show_error_message_box(const char *title, const char *message);
|
||||
|
||||
// Apple specific methods that usually require Objective-C. Implemented in support_apple.mm.
|
||||
#ifdef __APPLE__
|
||||
void dispatch_on_ui_thread(std::function<void()> func);
|
||||
std::optional<std::filesystem::path> get_application_support_directory();
|
||||
std::filesystem::path get_bundle_resource_directory();
|
||||
std::filesystem::path get_bundle_directory();
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef __OVERLOADED_H__
|
||||
#define __OVERLOADED_H__
|
||||
|
||||
// Helper for std::visit
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts... { using Ts::operator()...; };
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
#endif
|
||||
@@ -1,9 +1,11 @@
|
||||
#ifndef __RECOMP_DATA_H__
|
||||
#define __RECOMP_DATA_H__
|
||||
|
||||
namespace recomp {
|
||||
namespace recomputil {
|
||||
void init_extended_actor_data();
|
||||
void reset_actor_data();
|
||||
|
||||
void register_data_api_exports();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+45
-15
@@ -4,6 +4,7 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <list>
|
||||
|
||||
// TODO move this file into src/ui
|
||||
|
||||
@@ -29,7 +30,7 @@ namespace recompui {
|
||||
class MenuController {
|
||||
public:
|
||||
virtual ~MenuController() {}
|
||||
virtual Rml::ElementDocument* load_document(Rml::Context* context) = 0;
|
||||
virtual void load_document() = 0;
|
||||
virtual void register_events(UiEventListenerInstancer& listener) = 0;
|
||||
virtual void make_bindings(Rml::Context* context) = 0;
|
||||
};
|
||||
@@ -49,13 +50,14 @@ namespace recompui {
|
||||
void hide_context(ContextId context);
|
||||
void hide_all_contexts();
|
||||
bool is_context_shown(ContextId context);
|
||||
bool is_context_taking_input();
|
||||
bool is_context_capturing_input();
|
||||
bool is_context_capturing_mouse();
|
||||
bool is_any_context_shown();
|
||||
ContextId try_close_current_context();
|
||||
|
||||
ContextId get_launcher_context_id();
|
||||
ContextId get_config_context_id();
|
||||
ContextId get_config_sub_menu_context_id();
|
||||
ContextId get_close_prompt_context_id();
|
||||
|
||||
enum class ConfigTab {
|
||||
General,
|
||||
@@ -67,6 +69,11 @@ namespace recompui {
|
||||
};
|
||||
|
||||
void set_config_tab(ConfigTab tab);
|
||||
int config_tab_to_index(ConfigTab tab);
|
||||
Rml::ElementTabSet* get_config_tabset();
|
||||
Rml::Element* get_mod_tab();
|
||||
void set_config_tabset_mod_nav();
|
||||
void focus_mod_configure_button();
|
||||
|
||||
enum class ButtonVariant {
|
||||
Primary,
|
||||
@@ -78,19 +85,37 @@ namespace recompui {
|
||||
NumVariants,
|
||||
};
|
||||
|
||||
void open_prompt(
|
||||
const std::string& headerText,
|
||||
const std::string& contentText,
|
||||
const std::string& confirmLabelText,
|
||||
const std::string& cancelLabelText,
|
||||
std::function<void()> confirmCb,
|
||||
std::function<void()> cancelCb,
|
||||
ButtonVariant _confirmVariant = ButtonVariant::Success,
|
||||
ButtonVariant _cancelVariant = ButtonVariant::Error,
|
||||
bool _focusOnCancel = true,
|
||||
const std::string& _returnElementId = ""
|
||||
void init_styling(const std::filesystem::path& rcss_file);
|
||||
void init_prompt_context();
|
||||
void 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::Success,
|
||||
ButtonVariant cancel_variant = ButtonVariant::Error,
|
||||
bool focus_on_cancel = true,
|
||||
const std::string& return_element_id = ""
|
||||
);
|
||||
void 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 = ButtonVariant::Error,
|
||||
const std::string& return_element_id = ""
|
||||
);
|
||||
void open_notification(
|
||||
const std::string& header_text,
|
||||
const std::string& content_text,
|
||||
const std::string& return_element_id = ""
|
||||
);
|
||||
void close_prompt();
|
||||
bool is_prompt_open();
|
||||
void update_mod_list(bool scan_mods = true);
|
||||
void process_game_started();
|
||||
|
||||
void apply_color_hack();
|
||||
void get_window_size(int& width, int& height);
|
||||
@@ -109,8 +134,13 @@ namespace recompui {
|
||||
Rml::ElementPtr create_custom_element(Rml::Element* parent, std::string tag);
|
||||
Rml::ElementDocument* load_document(const std::filesystem::path& path);
|
||||
Rml::ElementDocument* create_empty_document();
|
||||
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes);
|
||||
Rml::Element* get_child_by_tag(Rml::Element* parent, const std::string& tag);
|
||||
|
||||
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height);
|
||||
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes);
|
||||
void release_image(const std::string &src);
|
||||
|
||||
void drop_files(const std::list<std::filesystem::path> &file_list);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+1
-1
Submodule lib/N64ModernRuntime updated: 6ffd6b8856...c5e268aa0f
+1
-1
Submodule lib/lunasvg updated: 610b8bf514...4166d0cccf
+1
-1
Submodule lib/rt64 updated: 1db8c347ca...cf75b17fc2
@@ -0,0 +1,14 @@
|
||||
#ifndef __MEM_FUNCS_H__
|
||||
#define __MEM_FUNCS_H__
|
||||
|
||||
#include "patch_helpers.h"
|
||||
|
||||
DECLARE_FUNC(u32, recomp_register_actor_extension, u32 actor_type, u32 size);
|
||||
DECLARE_FUNC(u32, recomp_register_actor_extension_generic, u32 size);
|
||||
DECLARE_FUNC(void, recomp_clear_all_actor_data);
|
||||
DECLARE_FUNC(u32, recomp_create_actor_data, u32 actor_type);
|
||||
DECLARE_FUNC(void, recomp_destroy_actor_data, u32 actor_handle);
|
||||
DECLARE_FUNC(void*, recomp_get_actor_data, u32 actor_handle, u32 extension_handle, u32 actor_type);
|
||||
DECLARE_FUNC(u32, recomp_get_actor_spawn_index, u32 actor_handle);
|
||||
|
||||
#endif
|
||||
@@ -1,60 +1,5 @@
|
||||
#include "patches.h"
|
||||
|
||||
typedef struct Cube_s Cube;
|
||||
|
||||
RECOMP_PATCH bool viewport_cube_isInFrustum(Cube *cube) {
|
||||
// f32 sp24[3];
|
||||
// f32 sp18[3];
|
||||
|
||||
// sp24[0] = (f32) ((cube->x * 1000) - 150);
|
||||
// sp24[1] = (f32) ((cube->y * 1000) - 150);
|
||||
// sp24[2] = (f32) ((cube->z * 1000) - 150);
|
||||
// sp18[0] = sp24[0] + 1300.0f;
|
||||
// sp18[1] = sp24[1] + 1300.0f;
|
||||
// sp18[2] = sp24[2] + 1300.0f;
|
||||
// return func_8024D374(sp24, sp18);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
RECOMP_PATCH bool viewport_cube_isInFrustum2(Cube *cube) {
|
||||
// f32 sp34[3];
|
||||
// f32 sp28[3];
|
||||
// f32 sp1C[3];
|
||||
|
||||
// if (cube->x == -0x10) {
|
||||
// return TRUE;
|
||||
// }
|
||||
// sp1C[0] = (f32) ((cube->x * 1000) + 500) - viewportPosition[0];
|
||||
// sp1C[1] = (f32) ((cube->y * 1000) + 500) - viewportPosition[1];
|
||||
// sp1C[2] = (f32) ((cube->z * 1000) + 500) - viewportPosition[2];
|
||||
// if (LENGTH_SQ_VEC3F(sp1C) > 1.6e7f) {
|
||||
// return FALSE;
|
||||
// }
|
||||
// sp34[0] = (f32) ((cube->x * 1000) - 150);
|
||||
// sp34[1] = (f32) ((cube->y * 1000) - 150);
|
||||
// sp34[2] = (f32) ((cube->z * 1000) - 150);
|
||||
// sp28[0] = sp34[0] + 1300.0f;
|
||||
// sp28[1] = sp34[1] + 1300.0f;
|
||||
// sp28[2] = sp34[2] + 1300.0f;
|
||||
// return func_8024D374(sp34, sp28);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
RECOMP_PATCH bool viewport_func_8024DB50(f32 arg0[3], f32 arg1) {
|
||||
// f32 sp3C[3];
|
||||
// s32 i;
|
||||
|
||||
// sp3C[0] = arg0[0] - viewportPosition[0];
|
||||
// sp3C[1] = arg0[1] - viewportPosition[1];
|
||||
// sp3C[2] = arg0[2] - viewportPosition[2];
|
||||
// for(i = 0; i < 4; i++){
|
||||
// if(arg1 <= ml_dotProduct_vec3f(sp3C, D_80280ED0[i])){
|
||||
// return FALSE;
|
||||
// }
|
||||
// }
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
RECOMP_PATCH bool viewport_isBoundingBoxInFrustum(f32 arg0[3], f32 arg1[3]) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ extern RecompAimingOverideMode recomp_aiming_override_mode;
|
||||
|
||||
DECLARE_FUNC(void, recomp_get_gyro_deltas, float* x, float* y);
|
||||
DECLARE_FUNC(void, recomp_get_mouse_deltas, float* x, float* y);
|
||||
DECLARE_FUNC(s32, recomp_get_targeting_mode);
|
||||
DECLARE_FUNC(void, recomp_get_inverted_axes, s32* x, s32* y);
|
||||
DECLARE_FUNC(s32, recomp_get_analog_cam_enabled);
|
||||
DECLARE_FUNC(void, recomp_get_analog_inverted_axes, s32* x, s32* y);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef __UI_FUNCS_H__
|
||||
#define __UI_FUNCS_H__
|
||||
|
||||
// These two enums must be kept in sync with src/ui/elements/ui_types.h!
|
||||
typedef enum {
|
||||
UI_EVENT_NONE,
|
||||
UI_EVENT_CLICK,
|
||||
UI_EVENT_FOCUS,
|
||||
UI_EVENT_HOVER,
|
||||
UI_EVENT_ENABLE,
|
||||
UI_EVENT_DRAG,
|
||||
UI_EVENT_RESERVED1, // Would be UI_EVENT_TEXT but text events aren't usable in mods currently
|
||||
UI_EVENT_UPDATE,
|
||||
UI_EVENT_COUNT
|
||||
} RecompuiEventType;
|
||||
|
||||
typedef enum {
|
||||
UI_DRAG_NONE,
|
||||
UI_DRAG_START,
|
||||
UI_DRAG_MOVE,
|
||||
UI_DRAG_END
|
||||
} RecompuiDragPhase;
|
||||
|
||||
typedef struct {
|
||||
RecompuiEventType type;
|
||||
union {
|
||||
struct {
|
||||
float x;
|
||||
float y;
|
||||
} click;
|
||||
|
||||
struct {
|
||||
bool active;
|
||||
} focus;
|
||||
|
||||
struct {
|
||||
bool active;
|
||||
} hover;
|
||||
|
||||
struct {
|
||||
bool active;
|
||||
} enable;
|
||||
|
||||
struct {
|
||||
float x;
|
||||
float y;
|
||||
RecompuiDragPhase phase;
|
||||
} drag;
|
||||
} data;
|
||||
} RecompuiEventData;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef __TRANSFORM_IDS_H__
|
||||
#define __TRANSFORM_IDS_H__
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "patches.h"
|
||||
#include "transform_ids.h"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef __UI_FUNCS_INTERNAL_H__
|
||||
#define __UI_FUNCS_INTERNAL_H__
|
||||
|
||||
#include "patch_helpers.h"
|
||||
#include "recompui_event_structs.h"
|
||||
|
||||
DECLARE_FUNC(void, recomp_run_ui_callbacks);
|
||||
|
||||
#endif
|
||||
+30
-8
@@ -2,6 +2,7 @@
|
||||
#include "recomp_input.h"
|
||||
#include "banjo_sound.h"
|
||||
#include "banjo_render.h"
|
||||
#include "banjo_support.h"
|
||||
#include "ultramodern/config.hpp"
|
||||
#include "librecomp/files.hpp"
|
||||
#include <filesystem>
|
||||
@@ -13,6 +14,8 @@
|
||||
#elif defined(__linux__)
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include "apple/rt64_apple.h"
|
||||
#endif
|
||||
|
||||
constexpr std::u8string_view general_filename = u8"general.json";
|
||||
@@ -71,7 +74,7 @@ T from_or_default(const json& j, const std::string& key, T default_value) {
|
||||
else {
|
||||
ret = default_value;
|
||||
}
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -129,11 +132,19 @@ namespace recomp {
|
||||
}
|
||||
|
||||
std::filesystem::path banjo::get_app_folder_path() {
|
||||
// directly check for portable.txt (windows and native linux binary)
|
||||
// directly check for portable.txt (windows and native linux binary)
|
||||
if (std::filesystem::exists("portable.txt")) {
|
||||
return std::filesystem::current_path();
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Check for portable file in the directory containing the app bundle.
|
||||
const auto app_bundle_path = banjo::get_bundle_directory().parent_path();
|
||||
if (std::filesystem::exists(app_bundle_path / "portable.txt")) {
|
||||
return app_bundle_path;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::filesystem::path recomp_dir{};
|
||||
|
||||
#if defined(_WIN32)
|
||||
@@ -145,16 +156,27 @@ std::filesystem::path banjo::get_app_folder_path() {
|
||||
}
|
||||
|
||||
CoTaskMemFree(known_path);
|
||||
#elif defined(__linux__)
|
||||
// check for APP_FOLDER_PATH env var used by AppImage
|
||||
#elif defined(__linux__) || defined(__APPLE__)
|
||||
// check for APP_FOLDER_PATH env var
|
||||
if (getenv("APP_FOLDER_PATH") != nullptr) {
|
||||
return std::filesystem::path{getenv("APP_FOLDER_PATH")};
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
const auto supportdir = banjo::get_application_support_directory();
|
||||
if (supportdir) {
|
||||
return *supportdir / banjo::program_id;
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *homedir;
|
||||
|
||||
if ((homedir = getenv("HOME")) == nullptr) {
|
||||
#if defined(__linux__)
|
||||
homedir = getpwuid(getuid())->pw_dir;
|
||||
#elif defined(__APPLE__)
|
||||
homedir = GetHomeDirectory();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (homedir != nullptr) {
|
||||
@@ -206,7 +228,7 @@ bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::j
|
||||
return recomp::finalize_output_file_with_backup(path);
|
||||
}
|
||||
|
||||
bool save_general_config(const std::filesystem::path& path) {
|
||||
bool save_general_config(const std::filesystem::path& path) {
|
||||
nlohmann::json config_json{};
|
||||
|
||||
recomp::to_json(config_json["background_input_mode"], recomp::get_background_input_mode());
|
||||
@@ -218,7 +240,7 @@ bool save_general_config(const std::filesystem::path& path) {
|
||||
config_json["analog_cam_mode"] = banjo::get_analog_cam_mode();
|
||||
config_json["analog_camera_invert_mode"] = banjo::get_analog_camera_invert_mode();
|
||||
config_json["debug_mode"] = banjo::get_debug_mode_enabled();
|
||||
|
||||
|
||||
return save_json_with_backups(path, config_json);
|
||||
}
|
||||
|
||||
@@ -433,7 +455,7 @@ bool save_sound_config(const std::filesystem::path& path) {
|
||||
|
||||
config_json["main_volume"] = banjo::get_main_volume();
|
||||
config_json["bgm_volume"] = banjo::get_bgm_volume();
|
||||
|
||||
|
||||
return save_json_with_backups(path, config_json);
|
||||
}
|
||||
|
||||
@@ -494,7 +516,7 @@ void banjo::save_config() {
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(recomp_dir);
|
||||
|
||||
|
||||
// TODO error handling for failing to save config files.
|
||||
|
||||
save_general_config(recomp_dir / general_filename);
|
||||
|
||||
+129
-102
@@ -31,7 +31,7 @@ static struct {
|
||||
std::mutex cur_controllers_mutex;
|
||||
std::vector<SDL_GameController*> cur_controllers{};
|
||||
std::unordered_map<SDL_JoystickID, ControllerState> controller_states;
|
||||
|
||||
|
||||
std::array<float, 2> rotation_delta{};
|
||||
std::array<float, 2> mouse_delta{};
|
||||
std::mutex pending_input_mutex;
|
||||
@@ -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;
|
||||
|
||||
@@ -93,85 +97,82 @@ bool should_override_keystate(SDL_Scancode key, SDL_Keymod mod) {
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
switch (event->type) {
|
||||
case SDL_EventType::SDL_KEYDOWN:
|
||||
{
|
||||
SDL_KeyboardEvent* keyevent = &event->key;
|
||||
{
|
||||
SDL_KeyboardEvent* keyevent = &event->key;
|
||||
|
||||
// Skip repeated events when not in the menu
|
||||
if (!recompui::is_context_taking_input() &&
|
||||
event->key.repeat) {
|
||||
break;
|
||||
}
|
||||
// Skip repeated events when not in the menu
|
||||
if (!recompui::is_context_capturing_input() &&
|
||||
event->key.repeat) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
|
||||
keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
|
||||
if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
|
||||
keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
|
||||
) {
|
||||
recompui::toggle_fullscreen();
|
||||
recompui::toggle_fullscreen();
|
||||
}
|
||||
if (scanning_device != recomp::InputDevice::COUNT) {
|
||||
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
|
||||
recomp::cancel_scanning_input();
|
||||
}
|
||||
if (scanning_device != recomp::InputDevice::COUNT) {
|
||||
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
|
||||
recomp::cancel_scanning_input();
|
||||
} else if (scanning_device == recomp::InputDevice::Keyboard) {
|
||||
set_scanned_input({(uint32_t)InputType::Keyboard, keyevent->keysym.scancode});
|
||||
}
|
||||
} else {
|
||||
if (!should_override_keystate(keyevent->keysym.scancode, static_cast<SDL_Keymod>(keyevent->keysym.mod))) {
|
||||
queue_if_enabled(event);
|
||||
}
|
||||
else if (scanning_device == recomp::InputDevice::Keyboard) {
|
||||
set_scanned_input({ (uint32_t)InputType::Keyboard, keyevent->keysym.scancode });
|
||||
}
|
||||
}
|
||||
break;
|
||||
else {
|
||||
if (!should_override_keystate(keyevent->keysym.scancode, static_cast<SDL_Keymod>(keyevent->keysym.mod))) {
|
||||
queue_if_enabled(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SDL_EventType::SDL_CONTROLLERDEVICEADDED:
|
||||
{
|
||||
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
|
||||
SDL_GameController* controller = SDL_GameControllerOpen(controller_event->which);
|
||||
printf("Controller added: %d\n", controller_event->which);
|
||||
if (controller != nullptr) {
|
||||
printf(" Instance ID: %d\n", SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)));
|
||||
ControllerState& state = InputState.controller_states[SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))];
|
||||
state.controller = controller;
|
||||
{
|
||||
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
|
||||
SDL_GameController* controller = SDL_GameControllerOpen(controller_event->which);
|
||||
printf("Controller added: %d\n", controller_event->which);
|
||||
if (controller != nullptr) {
|
||||
printf(" Instance ID: %d\n", SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)));
|
||||
ControllerState& state = InputState.controller_states[SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))];
|
||||
state.controller = controller;
|
||||
|
||||
if (SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_GYRO) && SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_ACCEL)) {
|
||||
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_GYRO, SDL_TRUE);
|
||||
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_ACCEL, SDL_TRUE);
|
||||
}
|
||||
if (SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_GYRO) && SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_ACCEL)) {
|
||||
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_GYRO, SDL_TRUE);
|
||||
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_ACCEL, SDL_TRUE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SDL_EventType::SDL_CONTROLLERDEVICEREMOVED:
|
||||
{
|
||||
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
|
||||
printf("Controller removed: %d\n", controller_event->which);
|
||||
InputState.controller_states.erase(controller_event->which);
|
||||
}
|
||||
break;
|
||||
{
|
||||
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
|
||||
printf("Controller removed: %d\n", controller_event->which);
|
||||
InputState.controller_states.erase(controller_event->which);
|
||||
}
|
||||
break;
|
||||
case SDL_EventType::SDL_QUIT: {
|
||||
if (!ultramodern::is_game_started()) {
|
||||
ultramodern::quit();
|
||||
return true;
|
||||
}
|
||||
|
||||
recompui::ContextId config_context_id = recompui::get_config_context_id();
|
||||
if (!recompui::is_context_shown(config_context_id)) {
|
||||
recompui::show_context(config_context_id, "");
|
||||
}
|
||||
|
||||
banjo::open_quit_game_prompt();
|
||||
recompui::activate_mouse();
|
||||
break;
|
||||
}
|
||||
case SDL_EventType::SDL_MOUSEWHEEL:
|
||||
{
|
||||
SDL_MouseWheelEvent* wheel_event = &event->wheel;
|
||||
InputState.mouse_wheel_pos.fetch_add(wheel_event->y * (wheel_event->direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1));
|
||||
}
|
||||
queue_if_enabled(event);
|
||||
break;
|
||||
{
|
||||
SDL_MouseWheelEvent* wheel_event = &event->wheel;
|
||||
InputState.mouse_wheel_pos.fetch_add(wheel_event->y * (wheel_event->direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1));
|
||||
}
|
||||
queue_if_enabled(event);
|
||||
break;
|
||||
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
|
||||
if (scanning_device != recomp::InputDevice::COUNT) {
|
||||
auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
|
||||
@@ -180,22 +181,24 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
if ((menuToggleBinding0.input_type != 0 && event->cbutton.button == menuToggleBinding0.input_id) ||
|
||||
(menuToggleBinding1.input_type != 0 && event->cbutton.button == menuToggleBinding1.input_id)) {
|
||||
recomp::cancel_scanning_input();
|
||||
} else if (scanning_device == recomp::InputDevice::Controller) {
|
||||
}
|
||||
else if (scanning_device == recomp::InputDevice::Controller) {
|
||||
SDL_ControllerButtonEvent* button_event = &event->cbutton;
|
||||
auto scanned_input_index = recomp::get_scanned_input_index();
|
||||
if ((scanned_input_index == static_cast<int>(recomp::GameInput::TOGGLE_MENU) ||
|
||||
scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
|
||||
scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) && (
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP ||
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN ||
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT ||
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
|
||||
scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
|
||||
scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) && (
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP ||
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN ||
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT ||
|
||||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
|
||||
break;
|
||||
}
|
||||
|
||||
set_scanned_input({(uint32_t)InputType::ControllerDigital, button_event->button});
|
||||
set_scanned_input({ (uint32_t)InputType::ControllerDigital, button_event->button });
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
queue_if_enabled(event);
|
||||
}
|
||||
break;
|
||||
@@ -217,8 +220,8 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
set_stick_return_event.user.data1 = nullptr;
|
||||
set_stick_return_event.user.data2 = nullptr;
|
||||
recompui::queue_event(set_stick_return_event);
|
||||
|
||||
set_scanned_input({(uint32_t)InputType::ControllerAnalog, axis_event->axis + 1});
|
||||
|
||||
set_scanned_input({ (uint32_t)InputType::ControllerAnalog, axis_event->axis + 1 });
|
||||
}
|
||||
else if (axis_value < -axis_threshold) {
|
||||
SDL_Event set_stick_return_event;
|
||||
@@ -228,9 +231,10 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
set_stick_return_event.user.data2 = nullptr;
|
||||
recompui::queue_event(set_stick_return_event);
|
||||
|
||||
set_scanned_input({(uint32_t)InputType::ControllerAnalog, -axis_event->axis - 1});
|
||||
set_scanned_input({ (uint32_t)InputType::ControllerAnalog, -axis_event->axis - 1 });
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
queue_if_enabled(event);
|
||||
}
|
||||
break;
|
||||
@@ -276,6 +280,22 @@ 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;
|
||||
case SDL_EventType::SDL_CONTROLLERBUTTONUP:
|
||||
// Always queue button up events to avoid missing them during binding.
|
||||
recompui::queue_event(*event);
|
||||
break;
|
||||
default:
|
||||
queue_if_enabled(event);
|
||||
break;
|
||||
@@ -285,6 +305,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);
|
||||
@@ -301,6 +322,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;
|
||||
@@ -465,7 +491,7 @@ void recomp::poll_inputs() {
|
||||
// Read the deltas while resetting them to zero.
|
||||
{
|
||||
std::lock_guard lock{ InputState.pending_input_mutex };
|
||||
|
||||
|
||||
InputState.rotation_delta = InputState.pending_rotation_delta;
|
||||
InputState.pending_rotation_delta = { 0.0f, 0.0f };
|
||||
|
||||
@@ -482,14 +508,14 @@ void recomp::set_rumble(int controller_num, bool on) {
|
||||
|
||||
ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) {
|
||||
switch (controller_num) {
|
||||
case 0:
|
||||
return ultramodern::input::connected_device_info_t {
|
||||
.connected_device = ultramodern::input::Device::Controller,
|
||||
.connected_pak = ultramodern::input::Pak::RumblePak,
|
||||
};
|
||||
case 0:
|
||||
return ultramodern::input::connected_device_info_t{
|
||||
.connected_device = ultramodern::input::Device::Controller,
|
||||
.connected_pak = ultramodern::input::Pak::RumblePak,
|
||||
};
|
||||
}
|
||||
|
||||
return ultramodern::input::connected_device_info_t {
|
||||
return ultramodern::input::connected_device_info_t{
|
||||
.connected_device = ultramodern::input::Device::None,
|
||||
.connected_pak = ultramodern::input::Pak::None,
|
||||
};
|
||||
@@ -506,7 +532,8 @@ void recomp::update_rumble() {
|
||||
if (InputState.rumble_active) {
|
||||
InputState.cur_rumble += 0.17f;
|
||||
if (InputState.cur_rumble > 1) InputState.cur_rumble = 1;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
InputState.cur_rumble *= 0.92f;
|
||||
InputState.cur_rumble -= 0.01f;
|
||||
if (InputState.cur_rumble < 0) InputState.cur_rumble = 0;
|
||||
@@ -647,13 +674,13 @@ void recomp::get_mouse_deltas(float* x, float* y) {
|
||||
void recomp::apply_joystick_deadzone(float x_in, float y_in, float* x_out, float* y_out) {
|
||||
float joystick_deadzone = (float)recomp::get_joystick_deadzone() / 100.0f;
|
||||
|
||||
if(fabsf(x_in) < joystick_deadzone) {
|
||||
if (fabsf(x_in) < joystick_deadzone) {
|
||||
x_in = 0.0f;
|
||||
}
|
||||
else {
|
||||
if(x_in > 0.0f) {
|
||||
if (x_in > 0.0f) {
|
||||
x_in -= joystick_deadzone;
|
||||
}
|
||||
}
|
||||
else {
|
||||
x_in += joystick_deadzone;
|
||||
}
|
||||
@@ -661,13 +688,13 @@ void recomp::apply_joystick_deadzone(float x_in, float y_in, float* x_out, float
|
||||
x_in /= (1.0f - joystick_deadzone);
|
||||
}
|
||||
|
||||
if(fabsf(y_in) < joystick_deadzone) {
|
||||
if (fabsf(y_in) < joystick_deadzone) {
|
||||
y_in = 0.0f;
|
||||
}
|
||||
else {
|
||||
if(y_in > 0.0f) {
|
||||
if (y_in > 0.0f) {
|
||||
y_in -= joystick_deadzone;
|
||||
}
|
||||
}
|
||||
else {
|
||||
y_in += joystick_deadzone;
|
||||
}
|
||||
@@ -695,7 +722,7 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
|
||||
|
||||
bool recomp::game_input_disabled() {
|
||||
// Disable input if any menu that blocks input is open.
|
||||
return recompui::is_context_taking_input();
|
||||
return recompui::is_context_capturing_input();
|
||||
}
|
||||
|
||||
bool recomp::all_input_disabled() {
|
||||
@@ -735,16 +762,16 @@ std::string controller_button_to_string(SDL_GameControllerButton button) {
|
||||
return PF_DPAD_LEFT;
|
||||
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
||||
return PF_DPAD_RIGHT;
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
|
||||
// return "";
|
||||
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
|
||||
// return "";
|
||||
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD:
|
||||
return PF_SONY_TOUCHPAD;
|
||||
default:
|
||||
@@ -752,7 +779,7 @@ std::string controller_button_to_string(SDL_GameControllerButton button) {
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<SDL_Scancode, std::string> scancode_codepoints {
|
||||
std::unordered_map<SDL_Scancode, std::string> scancode_codepoints{
|
||||
{SDL_SCANCODE_LEFT, PF_KEYBOARD_LEFT},
|
||||
// NOTE: UP and RIGHT are swapped with promptfont.
|
||||
{SDL_SCANCODE_UP, PF_KEYBOARD_RIGHT},
|
||||
@@ -856,15 +883,15 @@ std::string controller_axis_to_string(int axis) {
|
||||
|
||||
std::string recomp::InputField::to_string() const {
|
||||
switch ((InputType)input_type) {
|
||||
case InputType::None:
|
||||
return "";
|
||||
case InputType::ControllerDigital:
|
||||
return controller_button_to_string((SDL_GameControllerButton)input_id);
|
||||
case InputType::ControllerAnalog:
|
||||
return controller_axis_to_string(input_id);
|
||||
case InputType::Keyboard:
|
||||
return keyboard_input_to_string((SDL_Scancode)input_id);
|
||||
default:
|
||||
return std::to_string(input_type) + "," + std::to_string(input_id);
|
||||
case InputType::None:
|
||||
return "";
|
||||
case InputType::ControllerDigital:
|
||||
return controller_button_to_string((SDL_GameControllerButton)input_id);
|
||||
case InputType::ControllerAnalog:
|
||||
return controller_axis_to_string(input_id);
|
||||
case InputType::Keyboard:
|
||||
return keyboard_input_to_string((SDL_Scancode)input_id);
|
||||
default:
|
||||
return std::to_string(input_type) + "," + std::to_string(input_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
#include "recomp_ui.h"
|
||||
#include "recomp_data.h"
|
||||
#include "../patches/mem_funcs.h"
|
||||
#include "../patches/actor_funcs.h"
|
||||
|
||||
struct ExtensionInfo {
|
||||
// Either the actor's type ID, or 0xFFFFFFFF if this is for generic data.
|
||||
@@ -41,7 +41,7 @@ bool can_register = false;
|
||||
size_t alloc_count = 0;
|
||||
size_t free_count = 0;
|
||||
|
||||
void recomp::init_extended_actor_data() {
|
||||
void recomputil::init_extended_actor_data() {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
|
||||
actor_data_sizes.clear();
|
||||
@@ -54,7 +54,7 @@ void recomp::init_extended_actor_data() {
|
||||
actor_extensions.push_back({});
|
||||
}
|
||||
|
||||
void recomp::reset_actor_data() {
|
||||
void recomputil::reset_actor_data() {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
actor_data.reset();
|
||||
actor_spawn_count = 0;
|
||||
@@ -113,7 +113,7 @@ extern "C" void recomp_register_actor_extension_generic(uint8_t* rdram, recomp_c
|
||||
extern "C" void recomp_clear_all_actor_data(uint8_t* rdram, recomp_context* ctx) {
|
||||
(void)rdram;
|
||||
(void)ctx;
|
||||
recomp::reset_actor_data();
|
||||
recomputil::reset_actor_data();
|
||||
}
|
||||
|
||||
extern "C" void recomp_create_actor_data(uint8_t* rdram, recomp_context* ctx) {
|
||||
@@ -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);
|
||||
}
|
||||
+30
-11
@@ -25,6 +25,7 @@
|
||||
#include "banjo_config.h"
|
||||
#include "banjo_sound.h"
|
||||
#include "banjo_render.h"
|
||||
#include "banjo_support.h"
|
||||
#include "banjo_game.h"
|
||||
#include "recomp_data.h"
|
||||
#include "ovl_patches.hpp"
|
||||
@@ -45,14 +46,15 @@
|
||||
|
||||
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
|
||||
|
||||
const std::string version_string = "0.0.1";
|
||||
const std::string version_string = "0.1.0";
|
||||
|
||||
template<typename... Ts>
|
||||
void exit_error(const char* str, Ts ...args) {
|
||||
// TODO pop up an error
|
||||
((void)fprintf(stderr, str, args), ...);
|
||||
assert(false);
|
||||
std::quick_exit(EXIT_FAILURE);
|
||||
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
|
||||
@@ -126,11 +128,13 @@ SDL_Window* window;
|
||||
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
|
||||
uint32_t flags = SDL_WINDOW_RESIZABLE;
|
||||
|
||||
#if defined(RT64_SDL_WINDOW_VULKAN)
|
||||
#if defined(__APPLE__)
|
||||
flags |= SDL_WINDOW_METAL;
|
||||
#elif defined(RT64_SDL_WINDOW_VULKAN)
|
||||
flags |= SDL_WINDOW_VULKAN;
|
||||
#endif
|
||||
|
||||
window = SDL_CreateWindow("Banjo: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, flags);
|
||||
window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, flags);
|
||||
#if defined(__linux__)
|
||||
SetImageAsIcon("icons/512.png",window);
|
||||
if (ultramodern::renderer::get_graphics_config().wm_option == ultramodern::renderer::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux
|
||||
@@ -152,6 +156,9 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::
|
||||
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
|
||||
#elif defined(__linux__) || defined(__ANDROID__)
|
||||
return ultramodern::renderer::WindowHandle{ window };
|
||||
#elif defined(__APPLE__)
|
||||
SDL_MetalView view = SDL_Metal_CreateView(window);
|
||||
return ultramodern::renderer::WindowHandle{ wmInfo.info.cocoa.window, SDL_Metal_GetLayer(view) };
|
||||
#else
|
||||
static_assert(false && "Unimplemented");
|
||||
#endif
|
||||
@@ -503,15 +510,17 @@ void release_preload(PreloadContext& context) {
|
||||
#endif
|
||||
|
||||
void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
|
||||
(void)context;
|
||||
banjo::renderer::enable_texture_pack(mod);
|
||||
banjo::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) {
|
||||
banjo::renderer::disable_texture_pack(mod);
|
||||
}
|
||||
|
||||
void reorder_texture_pack(recomp::mods::ModContext&) {
|
||||
banjo::renderer::trigger_texture_pack_update();
|
||||
}
|
||||
|
||||
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -555,14 +564,22 @@ int main(int argc, char** argv) {
|
||||
// Force wasapi on Windows, as there seems to be some issue with sample queueing with directsound currently.
|
||||
SDL_setenv("SDL_AUDIODRIVER", "wasapi", true);
|
||||
#endif
|
||||
//printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
|
||||
|
||||
#if defined(__linux__) && defined(RECOMP_FLATPAK)
|
||||
// When using Flatpak, applications tend to launch from the home directory by default.
|
||||
// Mods might use the current working directory to store the data, so we switch it to a directory
|
||||
// with persistent data storage and write permissions under Flatpak to ensure it works.
|
||||
std::error_code ec;
|
||||
std::filesystem::current_path("/var/data", ec);
|
||||
#endif
|
||||
|
||||
// Initialize SDL audio and set the output frequency.
|
||||
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||
reset_audio(48000);
|
||||
|
||||
// Source controller mappings file
|
||||
if (SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt") < 0) {
|
||||
std::u8string controller_db_path = (banjo::get_program_path() / "recompcontrollerdb.txt").u8string();
|
||||
if (SDL_GameControllerAddMappingsFromFile(reinterpret_cast<const char *>(controller_db_path.c_str())) < 0) {
|
||||
fprintf(stderr, "Failed to load controller mappings: %s\n", SDL_GetError());
|
||||
}
|
||||
|
||||
@@ -584,10 +601,11 @@ int main(int argc, char** argv) {
|
||||
REGISTER_FUNC(recomp_get_inverted_axes);
|
||||
REGISTER_FUNC(recomp_get_analog_inverted_axes);
|
||||
recompui::register_ui_exports();
|
||||
recomputil::register_data_api_exports();
|
||||
|
||||
banjo::register_bk_overlays();
|
||||
banjo::register_bk_patches();
|
||||
recomp::init_extended_actor_data();
|
||||
recomputil::init_extended_actor_data();
|
||||
banjo::load_config();
|
||||
|
||||
recomp::rsp::callbacks_t rsp_callbacks{
|
||||
@@ -636,6 +654,7 @@ 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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool debug) {
|
||||
static unsigned char dummy_rom_header[0x40];
|
||||
recompui::set_render_hooks();
|
||||
@@ -263,9 +288,11 @@ banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer:
|
||||
case ultramodern::renderer::GraphicsApi::Vulkan:
|
||||
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Vulkan;
|
||||
break;
|
||||
default:
|
||||
case ultramodern::renderer::GraphicsApi::Metal:
|
||||
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Metal;
|
||||
break;
|
||||
case ultramodern::renderer::GraphicsApi::Auto:
|
||||
// Don't override if auto is selected.
|
||||
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Automatic;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -275,6 +302,8 @@ banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer:
|
||||
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;
|
||||
@@ -303,32 +332,7 @@ banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer:
|
||||
banjo::renderer::RT64Context::~RT64Context() = default;
|
||||
|
||||
void banjo::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);
|
||||
@@ -394,6 +398,66 @@ float banjo::renderer::RT64Context::get_resolution_scale() const {
|
||||
}
|
||||
}
|
||||
|
||||
void banjo::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 banjo::renderer::RT64MaxMSAA() {
|
||||
return device_max_msaa;
|
||||
}
|
||||
@@ -410,10 +474,72 @@ bool banjo::renderer::RT64HighPrecisionFBEnabled() {
|
||||
return high_precision_fb_enabled;
|
||||
}
|
||||
|
||||
void banjo::renderer::enable_texture_pack(const recomp::mods::ModHandle& mod) {
|
||||
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_root_path});
|
||||
void banjo::renderer::trigger_texture_pack_update() {
|
||||
texture_pack_action_queue.enqueue(TexturePackUpdateAction{});
|
||||
}
|
||||
|
||||
void banjo::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(banjo::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) {
|
||||
banjo::renderer::secondary_enable_texture_pack(mod.manifest.mod_id);
|
||||
}
|
||||
else {
|
||||
banjo::renderer::secondary_disable_texture_pack(mod.manifest.mod_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void banjo::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 banjo::renderer::secondary_enable_texture_pack(const std::string& mod_id) {
|
||||
texture_pack_action_queue.enqueue(TexturePackSecondaryEnableAction{mod_id});
|
||||
}
|
||||
|
||||
void banjo::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 banjo::renderer::is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors) {
|
||||
if (option.id == banjo::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 (\"" + banjo::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 (\"" + banjo::renderer::special_option_texture_pack_enabled + "\"), but the config option doesn't have exactly 2 values.").c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "banjo_support.h"
|
||||
#include <SDL.h>
|
||||
#include "nfd.h"
|
||||
#include "RmlUi/Core.h"
|
||||
|
||||
namespace banjo {
|
||||
// MARK: - Internal Helpers
|
||||
void perform_file_dialog_operation(const std::function<void(bool, const std::filesystem::path&)>& callback) {
|
||||
nfdnchar_t* native_path = nullptr;
|
||||
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
|
||||
|
||||
bool success = (result == NFD_OKAY);
|
||||
std::filesystem::path path;
|
||||
|
||||
if (success) {
|
||||
path = std::filesystem::path{native_path};
|
||||
NFD_FreePathN(native_path);
|
||||
}
|
||||
|
||||
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_program_path() {
|
||||
#if defined(__APPLE__)
|
||||
return get_bundle_resource_directory();
|
||||
#elif defined(__linux__) && defined(RECOMP_FLATPAK)
|
||||
return "/app/bin";
|
||||
#else
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path get_asset_path(const char* asset) {
|
||||
return get_program_path() / "assets" / asset;
|
||||
}
|
||||
|
||||
void open_file_dialog(std::function<void(bool success, const std::filesystem::path& path)> callback) {
|
||||
#ifdef __APPLE__
|
||||
dispatch_on_ui_thread([callback]() {
|
||||
perform_file_dialog_operation(callback);
|
||||
});
|
||||
#else
|
||||
perform_file_dialog_operation(callback);
|
||||
#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);
|
||||
std::string message_copy(message);
|
||||
|
||||
dispatch_on_ui_thread([title_copy, message_copy] {
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title_copy.c_str(), message_copy.c_str(), nullptr);
|
||||
});
|
||||
#else
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, message, nullptr);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+174
-14
@@ -1,8 +1,10 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <fstream>
|
||||
|
||||
#include "slot_map.h"
|
||||
#include "RmlUi/Core/StreamMemory.h"
|
||||
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
#include "recomp_ui.h"
|
||||
@@ -32,8 +34,12 @@ namespace recompui {
|
||||
resource_slotmap resources;
|
||||
Rml::ElementDocument* document;
|
||||
Element root_element;
|
||||
Element* autofocus_element = nullptr;
|
||||
std::vector<Element*> loose_elements;
|
||||
std::unordered_set<ResourceId> to_update;
|
||||
std::vector<std::tuple<Element*, ResourceId, std::string>> to_set_text;
|
||||
bool captures_input = true;
|
||||
bool captures_mouse = true;
|
||||
Context(Rml::ElementDocument* document) : document(document), root_element(document) {}
|
||||
};
|
||||
} // namespace recompui
|
||||
@@ -41,10 +47,11 @@ namespace recompui {
|
||||
using context_slotmap = dod::slot_map32<recompui::Context>;
|
||||
|
||||
static struct {
|
||||
std::mutex all_contexts_lock;
|
||||
std::recursive_mutex all_contexts_lock;
|
||||
context_slotmap all_contexts;
|
||||
std::unordered_set<recompui::ContextId> opened_contexts;
|
||||
std::unordered_map<Rml::ElementDocument*, recompui::ContextId> documents_to_contexts;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> style_sheet;
|
||||
} context_state;
|
||||
|
||||
thread_local recompui::Context* opened_context = nullptr;
|
||||
@@ -61,12 +68,16 @@ enum class ContextErrorType {
|
||||
AddResourceToWrongContext,
|
||||
UpdateElementWithoutContext,
|
||||
UpdateElementInWrongContext,
|
||||
SetTextElementWithoutContext,
|
||||
SetTextElementInWrongContext,
|
||||
GetResourceWithoutOpen,
|
||||
GetResourceFailed,
|
||||
DestroyResourceWithoutOpen,
|
||||
DestroyResourceInWrongContext,
|
||||
DestroyResourceNotFound,
|
||||
GetDocumentInvalidContext,
|
||||
GetAutofocusInvalidContext,
|
||||
SetAutofocusInvalidContext,
|
||||
InternalError,
|
||||
};
|
||||
|
||||
@@ -111,6 +122,12 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
|
||||
case ContextErrorType::UpdateElementInWrongContext:
|
||||
error_message = "Attempted to update a UI element in a different UI context than the one that's open";
|
||||
break;
|
||||
case ContextErrorType::SetTextElementWithoutContext:
|
||||
error_message = "Attempted to set the text of a UI element with no open UI context";
|
||||
break;
|
||||
case ContextErrorType::SetTextElementInWrongContext:
|
||||
error_message = "Attempted to set the text of a UI element in a different UI context than the one that's open";
|
||||
break;
|
||||
case ContextErrorType::GetResourceWithoutOpen:
|
||||
error_message = "Attempted to get a UI resource with no open UI context";
|
||||
break;
|
||||
@@ -129,6 +146,12 @@ void context_error(recompui::ContextId id, ContextErrorType type) {
|
||||
case ContextErrorType::GetDocumentInvalidContext:
|
||||
error_message = "Attempted to get the document of an invalid UI context";
|
||||
break;
|
||||
case ContextErrorType::GetAutofocusInvalidContext:
|
||||
error_message = "Attempted to get the autofocus element of an invalid UI context";
|
||||
break;
|
||||
case ContextErrorType::SetAutofocusInvalidContext:
|
||||
error_message = "Attempted to set the autofocus element of an invalid UI context";
|
||||
break;
|
||||
case ContextErrorType::InternalError:
|
||||
error_message = "Internal error in UI context";
|
||||
break;
|
||||
@@ -166,6 +189,21 @@ recompui::ContextId create_context_impl(Rml::ElementDocument* document) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void recompui::init_styling(const std::filesystem::path& rcss_file) {
|
||||
std::string style{};
|
||||
{
|
||||
std::ifstream style_stream{rcss_file};
|
||||
style_stream.seekg(0, std::ios::end);
|
||||
style.resize(style_stream.tellg());
|
||||
style_stream.seekg(0, std::ios::beg);
|
||||
|
||||
style_stream.read(style.data(), style.size());
|
||||
}
|
||||
std::unique_ptr<Rml::StreamMemory> rml_stream = std::make_unique<Rml::StreamMemory>(reinterpret_cast<Rml::byte*>(style.data()), style.size());
|
||||
rml_stream->SetSourceURL(rcss_file.filename().string());
|
||||
context_state.style_sheet = Rml::Factory::InstanceStyleSheetStream(rml_stream.get());
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::create_context(const std::filesystem::path& path) {
|
||||
ContextId new_context = create_context_impl(nullptr);
|
||||
|
||||
@@ -193,29 +231,20 @@ recompui::ContextId recompui::create_context(Rml::ElementDocument* document) {
|
||||
|
||||
recompui::ContextId recompui::create_context() {
|
||||
Rml::ElementDocument* doc = create_empty_document();
|
||||
doc->SetStyleSheetContainer(context_state.style_sheet);
|
||||
ContextId ret = create_context_impl(doc);
|
||||
Element* root = ret.get_root_element();
|
||||
// Mark the root element as not being a shim, as that's only needed for elements that were parented to Rml ones manually.
|
||||
root->shim = false;
|
||||
|
||||
// TODO move these defaults elsewhere. Copied from the existing rcss.
|
||||
ret.open();
|
||||
root->set_width(100.0f, Unit::Percent);
|
||||
root->set_height(100.0f, Unit::Percent);
|
||||
root->set_display(Display::Flex);
|
||||
root->set_opacity(1.0f);
|
||||
root->set_font_family("LatoLatin");
|
||||
root->set_font_style(FontStyle::Normal);
|
||||
root->set_font_weight(400);
|
||||
|
||||
float sz = 16.0f;
|
||||
float spacing = 0.0f;
|
||||
float sz_add = sz + 4;
|
||||
root->set_font_size(sz_add, Unit::Dp);
|
||||
root->set_letter_spacing(sz_add * spacing, Unit::Dp);
|
||||
root->set_line_height(sz_add, Unit::Dp);
|
||||
ret.close();
|
||||
|
||||
doc->Hide();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -307,6 +336,15 @@ void recompui::ContextId::open() {
|
||||
opened_context_id = *this;
|
||||
}
|
||||
|
||||
bool recompui::ContextId::open_if_not_already() {
|
||||
if (opened_context_id == *this) {
|
||||
return false;
|
||||
}
|
||||
|
||||
open();
|
||||
return true;
|
||||
}
|
||||
|
||||
void recompui::ContextId::close() {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
@@ -330,6 +368,15 @@ void recompui::ContextId::close() {
|
||||
}
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::try_close_current_context() {
|
||||
if (opened_context_id != ContextId::null()) {
|
||||
ContextId prev_context = opened_context_id;
|
||||
opened_context_id.close();
|
||||
return prev_context;
|
||||
}
|
||||
return ContextId::null();
|
||||
}
|
||||
|
||||
void recompui::ContextId::process_updates() {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
@@ -367,8 +414,83 @@ void recompui::ContextId::process_updates() {
|
||||
continue;
|
||||
}
|
||||
|
||||
static_cast<Element*>(cur_resource->get())->process_event(update_event);
|
||||
static_cast<Element*>(cur_resource->get())->handle_event(update_event);
|
||||
}
|
||||
|
||||
std::vector<std::tuple<Element*, ResourceId, std::string>> to_set_text = std::move(opened_context->to_set_text);
|
||||
|
||||
// Delete the Rml elements that are pending deletion.
|
||||
for (auto cur_text_update : to_set_text) {
|
||||
Element* element_ptr = std::get<0>(cur_text_update);
|
||||
ResourceId resource = std::get<1>(cur_text_update);
|
||||
std::string& text = std::get<2>(cur_text_update);
|
||||
|
||||
// If the resource ID is valid, prefer that as we can quickly validate if the resource still exists.
|
||||
if (resource != ResourceId::null()) {
|
||||
resource_slotmap::key cur_key{ resource.slot_id };
|
||||
std::unique_ptr<Style>* cur_resource = opened_context->resources.get(cur_key);
|
||||
|
||||
// Make sure the resource exists before setting its text, as it may have been deleted.
|
||||
if (cur_resource == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform the text update.
|
||||
static_cast<Element*>(cur_resource->get())->base->SetInnerRML(text);
|
||||
}
|
||||
// Otherwise we use the element pointer, but we need to validate that it still exists before doing so.
|
||||
else {
|
||||
// Scan the current resources to find the target element.
|
||||
for (const std::unique_ptr<Style>& cur_e : opened_context->resources) {
|
||||
if (cur_e.get() == element_ptr) {
|
||||
element_ptr->base->SetInnerRML(text);
|
||||
// We can stop after finding the element.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool recompui::ContextId::captures_input() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return ctx->captures_input;
|
||||
|
||||
}
|
||||
|
||||
bool recompui::ContextId::captures_mouse() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return ctx->captures_mouse;
|
||||
}
|
||||
|
||||
void recompui::ContextId::set_captures_input(bool captures_input) {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return;
|
||||
}
|
||||
ctx->captures_input = captures_input;
|
||||
}
|
||||
|
||||
void recompui::ContextId::set_captures_mouse(bool captures_mouse) {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return;
|
||||
}
|
||||
ctx->captures_mouse = captures_mouse;
|
||||
}
|
||||
|
||||
recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&& resource) {
|
||||
@@ -387,6 +509,8 @@ recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&&
|
||||
auto key = opened_context->resources.emplace(std::move(resource));
|
||||
|
||||
if (is_element) {
|
||||
Element* element_ptr = static_cast<Element*>(resource_ptr);
|
||||
element_ptr->set_id(std::string{element_ptr->get_type_name()} + "-" + std::to_string(key.raw));
|
||||
key.set_tag(static_cast<uint8_t>(SlotTag::Element));
|
||||
// Send one update to the element.
|
||||
opened_context->to_update.emplace(ResourceId{ key.raw });
|
||||
@@ -433,6 +557,20 @@ void recompui::ContextId::queue_element_update(ResourceId element) {
|
||||
opened_context->to_update.emplace(element);
|
||||
}
|
||||
|
||||
void recompui::ContextId::queue_set_text(Element* element, std::string&& text) {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::SetTextElementWithoutContext);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::SetTextElementInWrongContext);
|
||||
}
|
||||
|
||||
opened_context->to_set_text.emplace_back(std::make_tuple(element, element->resource_id, std::move(text)));
|
||||
}
|
||||
|
||||
recompui::Style* recompui::ContextId::create_style() {
|
||||
return add_resource_impl(std::make_unique<Style>());
|
||||
}
|
||||
@@ -502,6 +640,28 @@ recompui::Element* recompui::ContextId::get_root_element() {
|
||||
return &ctx->root_element;
|
||||
}
|
||||
|
||||
recompui::Element* recompui::ContextId::get_autofocus_element() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
context_error(*this, ContextErrorType::GetAutofocusInvalidContext);
|
||||
}
|
||||
|
||||
return ctx->autofocus_element;
|
||||
}
|
||||
|
||||
void recompui::ContextId::set_autofocus_element(Element* element) {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
context_error(*this, ContextErrorType::SetAutofocusInvalidContext);
|
||||
}
|
||||
|
||||
ctx->autofocus_element = element;
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::get_current_context() {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace recompui {
|
||||
|
||||
void add_loose_element(Element* element);
|
||||
void queue_element_update(ResourceId element);
|
||||
void queue_set_text(Element* element, std::string&& text);
|
||||
|
||||
Style* create_style();
|
||||
|
||||
@@ -40,15 +41,21 @@ namespace recompui {
|
||||
|
||||
Rml::ElementDocument* get_document();
|
||||
Element* get_root_element();
|
||||
Element* get_autofocus_element();
|
||||
void set_autofocus_element(Element* element);
|
||||
|
||||
void open();
|
||||
bool open_if_not_already();
|
||||
void close();
|
||||
void process_updates();
|
||||
|
||||
static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; }
|
||||
|
||||
// TODO
|
||||
bool takes_input() { return true; }
|
||||
bool captures_input();
|
||||
bool captures_mouse();
|
||||
|
||||
void set_captures_input(bool captures_input);
|
||||
void set_captures_mouse(bool captures_input);
|
||||
};
|
||||
|
||||
ContextId create_context(const std::filesystem::path& path);
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
|
||||
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "button", true) {
|
||||
this->style = style;
|
||||
|
||||
enable_focus();
|
||||
|
||||
set_text(text);
|
||||
set_display(Display::Block);
|
||||
set_padding(23.0f);
|
||||
@@ -21,6 +23,7 @@ namespace recompui {
|
||||
set_color(Color{ 204, 204, 204, 255 });
|
||||
set_tab_index(TabIndex::Auto);
|
||||
hover_style.set_color(Color{ 242, 242, 242, 255 });
|
||||
focus_style.set_color(Color{ 242, 242, 242, 255 });
|
||||
disabled_style.set_color(Color{ 204, 204, 204, 128 });
|
||||
hover_disabled_style.set_color(Color{ 242, 242, 242, 128 });
|
||||
|
||||
@@ -34,6 +37,8 @@ namespace recompui {
|
||||
set_background_color({ 185, 125, 242, background_opacity });
|
||||
hover_style.set_border_color({ 185, 125, 242, border_hover_opacity });
|
||||
hover_style.set_background_color({ 185, 125, 242, background_hover_opacity });
|
||||
focus_style.set_border_color({ 185, 125, 242, border_hover_opacity });
|
||||
focus_style.set_background_color({ 185, 125, 242, background_hover_opacity });
|
||||
disabled_style.set_border_color({ 185, 125, 242, border_opacity / 4 });
|
||||
disabled_style.set_background_color({ 185, 125, 242, background_opacity / 4 });
|
||||
hover_disabled_style.set_border_color({ 185, 125, 242, border_hover_opacity / 4 });
|
||||
@@ -45,6 +50,8 @@ namespace recompui {
|
||||
set_background_color({ 23, 214, 232, background_opacity });
|
||||
hover_style.set_border_color({ 23, 214, 232, border_hover_opacity });
|
||||
hover_style.set_background_color({ 23, 214, 232, background_hover_opacity });
|
||||
focus_style.set_border_color({ 23, 214, 232, border_hover_opacity });
|
||||
focus_style.set_background_color({ 23, 214, 232, background_hover_opacity });
|
||||
disabled_style.set_border_color({ 23, 214, 232, border_opacity / 4 });
|
||||
disabled_style.set_background_color({ 23, 214, 232, background_opacity / 4 });
|
||||
hover_disabled_style.set_border_color({ 23, 214, 232, border_hover_opacity / 4 });
|
||||
@@ -57,6 +64,7 @@ namespace recompui {
|
||||
}
|
||||
|
||||
add_style(&hover_style, hover_state);
|
||||
add_style(&focus_style, focus_state);
|
||||
add_style(&disabled_style, disabled_state);
|
||||
add_style(&hover_disabled_style, { hover_state, disabled_state });
|
||||
|
||||
@@ -73,10 +81,24 @@ namespace recompui {
|
||||
}
|
||||
break;
|
||||
case EventType::Hover:
|
||||
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
|
||||
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
|
||||
break;
|
||||
case EventType::Enable:
|
||||
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
|
||||
{
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
set_style_enabled(disabled_state, !enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Focus:
|
||||
set_style_enabled(focus_state, std::get<EventFocus>(e.variant).active);
|
||||
break;
|
||||
case EventType::Update:
|
||||
break;
|
||||
|
||||
@@ -13,15 +13,21 @@ namespace recompui {
|
||||
protected:
|
||||
ButtonStyle style = ButtonStyle::Primary;
|
||||
Style hover_style;
|
||||
Style focus_style;
|
||||
Style disabled_style;
|
||||
Style hover_disabled_style;
|
||||
std::list<std::function<void()>> pressed_callbacks;
|
||||
|
||||
// Element overrides.
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Button"; }
|
||||
public:
|
||||
Button(Element *parent, const std::string &text, ButtonStyle style);
|
||||
void add_pressed_callback(std::function<void()> callback);
|
||||
Style* get_hover_style() { return &hover_style; }
|
||||
Style* get_focus_style() { return &focus_style; }
|
||||
Style* get_disabled_style() { return &disabled_style; }
|
||||
Style* get_hover_disabled_style() { return &hover_disabled_style; }
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
|
||||
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::MouseButton, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
if (draggable) {
|
||||
set_drag(Drag::Drag);
|
||||
}
|
||||
@@ -11,30 +12,60 @@ namespace recompui {
|
||||
void Clickable::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Click: {
|
||||
const EventClick &click = std::get<EventClick>(e.variant);
|
||||
for (const auto &function : pressed_callbacks) {
|
||||
function(click.x, click.y);
|
||||
if (is_enabled()) {
|
||||
const EventClick &click = std::get<EventClick>(e.variant);
|
||||
for (const auto &function : clicked_callbacks) {
|
||||
function(click.x, click.y);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
case EventType::MouseButton: {
|
||||
if (is_enabled()) {
|
||||
const EventMouseButton &mousebutton = std::get<EventMouseButton>(e.variant);
|
||||
if (mousebutton.button == MouseButton::Left && mousebutton.pressed) {
|
||||
for (const auto &function : pressed_callbacks) {
|
||||
function(mousebutton.x, mousebutton.y);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EventType::Hover:
|
||||
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
|
||||
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
|
||||
break;
|
||||
case EventType::Enable:
|
||||
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
|
||||
break;
|
||||
case EventType::Drag: {
|
||||
const EventDrag &drag = std::get<EventDrag>(e.variant);
|
||||
for (const auto &function : dragged_callbacks) {
|
||||
function(drag.x, drag.y, drag.phase);
|
||||
{
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
set_style_enabled(disabled_state, !enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Drag: {
|
||||
if (is_enabled()) {
|
||||
const EventDrag &drag = std::get<EventDrag>(e.variant);
|
||||
for (const auto &function : dragged_callbacks) {
|
||||
function(drag.x, drag.y, drag.phase);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Clickable::add_clicked_callback(std::function<void(float, float)> callback) {
|
||||
clicked_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
void Clickable::add_pressed_callback(std::function<void(float, float)> callback) {
|
||||
pressed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ namespace recompui {
|
||||
|
||||
class Clickable : public Element {
|
||||
protected:
|
||||
std::vector<std::function<void(float, float)>> clicked_callbacks;
|
||||
std::vector<std::function<void(float, float)>> pressed_callbacks;
|
||||
std::vector<std::function<void(float, float, DragPhase)>> dragged_callbacks;
|
||||
|
||||
// Element overrides.
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Clickable"; }
|
||||
public:
|
||||
Clickable(Element *parent, bool draggable = false);
|
||||
void add_clicked_callback(std::function<void(float, float)> callback);
|
||||
void add_pressed_callback(std::function<void(float, float)> callback);
|
||||
void add_dragged_callback(std::function<void(float, float, DragPhase)> callback);
|
||||
};
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content) : Element(parent) {
|
||||
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled) : Element(parent, events_enabled) {
|
||||
set_display(Display::Flex);
|
||||
set_flex(1.0f, 1.0f);
|
||||
set_flex_direction(direction);
|
||||
set_justify_content(justify_content);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
namespace recompui {
|
||||
|
||||
class Container : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "Container"; }
|
||||
public:
|
||||
Container(Element* parent, FlexDirection direction, JustifyContent justify_content);
|
||||
Container(Element* parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled = 0);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
|
||||
+219
-15
@@ -1,3 +1,7 @@
|
||||
#include "RmlUi/Core/StringUtilities.h"
|
||||
|
||||
#include "overloaded.h"
|
||||
#include "recomp_ui.h"
|
||||
#include "ui_element.h"
|
||||
#include "../core/ui_context.h"
|
||||
|
||||
@@ -13,7 +17,7 @@ Element::Element(Rml::Element *base) {
|
||||
this->shim = true;
|
||||
}
|
||||
|
||||
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class) {
|
||||
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class, bool can_set_text) : can_set_text(can_set_text) {
|
||||
ContextId context = get_current_context();
|
||||
base_owning = context.get_document()->CreateElement(base_class);
|
||||
|
||||
@@ -25,6 +29,9 @@ Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_clas
|
||||
base = base_owning.get();
|
||||
}
|
||||
|
||||
set_display(Display::Block);
|
||||
set_property(Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox);
|
||||
|
||||
register_event_listeners(events_enabled);
|
||||
}
|
||||
|
||||
@@ -39,6 +46,10 @@ Element::~Element() {
|
||||
|
||||
void Element::add_child(Element *child) {
|
||||
assert(child != nullptr);
|
||||
if (can_set_text) {
|
||||
assert(false && "Elements with settable text cannot have children");
|
||||
return;
|
||||
}
|
||||
|
||||
children.emplace_back(child);
|
||||
|
||||
@@ -61,7 +72,12 @@ void Element::register_event_listeners(uint32_t events_enabled) {
|
||||
this->events_enabled = events_enabled;
|
||||
|
||||
if (events_enabled & Events(EventType::Click)) {
|
||||
base->AddEventListener(Rml::EventId::Click, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::MouseButton)) {
|
||||
base->AddEventListener(Rml::EventId::Mousedown, this);
|
||||
base->AddEventListener(Rml::EventId::Mouseup, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::Focus)) {
|
||||
@@ -83,11 +99,20 @@ void Element::register_event_listeners(uint32_t events_enabled) {
|
||||
if (events_enabled & Events(EventType::Text)) {
|
||||
base->AddEventListener(Rml::EventId::Change, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::Navigate)) {
|
||||
base->AddEventListener(Rml::EventId::Keydown, this);
|
||||
}
|
||||
}
|
||||
|
||||
void Element::apply_style(Style *style) {
|
||||
for (auto it : style->property_map) {
|
||||
base->SetProperty(it.first, it.second);
|
||||
// Skip redundant SetProperty calls to prevent dirtying unnecessary state.
|
||||
// This avoids expensive layout operations when a simple color-only style is applied.
|
||||
const Rml::Property* cur_value = base->GetLocalProperty(it.first);
|
||||
if (*cur_value != it.second) {
|
||||
base->SetProperty(it.first, it.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +135,7 @@ void Element::propagate_disabled(bool disabled) {
|
||||
base->SetAttribute("disabled", attribute_state);
|
||||
|
||||
if (events_enabled & Events(EventType::Enable)) {
|
||||
process_event(Event::enable_event(!attribute_state));
|
||||
handle_event(Event::enable_event(!attribute_state));
|
||||
}
|
||||
|
||||
for (auto &child : children) {
|
||||
@@ -119,25 +144,86 @@ void Element::propagate_disabled(bool disabled) {
|
||||
}
|
||||
}
|
||||
|
||||
void Element::handle_event(const Event& event) {
|
||||
for (const auto& callback : callbacks) {
|
||||
recompui::queue_ui_callback(resource_id, event, callback);
|
||||
}
|
||||
|
||||
process_event(event);
|
||||
}
|
||||
|
||||
void Element::set_id(const std::string& new_id) {
|
||||
id = new_id;
|
||||
base->SetId(new_id);
|
||||
}
|
||||
|
||||
recompui::MouseButton convert_rml_mouse_button(int button) {
|
||||
switch (button) {
|
||||
case 0:
|
||||
return recompui::MouseButton::Left;
|
||||
case 1:
|
||||
return recompui::MouseButton::Right;
|
||||
case 2:
|
||||
return recompui::MouseButton::Middle;
|
||||
default:
|
||||
return recompui::MouseButton::Count;
|
||||
}
|
||||
}
|
||||
|
||||
void Element::ProcessEvent(Rml::Event &event) {
|
||||
ContextId prev_context = recompui::try_close_current_context();
|
||||
ContextId context = ContextId::null();
|
||||
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
|
||||
if (doc != nullptr) {
|
||||
context = get_context_from_document(doc);
|
||||
}
|
||||
|
||||
bool did_open = false;
|
||||
|
||||
// TODO disallow null contexts once the entire UI system has been migrated.
|
||||
if (context != ContextId::null()) {
|
||||
context.open();
|
||||
did_open = context.open_if_not_already();
|
||||
}
|
||||
|
||||
// Events that are processed during any phase.
|
||||
switch (event.GetId()) {
|
||||
case Rml::EventId::Click:
|
||||
handle_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
|
||||
break;
|
||||
case Rml::EventId::Mousedown:
|
||||
process_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
|
||||
{
|
||||
MouseButton mouse_button = convert_rml_mouse_button(event.GetParameter("button", 3));
|
||||
if (mouse_button != MouseButton::Count) {
|
||||
handle_event(Event::mousebutton_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), mouse_button, true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Rml::EventId::Mouseup:
|
||||
{
|
||||
MouseButton mouse_button = convert_rml_mouse_button(event.GetParameter("button", 3));
|
||||
if (mouse_button != MouseButton::Count) {
|
||||
handle_event(Event::mousebutton_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), mouse_button, false));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Rml::EventId::Keydown:
|
||||
switch ((Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0)) {
|
||||
case Rml::Input::KeyIdentifier::KI_LEFT:
|
||||
handle_event(Event::navigate_event(NavDirection::Left));
|
||||
break;
|
||||
case Rml::Input::KeyIdentifier::KI_UP:
|
||||
handle_event(Event::navigate_event(NavDirection::Up));
|
||||
break;
|
||||
case Rml::Input::KeyIdentifier::KI_RIGHT:
|
||||
handle_event(Event::navigate_event(NavDirection::Right));
|
||||
break;
|
||||
case Rml::Input::KeyIdentifier::KI_DOWN:
|
||||
handle_event(Event::navigate_event(NavDirection::Down));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Rml::EventId::Drag:
|
||||
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
|
||||
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -147,28 +233,28 @@ void Element::ProcessEvent(Rml::Event &event) {
|
||||
if (event.GetPhase() == Rml::EventPhase::Target) {
|
||||
switch (event.GetId()) {
|
||||
case Rml::EventId::Mouseover:
|
||||
process_event(Event::hover_event(true));
|
||||
handle_event(Event::hover_event(true));
|
||||
break;
|
||||
case Rml::EventId::Mouseout:
|
||||
process_event(Event::hover_event(false));
|
||||
handle_event(Event::hover_event(false));
|
||||
break;
|
||||
case Rml::EventId::Focus:
|
||||
process_event(Event::focus_event(true));
|
||||
handle_event(Event::focus_event(true));
|
||||
break;
|
||||
case Rml::EventId::Blur:
|
||||
process_event(Event::focus_event(false));
|
||||
handle_event(Event::focus_event(false));
|
||||
break;
|
||||
case Rml::EventId::Dragstart:
|
||||
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
|
||||
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
|
||||
break;
|
||||
case Rml::EventId::Dragend:
|
||||
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End));
|
||||
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End));
|
||||
break;
|
||||
case Rml::EventId::Change: {
|
||||
if (events_enabled & Events(EventType::Text)) {
|
||||
Rml::Variant *value_variant = base->GetAttribute("value");
|
||||
if (value_variant != nullptr) {
|
||||
process_event(Event::text_event(value_variant->Get<std::string>()));
|
||||
handle_event(Event::text_event(value_variant->Get<std::string>()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,9 +265,13 @@ void Element::ProcessEvent(Rml::Event &event) {
|
||||
}
|
||||
}
|
||||
|
||||
if (context != ContextId::null()) {
|
||||
if (context != ContextId::null() && did_open) {
|
||||
context.close();
|
||||
}
|
||||
|
||||
if (prev_context != ContextId::null()) {
|
||||
prev_context.open();
|
||||
}
|
||||
}
|
||||
|
||||
void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) {
|
||||
@@ -192,6 +282,15 @@ void Element::process_event(const Event &) {
|
||||
// Does nothing by default.
|
||||
}
|
||||
|
||||
void Element::enable_focus() {
|
||||
set_tab_index_auto();
|
||||
set_focusable(true);
|
||||
set_nav_auto(NavDirection::Up);
|
||||
set_nav_auto(NavDirection::Down);
|
||||
set_nav_auto(NavDirection::Left);
|
||||
set_nav_auto(NavDirection::Right);
|
||||
}
|
||||
|
||||
void Element::clear_children() {
|
||||
if (children.empty()) {
|
||||
return;
|
||||
@@ -207,6 +306,24 @@ void Element::clear_children() {
|
||||
children.clear();
|
||||
}
|
||||
|
||||
bool Element::remove_child(ResourceId child) {
|
||||
bool found = false;
|
||||
|
||||
ContextId context = get_current_context();
|
||||
|
||||
for (auto it = children.begin(); it != children.end(); ++it) {
|
||||
Element* cur_child = *it;
|
||||
if (cur_child->get_resource_id() == child) {
|
||||
children.erase(it);
|
||||
context.destroy_resource(cur_child);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
void Element::add_style(Style *style, const std::string_view style_name) {
|
||||
add_style(style, { style_name });
|
||||
}
|
||||
@@ -238,8 +355,46 @@ bool Element::is_enabled() const {
|
||||
return enabled && !disabled_from_parent;
|
||||
}
|
||||
|
||||
// Adapted from RmlUi's `EncodeRml`.
|
||||
std::string escape_rml(std::string_view string)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(string.size());
|
||||
for (char c : string)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '<': result += "<"; break;
|
||||
case '>': result += ">"; break;
|
||||
case '&': result += "&"; break;
|
||||
case '"': result += """; break;
|
||||
case '\n': result += "<br/>"; break;
|
||||
default: result += c; break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Element::set_text(std::string_view text) {
|
||||
base->SetInnerRML(std::string(text));
|
||||
if (can_set_text) {
|
||||
// Queue the text update. If it's applied immediately, it might happen
|
||||
// while the document is being updated or rendered. This can cause a crash
|
||||
// due to the child elements being deleted while the document is being updated.
|
||||
// Queueing them defers it to the update thread, which prevents that issue.
|
||||
// Escape the string into Rml to prevent element injection.
|
||||
get_current_context().queue_set_text(this, escape_rml(text));
|
||||
}
|
||||
else {
|
||||
assert(false && "Attempted to set text of an element that cannot have its text set.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string Element::get_input_text() {
|
||||
return base->GetAttribute("value", std::string{});
|
||||
}
|
||||
|
||||
void Element::set_input_text(std::string_view val) {
|
||||
base->SetAttribute("value", std::string{ val });
|
||||
}
|
||||
|
||||
void Element::set_src(std::string_view src) {
|
||||
@@ -274,6 +429,10 @@ void Element::set_style_enabled(std::string_view style_name, bool enable) {
|
||||
apply_styles();
|
||||
}
|
||||
|
||||
bool Element::is_style_enabled(std::string_view style_name) {
|
||||
return style_active_set.contains(style_name);
|
||||
}
|
||||
|
||||
float Element::get_absolute_left() {
|
||||
return base->GetAbsoluteLeft();
|
||||
}
|
||||
@@ -298,6 +457,47 @@ float Element::get_client_height() {
|
||||
return base->GetClientHeight();
|
||||
}
|
||||
|
||||
uint32_t Element::get_input_value_u32() {
|
||||
ElementValue value = get_element_value();
|
||||
|
||||
return std::visit(overloaded {
|
||||
[](double d) { return (uint32_t)d; },
|
||||
[](float f) { return (uint32_t)f; },
|
||||
[](uint32_t u) { return u; },
|
||||
[](std::monostate) { return 0U; }
|
||||
}, value);
|
||||
}
|
||||
|
||||
float Element::get_input_value_float() {
|
||||
ElementValue value = get_element_value();
|
||||
|
||||
return std::visit(overloaded {
|
||||
[](double d) { return (float)d; },
|
||||
[](float f) { return f; },
|
||||
[](uint32_t u) { return (float)u; },
|
||||
[](std::monostate) { return 0.0f; }
|
||||
}, value);
|
||||
}
|
||||
|
||||
double Element::get_input_value_double() {
|
||||
ElementValue value = get_element_value();
|
||||
|
||||
return std::visit(overloaded {
|
||||
[](double d) { return d; },
|
||||
[](float f) { return (double)f; },
|
||||
[](uint32_t u) { return (double)u; },
|
||||
[](std::monostate) { return 0.0; }
|
||||
}, value);
|
||||
}
|
||||
|
||||
void Element::focus() {
|
||||
base->Focus();
|
||||
}
|
||||
|
||||
void Element::blur() {
|
||||
base->Blur();
|
||||
}
|
||||
|
||||
void Element::queue_update() {
|
||||
ContextId cur_context = get_current_context();
|
||||
|
||||
@@ -309,4 +509,8 @@ void Element::queue_update() {
|
||||
cur_context.queue_element_update(resource_id);
|
||||
}
|
||||
|
||||
void Element::register_callback(ContextId context, PTR(void) callback, PTR(void) userdata) {
|
||||
callbacks.emplace_back(UICallback{.context = context, .callback = callback, .userdata = userdata});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,14 +3,26 @@
|
||||
#include "ui_style.h"
|
||||
#include "../core/ui_context.h"
|
||||
|
||||
#include "recomp.h"
|
||||
#include <ultramodern/ultra64.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
|
||||
namespace recompui {
|
||||
struct UICallback {
|
||||
ContextId context;
|
||||
PTR(void) callback;
|
||||
PTR(void) userdata;
|
||||
};
|
||||
|
||||
using ElementValue = std::variant<uint32_t, float, double, std::monostate>;
|
||||
|
||||
class ContextId;
|
||||
class Element : public Style, public Rml::EventListener {
|
||||
friend ContextId create_context(const std::filesystem::path& path);
|
||||
friend ContextId create_context();
|
||||
friend class ContextId; // To allow ContextId to call the process_event method directly.
|
||||
friend class ContextId; // To allow ContextId to call the handle_event method directly.
|
||||
private:
|
||||
Rml::Element *base = nullptr;
|
||||
Rml::ElementPtr base_owning = {};
|
||||
@@ -19,17 +31,21 @@ private:
|
||||
std::vector<uint32_t> styles_counter;
|
||||
std::unordered_set<std::string_view> style_active_set;
|
||||
std::unordered_multimap<std::string_view, uint32_t> style_name_index_map;
|
||||
std::vector<UICallback> callbacks;
|
||||
std::vector<Element *> children;
|
||||
std::string id;
|
||||
bool shim = false;
|
||||
bool enabled = true;
|
||||
bool disabled_attribute = false;
|
||||
bool disabled_from_parent = false;
|
||||
bool can_set_text = false;
|
||||
|
||||
void add_child(Element *child);
|
||||
void register_event_listeners(uint32_t events_enabled);
|
||||
void apply_style(Style *style);
|
||||
void apply_styles();
|
||||
void propagate_disabled(bool disabled);
|
||||
void handle_event(const Event &e);
|
||||
void set_id(const std::string& new_id);
|
||||
|
||||
// Style overrides.
|
||||
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override;
|
||||
@@ -40,21 +56,30 @@ protected:
|
||||
// Use of this method in inherited classes is discouraged unless it's necessary.
|
||||
void set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value);
|
||||
virtual void process_event(const Event &e);
|
||||
virtual ElementValue get_element_value() { return std::monostate{}; }
|
||||
virtual void set_input_value(const ElementValue&) {}
|
||||
virtual std::string_view get_type_name() { return "Element"; }
|
||||
public:
|
||||
// Used for backwards compatibility with legacy UI elements.
|
||||
Element(Rml::Element *base);
|
||||
|
||||
// Used to actually construct elements.
|
||||
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div");
|
||||
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div", bool can_set_text = false);
|
||||
virtual ~Element();
|
||||
void clear_children();
|
||||
bool remove_child(ResourceId child);
|
||||
bool remove_child(Element *child) { return remove_child(child->get_resource_id()); }
|
||||
void add_style(Style *style, std::string_view style_name);
|
||||
void add_style(Style *style, const std::initializer_list<std::string_view> &style_names);
|
||||
void set_enabled(bool enabled);
|
||||
bool is_enabled() const;
|
||||
void set_text(std::string_view text);
|
||||
std::string get_input_text();
|
||||
void set_input_text(std::string_view text);
|
||||
void set_src(std::string_view src);
|
||||
void set_style_enabled(std::string_view style_name, bool enabled);
|
||||
bool is_style_enabled(std::string_view style_name);
|
||||
void apply_styles();
|
||||
bool is_element() override { return true; }
|
||||
float get_absolute_left();
|
||||
float get_absolute_top();
|
||||
@@ -62,7 +87,20 @@ public:
|
||||
float get_client_top();
|
||||
float get_client_width();
|
||||
float get_client_height();
|
||||
void enable_focus();
|
||||
void focus();
|
||||
void blur();
|
||||
void queue_update();
|
||||
void register_callback(ContextId context, PTR(void) callback, PTR(void) userdata);
|
||||
uint32_t get_input_value_u32();
|
||||
float get_input_value_float();
|
||||
double get_input_value_double();
|
||||
void set_input_value_u32(uint32_t val) { set_input_value(val); }
|
||||
void set_input_value_float(float val) { set_input_value(val); }
|
||||
void set_input_value_double(double val) { set_input_value(val); }
|
||||
const std::string& get_id() { return id; }
|
||||
};
|
||||
|
||||
void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback);
|
||||
|
||||
} // namespace recompui
|
||||
@@ -5,6 +5,8 @@
|
||||
namespace recompui {
|
||||
|
||||
class Image : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ImageView"; }
|
||||
public:
|
||||
Image(Element *parent, std::string_view src);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Label::Label(Element *parent, LabelStyle label_style) : Element(parent) {
|
||||
Label::Label(Element *parent, LabelStyle label_style) : Element(parent, 0U, "div", true) {
|
||||
switch (label_style) {
|
||||
case LabelStyle::Annotation:
|
||||
set_color(Color{ 185, 125, 242, 255 });
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace recompui {
|
||||
};
|
||||
|
||||
class Label : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "Label"; }
|
||||
public:
|
||||
Label(Element *parent, LabelStyle label_style);
|
||||
Label(Element *parent, const std::string &text, LabelStyle label_style);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#include "overloaded.h"
|
||||
#include "ui_radio.h"
|
||||
#include "../ui_utils.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
// RadioOption
|
||||
|
||||
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "label") {
|
||||
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::MouseButton, EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable, EventType::Update), "label", true) {
|
||||
this->index = index;
|
||||
|
||||
enable_focus();
|
||||
set_text(name);
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_font_size(20.0f);
|
||||
@@ -14,29 +17,44 @@ namespace recompui {
|
||||
set_line_height(20.0f);
|
||||
set_font_weight(400);
|
||||
set_font_style(FontStyle::Normal);
|
||||
set_border_color(Color{ 242, 242, 242, 255 });
|
||||
set_border_bottom_width(0.0f);
|
||||
set_border_color(Color{ 242, 242, 242, 0 });
|
||||
set_border_bottom_width(1.0f);
|
||||
set_color(Color{ 255, 255, 255, 153 });
|
||||
set_padding_bottom(8.0f);
|
||||
set_text_transform(TextTransform::Uppercase);
|
||||
set_height_auto();
|
||||
hover_style.set_color(Color{ 255, 255, 255, 204 });
|
||||
checked_style.set_color(Color{ 255, 255, 255, 255 });
|
||||
checked_style.set_border_bottom_width(1.0f);
|
||||
checked_style.set_border_color(Color{ 242, 242, 242, 255 });
|
||||
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
|
||||
|
||||
add_style(&hover_style, { hover_state });
|
||||
add_style(&checked_style, { checked_state });
|
||||
add_style(&pulsing_style, { focus_state });
|
||||
}
|
||||
|
||||
void RadioOption::set_pressed_callback(std::function<void(uint32_t)> callback) {
|
||||
pressed_callback = callback;
|
||||
}
|
||||
|
||||
void RadioOption::set_focus_callback(std::function<void(bool)> callback) {
|
||||
focus_callback = callback;
|
||||
}
|
||||
|
||||
void RadioOption::set_selected_state(bool enable) {
|
||||
set_style_enabled(checked_state, enable);
|
||||
}
|
||||
|
||||
void RadioOption::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::MouseButton:
|
||||
{
|
||||
const EventMouseButton &mousebutton = std::get<EventMouseButton>(e.variant);
|
||||
if (mousebutton.button == MouseButton::Left && mousebutton.pressed) {
|
||||
pressed_callback(index);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Click:
|
||||
pressed_callback(index);
|
||||
break;
|
||||
@@ -46,6 +64,25 @@ namespace recompui {
|
||||
case EventType::Enable:
|
||||
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
|
||||
break;
|
||||
case EventType::Focus:
|
||||
{
|
||||
bool active = std::get<EventFocus>(e.variant).active;
|
||||
set_style_enabled(focus_state, active);
|
||||
if (active) {
|
||||
queue_update();
|
||||
}
|
||||
if (focus_callback != nullptr) {
|
||||
focus_callback(active);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Update:
|
||||
if (is_style_enabled(focus_state)) {
|
||||
pulsing_style.set_color(recompui::get_pulse_color(750));
|
||||
apply_styles();
|
||||
queue_update();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -70,10 +107,41 @@ namespace recompui {
|
||||
void Radio::option_selected(uint32_t index) {
|
||||
set_index_internal(index, false, true);
|
||||
}
|
||||
|
||||
void Radio::set_input_value(const ElementValue& val) {
|
||||
std::visit(overloaded {
|
||||
[this](uint32_t u) { set_index(u); },
|
||||
[this](float f) { set_index(f); },
|
||||
[this](double d) { set_index(d); },
|
||||
[](std::monostate) {}
|
||||
}, val);
|
||||
}
|
||||
|
||||
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart) {
|
||||
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus, EventType::Update)) {
|
||||
set_gap(24.0f);
|
||||
set_flex_grow(0.0f);
|
||||
set_align_items(AlignItems::FlexStart);
|
||||
enable_focus();
|
||||
}
|
||||
|
||||
void Radio::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Focus:
|
||||
if (!options.empty()) {
|
||||
if (std::get<EventFocus>(e.variant).active) {
|
||||
blur();
|
||||
queue_child_focus();
|
||||
}
|
||||
if (focus_callback != nullptr) {
|
||||
focus_callback(std::get<EventFocus>(e.variant).active);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Update:
|
||||
if (child_focus_queued) {
|
||||
child_focus_queued = false;
|
||||
options[index]->focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Radio::~Radio() {
|
||||
@@ -82,13 +150,23 @@ namespace recompui {
|
||||
|
||||
void Radio::add_option(std::string_view name) {
|
||||
RadioOption *option = get_current_context().create_element<RadioOption>(this, name, uint32_t(options.size()));
|
||||
option->set_pressed_callback(std::bind(&Radio::option_selected, this, std::placeholders::_1));
|
||||
option->set_pressed_callback([this](uint32_t index){ options[index]->focus(); option_selected(index); });
|
||||
option->set_focus_callback([this](bool active) {
|
||||
if (focus_callback != nullptr) {
|
||||
focus_callback(active);
|
||||
}
|
||||
});
|
||||
options.emplace_back(option);
|
||||
|
||||
// The first option was added, select it.
|
||||
if (options.size() == 1) {
|
||||
set_index_internal(0, true, false);
|
||||
}
|
||||
// At least one other option already existed, so set up navigation.
|
||||
else {
|
||||
options[options.size() - 2]->set_nav(NavDirection::Right, options[options.size() - 1]);
|
||||
options[options.size() - 1]->set_nav(NavDirection::Left, options[options.size() - 2]);
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_index(uint32_t index) {
|
||||
@@ -103,4 +181,88 @@ namespace recompui {
|
||||
index_changed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
};
|
||||
void Radio::set_focus_callback(std::function<void(bool)> callback) {
|
||||
focus_callback = callback;
|
||||
}
|
||||
|
||||
void Radio::set_nav_auto(NavDirection dir) {
|
||||
Element::set_nav_auto(dir);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav_auto(dir);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav_auto(dir);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav_auto(dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_nav_none(NavDirection dir) {
|
||||
Element::set_nav_none(dir);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav_none(dir);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav_none(dir);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav_none(dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_nav(NavDirection dir, Element* element) {
|
||||
Element::set_nav(dir, element);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav(dir, element);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav(dir, element);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav(dir, element);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_nav_manual(NavDirection dir, const std::string& target) {
|
||||
Element::set_nav_manual(dir, target);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav_manual(dir, target);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav_manual(dir, target);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav_manual(dir, target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -8,13 +8,17 @@ namespace recompui {
|
||||
private:
|
||||
Style hover_style;
|
||||
Style checked_style;
|
||||
Style pulsing_style;
|
||||
std::function<void(uint32_t)> pressed_callback = nullptr;
|
||||
std::function<void(bool)> focus_callback = nullptr;
|
||||
uint32_t index = 0;
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "LabelRadioOption"; }
|
||||
public:
|
||||
RadioOption(Element *parent, std::string_view name, uint32_t index);
|
||||
void set_pressed_callback(std::function<void(uint32_t)> callback);
|
||||
void set_focus_callback(std::function<void(bool)> callback);
|
||||
void set_selected_state(bool enable);
|
||||
};
|
||||
|
||||
@@ -23,9 +27,17 @@ namespace recompui {
|
||||
std::vector<RadioOption *> options;
|
||||
uint32_t index = 0;
|
||||
std::vector<std::function<void(uint32_t)>> index_changed_callbacks;
|
||||
std::function<void(bool)> focus_callback = nullptr;
|
||||
bool child_focus_queued = false;
|
||||
|
||||
void set_index_internal(uint32_t index, bool setup, bool trigger_callbacks);
|
||||
void option_selected(uint32_t index);
|
||||
void set_input_value(const ElementValue& val) override;
|
||||
ElementValue get_element_value() override { return get_index(); }
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "LabelRadio"; }
|
||||
void queue_child_focus() { child_focus_queued = true; queue_update(); }
|
||||
public:
|
||||
Radio(Element *parent);
|
||||
virtual ~Radio();
|
||||
@@ -33,6 +45,14 @@ namespace recompui {
|
||||
void set_index(uint32_t index);
|
||||
uint32_t get_index() const;
|
||||
void add_index_changed_callback(std::function<void(uint32_t)> callback);
|
||||
void set_focus_callback(std::function<void(bool)> callback);
|
||||
size_t num_options() const { return options.size(); }
|
||||
RadioOption* get_option_element(size_t option_index) { return options[option_index]; }
|
||||
RadioOption* get_current_option_element() { return options.empty() ? nullptr : options[index]; }
|
||||
void set_nav_auto(NavDirection dir) override;
|
||||
void set_nav_none(NavDirection dir) override;
|
||||
void set_nav(NavDirection dir, Element* element) override;
|
||||
void set_nav_manual(NavDirection dir, const std::string& target) override;
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
} // namespace recompui
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace recompui {
|
||||
};
|
||||
|
||||
class ScrollContainer : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ScrollContainer"; }
|
||||
public:
|
||||
ScrollContainer(Element *parent, ScrollDirection direction);
|
||||
};
|
||||
|
||||
+118
-20
@@ -1,4 +1,6 @@
|
||||
#include "overloaded.h"
|
||||
#include "ui_slider.h"
|
||||
#include "../ui_utils.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <charconv>
|
||||
@@ -23,7 +25,7 @@ namespace recompui {
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::bar_clicked(float x, float) {
|
||||
void Slider::bar_pressed(float x, float) {
|
||||
update_value_from_mouse(x);
|
||||
}
|
||||
|
||||
@@ -44,29 +46,104 @@ namespace recompui {
|
||||
|
||||
void Slider::update_circle_position() {
|
||||
double ratio = std::clamp((value - min_value) / (max_value - min_value), 0.0, 1.0);
|
||||
circle_element->set_left(slider_width_dp * ratio);
|
||||
circle_element->set_left(ratio * 100.0, Unit::Percent);
|
||||
}
|
||||
|
||||
void Slider::update_label_text() {
|
||||
char text_buffer[32];
|
||||
int precision = type == SliderType::Double ? 1 : 0;
|
||||
auto result = std::to_chars(text_buffer, text_buffer + sizeof(text_buffer) - 1, value, std::chars_format::fixed, precision);
|
||||
if (result.ec == std::errc()) {
|
||||
if (type == SliderType::Percent) {
|
||||
*result.ptr = '%';
|
||||
result.ptr++;
|
||||
}
|
||||
|
||||
value_label->set_text(std::string(text_buffer, result.ptr));
|
||||
if (type == SliderType::Double) {
|
||||
std::snprintf(text_buffer, sizeof(text_buffer), "%.1f", value);
|
||||
} else if (type == SliderType::Percent) {
|
||||
std::snprintf(text_buffer, sizeof(text_buffer), "%d%%", static_cast<int>(value));
|
||||
} else {
|
||||
std::snprintf(text_buffer, sizeof(text_buffer), "%d", static_cast<int>(value));
|
||||
}
|
||||
|
||||
value_label->set_text(text_buffer);
|
||||
}
|
||||
|
||||
void Slider::set_input_value(const ElementValue& val) {
|
||||
std::visit(overloaded {
|
||||
[this](uint32_t u) { set_value(u); },
|
||||
[this](float f) { set_value(f); },
|
||||
[this](double d) { set_value(d); },
|
||||
[](std::monostate) {}
|
||||
}, val);
|
||||
}
|
||||
|
||||
void Slider::process_event(const Event& e) {
|
||||
switch (e.type) {
|
||||
case EventType::Focus:
|
||||
{
|
||||
bool active = std::get<EventFocus>(e.variant).active;
|
||||
circle_element->set_style_enabled(focus_state, active);
|
||||
if (active) {
|
||||
queue_update();
|
||||
}
|
||||
if (focus_callback != nullptr) {
|
||||
focus_callback(active);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Update:
|
||||
if (is_enabled()) {
|
||||
if (circle_element->is_style_enabled(focus_state)) {
|
||||
circle_element->set_background_color(recompui::get_pulse_color(750));
|
||||
queue_update();
|
||||
}
|
||||
else {
|
||||
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
|
||||
}
|
||||
}
|
||||
else {
|
||||
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
|
||||
}
|
||||
break;
|
||||
case EventType::Navigate:
|
||||
{
|
||||
NavDirection dir = std::get<EventNavigate>(e.variant).direction;
|
||||
if (dir == NavDirection::Left) {
|
||||
do_step(false);
|
||||
}
|
||||
else if (dir == NavDirection::Right) {
|
||||
do_step(true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Enable:
|
||||
{
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
circle_element->set_enabled(enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Slider::Slider(Element *parent, SliderType type) : Element(parent) {
|
||||
Slider::Slider(Element *parent, SliderType type) : Element(parent, Events(EventType::Focus, EventType::Update, EventType::Navigate, EventType::Enable)) {
|
||||
this->type = type;
|
||||
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_display(Display::Flex);
|
||||
set_flex(1.0f, 1.0f, 100.0f, Unit::Percent);
|
||||
set_flex_direction(FlexDirection::Row);
|
||||
set_text_align(TextAlign::Left);
|
||||
set_min_width(120.0f);
|
||||
|
||||
enable_focus();
|
||||
set_nav_none(NavDirection::Left);
|
||||
set_nav_none(NavDirection::Right);
|
||||
|
||||
ContextId context = get_current_context();
|
||||
|
||||
@@ -75,8 +152,10 @@ namespace recompui {
|
||||
value_label->set_min_width(60.0f);
|
||||
value_label->set_max_width(60.0f);
|
||||
|
||||
slider_element = context.create_element<Element>(this);
|
||||
slider_element->set_width(slider_width_dp);
|
||||
slider_element = context.create_element<Clickable>(this, true);
|
||||
slider_element->set_flex(1.0f, 0.0f);
|
||||
slider_element->add_pressed_callback([this](float x, float y){ bar_pressed(x, y); focus(); });
|
||||
slider_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
|
||||
|
||||
{
|
||||
bar_element = context.create_element<Clickable>(slider_element, true);
|
||||
@@ -84,19 +163,20 @@ namespace recompui {
|
||||
bar_element->set_height(2.0f);
|
||||
bar_element->set_margin_top(8.0f);
|
||||
bar_element->set_background_color(Color{ 255, 255, 255, 50 });
|
||||
bar_element->add_pressed_callback(std::bind(&Slider::bar_clicked, this, std::placeholders::_1, std::placeholders::_2));
|
||||
bar_element->add_dragged_callback(std::bind(&Slider::bar_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||
bar_element->add_pressed_callback([this](float x, float y){ bar_pressed(x, y); focus(); });
|
||||
bar_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
|
||||
|
||||
circle_element = context.create_element<Clickable>(slider_element, true);
|
||||
circle_element = context.create_element<Clickable>(bar_element, true);
|
||||
circle_element->set_position(Position::Relative);
|
||||
circle_element->set_width(16.0f);
|
||||
circle_element->set_height(16.0f);
|
||||
circle_element->set_margin_top(-8.0f);
|
||||
circle_element->set_margin_top(-7.0f);
|
||||
circle_element->set_margin_right(-8.0f);
|
||||
circle_element->set_margin_left(-8.0f);
|
||||
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
|
||||
circle_element->set_border_radius(8.0f);
|
||||
circle_element->add_dragged_callback(std::bind(&Slider::circle_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||
circle_element->add_pressed_callback([this](float, float){ focus(); });
|
||||
circle_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ circle_dragged(x, y, phase); focus(); });
|
||||
circle_element->set_cursor(Cursor::Pointer);
|
||||
}
|
||||
|
||||
@@ -142,4 +222,22 @@ namespace recompui {
|
||||
value_changed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
} // namespace recompui
|
||||
void Slider::set_focus_callback(std::function<void(bool)> callback) {
|
||||
focus_callback = callback;
|
||||
}
|
||||
|
||||
void Slider::do_step(bool increment) {
|
||||
double new_value = value;
|
||||
if (increment) {
|
||||
new_value += step_value;
|
||||
}
|
||||
else {
|
||||
new_value -= step_value;
|
||||
}
|
||||
new_value = std::clamp(new_value, min_value, max_value);
|
||||
if (new_value != value) {
|
||||
set_value_internal(new_value, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace recompui
|
||||
|
||||
@@ -15,23 +15,29 @@ namespace recompui {
|
||||
private:
|
||||
SliderType type = SliderType::Percent;
|
||||
Label *value_label = nullptr;
|
||||
Element *slider_element = nullptr;
|
||||
Clickable *slider_element = nullptr;
|
||||
Clickable *bar_element = nullptr;
|
||||
Clickable *circle_element = nullptr;
|
||||
double value = 50.0;
|
||||
double min_value = 0.0;
|
||||
double max_value = 100.0;
|
||||
double step_value = 0.0;
|
||||
float slider_width_dp = 300.0;
|
||||
std::vector<std::function<void(double)>> value_changed_callbacks;
|
||||
std::function<void(bool)> focus_callback = nullptr;
|
||||
|
||||
void set_value_internal(double v, bool setup, bool trigger_callbacks);
|
||||
void bar_clicked(float x, float y);
|
||||
void bar_pressed(float x, float y);
|
||||
void bar_dragged(float x, float y, DragPhase phase);
|
||||
void circle_dragged(float x, float y, DragPhase phase);
|
||||
void update_value_from_mouse(float x);
|
||||
void update_circle_position();
|
||||
void update_label_text();
|
||||
void set_input_value(const ElementValue& val) override;
|
||||
ElementValue get_element_value() override { return get_value(); }
|
||||
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Slider"; }
|
||||
|
||||
public:
|
||||
Slider(Element *parent, SliderType type);
|
||||
@@ -45,6 +51,8 @@ namespace recompui {
|
||||
void set_step_value(double v);
|
||||
double get_step_value() const;
|
||||
void add_value_changed_callback(std::function<void(double)> callback);
|
||||
void do_step(bool increment);
|
||||
void set_focus_callback(std::function<void(bool)> callback);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "ui_span.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Span::Span(Element *parent) : Element(parent, 0, "span", true) {
|
||||
set_font_style(FontStyle::Normal);
|
||||
}
|
||||
|
||||
Span::Span(Element *parent, const std::string &text) : Span(parent) {
|
||||
set_text(text);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
#include "ui_label.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class Span : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "Span"; }
|
||||
public:
|
||||
Span(Element *parent);
|
||||
Span(Element *parent, const std::string &text);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ui_style.h"
|
||||
#include "ui_element.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
@@ -169,6 +170,22 @@ namespace recompui {
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::PropertyId nav_to_property(NavDirection dir) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
return Rml::PropertyId::NavUp;
|
||||
case NavDirection::Right:
|
||||
return Rml::PropertyId::NavRight;
|
||||
case NavDirection::Down:
|
||||
return Rml::PropertyId::NavDown;
|
||||
case NavDirection::Left:
|
||||
return Rml::PropertyId::NavLeft;
|
||||
default:
|
||||
assert(false && "Unknown nav direction.");
|
||||
return Rml::PropertyId::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
|
||||
property_map[property_id] = property;
|
||||
}
|
||||
@@ -181,6 +198,17 @@ namespace recompui {
|
||||
|
||||
}
|
||||
|
||||
void Style::set_visibility(Visibility visibility) {
|
||||
switch (visibility) {
|
||||
case Visibility::Visible:
|
||||
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
|
||||
break;
|
||||
case Visibility::Hidden:
|
||||
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_position(Position position) {
|
||||
switch (position) {
|
||||
case Position::Absolute:
|
||||
@@ -401,7 +429,7 @@ namespace recompui {
|
||||
void Style::set_cursor(Cursor cursor) {
|
||||
switch (cursor) {
|
||||
case Cursor::None:
|
||||
assert(false && "Unimplemented.");
|
||||
set_property(Rml::PropertyId::Cursor, Rml::Property("", Rml::Unit::STRING));
|
||||
break;
|
||||
case Cursor::Pointer:
|
||||
set_property(Rml::PropertyId::Cursor, Rml::Property("pointer", Rml::Unit::STRING));
|
||||
@@ -460,6 +488,12 @@ namespace recompui {
|
||||
case FlexDirection::Column:
|
||||
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column);
|
||||
break;
|
||||
case FlexDirection::RowReverse:
|
||||
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::RowReverse);
|
||||
break;
|
||||
case FlexDirection::ColumnReverse:
|
||||
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::ColumnReverse);
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown flex direction.");
|
||||
break;
|
||||
@@ -545,5 +579,34 @@ namespace recompui {
|
||||
void Style::set_font_family(std::string_view family) {
|
||||
set_property(Rml::PropertyId::FontFamily, Rml::Property(Rml::String{ family }, Rml::Unit::UNKNOWN));
|
||||
}
|
||||
|
||||
void Style::set_nav_auto(NavDirection dir) {
|
||||
set_property(nav_to_property(dir), Rml::Style::Nav::Auto);
|
||||
}
|
||||
|
||||
void Style::set_nav_none(NavDirection dir) {
|
||||
set_property(nav_to_property(dir), Rml::Style::Nav::None);
|
||||
}
|
||||
|
||||
void Style::set_nav(NavDirection dir, Element* element) {
|
||||
set_property(nav_to_property(dir), Rml::Property(Rml::String{ "#" + element->get_id() }, Rml::Unit::STRING));
|
||||
}
|
||||
|
||||
void Style::set_nav_manual(NavDirection dir, const std::string& target) {
|
||||
set_property(nav_to_property(dir), Rml::Property(target, Rml::Unit::STRING));
|
||||
}
|
||||
|
||||
void Style::set_tab_index_auto() {
|
||||
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::Auto);
|
||||
}
|
||||
|
||||
void Style::set_tab_index_none() {
|
||||
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::None);
|
||||
}
|
||||
|
||||
void Style::set_focusable(bool focusable) {
|
||||
set_property(Rml::PropertyId::Focus, focusable ? Rml::Style::Focus::Auto : Rml::Style::Focus::None);
|
||||
}
|
||||
|
||||
|
||||
} // namespace recompui
|
||||
@@ -20,6 +20,7 @@ namespace recompui {
|
||||
public:
|
||||
Style();
|
||||
virtual ~Style();
|
||||
void set_visibility(Visibility visibility);
|
||||
void set_position(Position position);
|
||||
void set_left(float left, Unit unit = Unit::Dp);
|
||||
void set_top(float top, Unit unit = Unit::Dp);
|
||||
@@ -93,6 +94,13 @@ namespace recompui {
|
||||
void set_drag(Drag drag);
|
||||
void set_tab_index(TabIndex focus);
|
||||
void set_font_family(std::string_view family);
|
||||
virtual void set_nav_auto(NavDirection dir);
|
||||
virtual void set_nav_none(NavDirection dir);
|
||||
virtual void set_nav(NavDirection dir, Element* element);
|
||||
virtual void set_nav_manual(NavDirection dir, const std::string& target);
|
||||
void set_tab_index_auto();
|
||||
void set_tab_index_none();
|
||||
void set_focusable(bool focusable);
|
||||
virtual bool is_element() { return false; }
|
||||
ResourceId get_resource_id() { return resource_id; }
|
||||
};
|
||||
|
||||
@@ -16,17 +16,30 @@ namespace recompui {
|
||||
|
||||
break;
|
||||
}
|
||||
case EventType::Focus: {
|
||||
const EventFocus &event = std::get<EventFocus>(e.variant);
|
||||
if (focus_callback != nullptr) {
|
||||
focus_callback(event.active);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TextInput::TextInput(Element *parent) : Element(parent, Events(EventType::Text), "input") {
|
||||
TextInput::TextInput(Element *parent, bool text_visible) : Element(parent, Events(EventType::Text, EventType::Focus), "input") {
|
||||
if (!text_visible) {
|
||||
set_attribute("type", "password");
|
||||
}
|
||||
set_min_width(60.0f);
|
||||
set_max_width(400.0f);
|
||||
set_border_color(Color{ 242, 242, 242, 255 });
|
||||
set_border_bottom_width(1.0f);
|
||||
set_padding_bottom(6.0f);
|
||||
set_focusable(true);
|
||||
set_nav_auto(NavDirection::Up);
|
||||
set_nav_auto(NavDirection::Down);
|
||||
set_tab_index_auto();
|
||||
}
|
||||
|
||||
void TextInput::set_text(std::string_view text) {
|
||||
@@ -42,4 +55,7 @@ namespace recompui {
|
||||
text_changed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
};
|
||||
void TextInput::set_focus_callback(std::function<void(bool)> callback) {
|
||||
focus_callback = callback;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,13 +8,16 @@ namespace recompui {
|
||||
private:
|
||||
std::string text;
|
||||
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
|
||||
std::function<void(bool)> focus_callback = nullptr;
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "TextInput"; }
|
||||
public:
|
||||
TextInput(Element *parent);
|
||||
TextInput(Element *parent, bool text_visible = true);
|
||||
void set_text(std::string_view text);
|
||||
const std::string &get_text();
|
||||
void add_text_changed_callback(std::function<void(const std::string &)> callback);
|
||||
void set_focus_callback(std::function<void(bool)> callback);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
|
||||
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
|
||||
enable_focus();
|
||||
|
||||
set_width(162.0f);
|
||||
set_height(72.0f);
|
||||
set_border_radius(36.0f);
|
||||
@@ -18,13 +20,19 @@ namespace recompui {
|
||||
checked_style.set_border_color(Color{ 34, 177, 76, 255 });
|
||||
hover_style.set_border_color(Color{ 177, 76, 34, 255 });
|
||||
hover_style.set_background_color(Color{ 206, 120, 68, 76 });
|
||||
focus_style.set_border_color(Color{ 177, 76, 34, 255 });
|
||||
focus_style.set_background_color(Color{ 206, 120, 68, 76 });
|
||||
checked_hover_style.set_border_color(Color{ 34, 177, 76, 255 });
|
||||
checked_hover_style.set_background_color(Color{ 68, 206, 120, 76 });
|
||||
checked_focus_style.set_border_color(Color{ 34, 177, 76, 255 });
|
||||
checked_focus_style.set_background_color(Color{ 68, 206, 120, 76 });
|
||||
disabled_style.set_border_color(Color{ 177, 76, 34, 128 });
|
||||
checked_disabled_style.set_border_color(Color{ 34, 177, 76, 128 });
|
||||
add_style(&checked_style, checked_state);
|
||||
add_style(&hover_style, hover_state);
|
||||
add_style(&focus_style, focus_state);
|
||||
add_style(&checked_hover_style, { checked_state, hover_state });
|
||||
add_style(&checked_focus_style, { checked_state, focus_state });
|
||||
add_style(&disabled_style, disabled_state);
|
||||
add_style(&checked_disabled_style, { checked_state, disabled_state });
|
||||
|
||||
@@ -85,15 +93,28 @@ namespace recompui {
|
||||
|
||||
break;
|
||||
case EventType::Hover: {
|
||||
bool hover_active = std::get<EventHover>(e.variant).active;
|
||||
bool hover_active = std::get<EventHover>(e.variant).active && is_enabled();
|
||||
set_style_enabled(hover_state, hover_active);
|
||||
floater->set_style_enabled(hover_state, hover_active);
|
||||
break;
|
||||
}
|
||||
case EventType::Focus: {
|
||||
bool focus_active = std::get<EventFocus>(e.variant).active;
|
||||
set_style_enabled(focus_state, focus_active);
|
||||
break;
|
||||
}
|
||||
case EventType::Enable: {
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
set_style_enabled(disabled_state, !enable_active);
|
||||
floater->set_style_enabled(disabled_state, !enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EventType::Update: {
|
||||
|
||||
@@ -12,7 +12,9 @@ namespace recompui {
|
||||
std::list<std::function<void(bool)>> checked_callbacks;
|
||||
Style checked_style;
|
||||
Style hover_style;
|
||||
Style focus_style;
|
||||
Style checked_hover_style;
|
||||
Style checked_focus_style;
|
||||
Style disabled_style;
|
||||
Style checked_disabled_style;
|
||||
Style floater_checked_style;
|
||||
@@ -25,6 +27,7 @@ namespace recompui {
|
||||
|
||||
// Element overrides.
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Toggle"; }
|
||||
public:
|
||||
Toggle(Element *parent);
|
||||
void set_checked(bool checked);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace recompui {
|
||||
|
||||
constexpr std::string_view checked_state = "checked";
|
||||
constexpr std::string_view hover_state = "hover";
|
||||
constexpr std::string_view focus_state = "focus";
|
||||
constexpr std::string_view disabled_state = "disabled";
|
||||
|
||||
struct Color {
|
||||
@@ -21,6 +22,7 @@ namespace recompui {
|
||||
Pointer
|
||||
};
|
||||
|
||||
// These two enums must be kept in sync with patches/recompui_event_structs.h!
|
||||
enum class EventType {
|
||||
None,
|
||||
Click,
|
||||
@@ -30,6 +32,8 @@ namespace recompui {
|
||||
Drag,
|
||||
Text,
|
||||
Update,
|
||||
Navigate,
|
||||
MouseButton,
|
||||
Count
|
||||
};
|
||||
|
||||
@@ -40,6 +44,20 @@ namespace recompui {
|
||||
End
|
||||
};
|
||||
|
||||
enum class NavDirection {
|
||||
Up,
|
||||
Right,
|
||||
Down,
|
||||
Left
|
||||
};
|
||||
|
||||
enum class MouseButton {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
Count
|
||||
};
|
||||
|
||||
template <typename Enum, typename = std::enable_if_t<std::is_enum_v<Enum>>>
|
||||
constexpr uint32_t Events(Enum first) {
|
||||
return 1u << static_cast<uint32_t>(first);
|
||||
@@ -77,7 +95,18 @@ namespace recompui {
|
||||
std::string text;
|
||||
};
|
||||
|
||||
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, std::monostate>;
|
||||
struct EventNavigate {
|
||||
NavDirection direction;
|
||||
};
|
||||
|
||||
struct EventMouseButton {
|
||||
float x;
|
||||
float y;
|
||||
MouseButton button;
|
||||
bool pressed;
|
||||
};
|
||||
|
||||
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, EventMouseButton, std::monostate>;
|
||||
|
||||
struct Event {
|
||||
EventType type;
|
||||
@@ -132,6 +161,20 @@ namespace recompui {
|
||||
e.variant = std::monostate{};
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event navigate_event(NavDirection direction) {
|
||||
Event e;
|
||||
e.type = EventType::Navigate;
|
||||
e.variant = EventNavigate{ direction };
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event mousebutton_event(float x, float y, MouseButton button, bool pressed) {
|
||||
Event e;
|
||||
e.type = EventType::MouseButton;
|
||||
e.variant = EventMouseButton{ x, y, button, pressed };
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
enum class Display {
|
||||
@@ -151,6 +194,11 @@ namespace recompui {
|
||||
TableCell
|
||||
};
|
||||
|
||||
enum class Visibility {
|
||||
Visible,
|
||||
Hidden
|
||||
};
|
||||
|
||||
enum class Position {
|
||||
Absolute,
|
||||
Relative
|
||||
@@ -167,7 +215,9 @@ namespace recompui {
|
||||
|
||||
enum class FlexDirection {
|
||||
Row,
|
||||
Column
|
||||
Column,
|
||||
RowReverse,
|
||||
ColumnReverse
|
||||
};
|
||||
|
||||
enum class AlignItems {
|
||||
|
||||
+335
-84
@@ -1,5 +1,8 @@
|
||||
#include "recomp_ui.h"
|
||||
|
||||
#include "ui_helpers.h"
|
||||
#include "ui_api_images.h"
|
||||
|
||||
#include "core/ui_context.h"
|
||||
#include "core/ui_resource.h"
|
||||
|
||||
@@ -12,6 +15,7 @@
|
||||
#include "elements/ui_radio.h"
|
||||
#include "elements/ui_scroll_container.h"
|
||||
#include "elements/ui_slider.h"
|
||||
#include "elements/ui_span.h"
|
||||
#include "elements/ui_style.h"
|
||||
#include "elements/ui_text_input.h"
|
||||
#include "elements/ui_toggle.h"
|
||||
@@ -19,92 +23,11 @@
|
||||
|
||||
#include "librecomp/overlays.hpp"
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "librecomp/addresses.hpp"
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
|
||||
using namespace recompui;
|
||||
|
||||
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
|
||||
|
||||
// Helpers
|
||||
|
||||
ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
|
||||
return ContextId{ .slot_id = context_id };
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
std::string arg_string(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(char) str = _arg<arg_index, PTR(char)>(rdram, ctx);
|
||||
|
||||
// Get the length of the byteswapped string.
|
||||
size_t len = 0;
|
||||
while (MEM_B(str, len) != 0x00) {
|
||||
len++;
|
||||
}
|
||||
|
||||
std::string ret{};
|
||||
ret.reserve(len + 1);
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
ret += (char)MEM_B(str, i);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
|
||||
|
||||
return ResourceId{ .slot_id = slot_id };
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
|
||||
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
|
||||
|
||||
if (resource == ResourceId::null()) {
|
||||
return nullptr;
|
||||
}
|
||||
else if (resource == root_element_id) {
|
||||
return ui_context.get_root_element();
|
||||
}
|
||||
|
||||
return resource.as_element();
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
|
||||
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
|
||||
|
||||
if (resource == ResourceId::null()) {
|
||||
return nullptr;
|
||||
}
|
||||
else if (resource == root_element_id) {
|
||||
ContextId ui_context = recompui::get_current_context();
|
||||
return ui_context.get_root_element();
|
||||
}
|
||||
|
||||
return *resource;
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
|
||||
|
||||
Color ret{};
|
||||
|
||||
ret.r = MEM_B(0, color_arg);
|
||||
ret.g = MEM_B(1, color_arg);
|
||||
ret.b = MEM_B(2, color_arg);
|
||||
ret.a = MEM_B(3, color_arg);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void return_resource(recomp_context* ctx, ResourceId resource) {
|
||||
_return<uint32_t>(ctx, resource.slot_id);
|
||||
}
|
||||
|
||||
// Contexts
|
||||
void recompui_create_context(uint8_t* rdram, recomp_context* ctx) {
|
||||
(void)rdram;
|
||||
@@ -144,6 +67,20 @@ void recompui_hide_context(uint8_t* rdram, recomp_context* ctx) {
|
||||
recompui::hide_context(ui_context);
|
||||
}
|
||||
|
||||
void recompui_set_context_captures_input(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
bool captures_input = _arg<1, int>(rdram, ctx) != 0;
|
||||
|
||||
ui_context.set_captures_input(captures_input);
|
||||
}
|
||||
|
||||
void recompui_set_context_captures_mouse(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
bool captures_mouse = _arg<1, int>(rdram, ctx) != 0;
|
||||
|
||||
ui_context.set_captures_mouse(captures_mouse);
|
||||
}
|
||||
|
||||
// Resources
|
||||
void recompui_create_style(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
@@ -160,17 +97,110 @@ void recompui_create_element(uint8_t* rdram, recomp_context* ctx) {
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_destroy_element(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* parent_resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (!parent_resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to remove child from non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* parent = static_cast<Element*>(parent_resource);
|
||||
ResourceId to_remove = arg_resource_id<1>(rdram, ctx);
|
||||
|
||||
if (!parent->remove_child(to_remove)) {
|
||||
recompui::message_box("Fatal error in mod - attempted to remove child from wrong parent");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
}
|
||||
|
||||
void recompui_create_button(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
std::string text = arg_string<2>(rdram, ctx);
|
||||
std::string text = _arg_string<2>(rdram, ctx);
|
||||
uint32_t style = _arg<3, uint32_t>(rdram, ctx);
|
||||
|
||||
Button* ret = ui_context.create_element<Button>(parent, text, static_cast<ButtonStyle>(style));
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_create_label(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
std::string text = _arg_string<2>(rdram, ctx);
|
||||
uint32_t style = _arg<3, uint32_t>(rdram, ctx);
|
||||
|
||||
Element* ret = ui_context.create_element<Label>(parent, text, static_cast<LabelStyle>(style));
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_create_span(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
std::string text = _arg_string<2>(rdram, ctx);
|
||||
|
||||
Element* ret = ui_context.create_element<Span>(parent, text);
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_create_textinput(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
|
||||
Element* ret = ui_context.create_element<TextInput>(parent);
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_create_passwordinput(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
|
||||
Element* ret = ui_context.create_element<TextInput>(parent, false);
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_create_labelradio(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
PTR(PTR(char)) options = _arg<2, PTR(PTR(char))>(rdram, ctx);
|
||||
uint32_t num_options = _arg<3, uint32_t>(rdram, ctx);
|
||||
|
||||
Radio* ret = ui_context.create_element<Radio>(parent);
|
||||
|
||||
for (size_t i = 0; i < num_options; i++) {
|
||||
ret->add_option(decode_string(rdram, MEM_W(sizeof(uint32_t) * i, options)));
|
||||
}
|
||||
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_create_slider(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
uint32_t type = _arg<2, uint32_t>(rdram, ctx);
|
||||
float min_value = arg_float3(rdram, ctx);
|
||||
float max_value = arg_float4(rdram, ctx);
|
||||
float step = arg_float5(rdram, ctx);
|
||||
float initial_value = arg_float6(rdram, ctx);
|
||||
|
||||
Slider* ret = ui_context.create_element<Slider>(parent, static_cast<SliderType>(type));
|
||||
ret->set_min_value(min_value);
|
||||
ret->set_max_value(max_value);
|
||||
ret->set_step_value(step);
|
||||
ret->set_value(initial_value);
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
// Position and Layout
|
||||
void recompui_set_visibility(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
uint32_t visibility = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
resource->set_visibility(static_cast<Visibility>(visibility));
|
||||
}
|
||||
|
||||
void recompui_set_position(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
uint32_t position = _arg<1, uint32_t>(rdram, ctx);
|
||||
@@ -610,6 +640,19 @@ void recompui_set_overflow_y(uint8_t* rdram, recomp_context* ctx) {
|
||||
}
|
||||
|
||||
// Text and Fonts
|
||||
void recompui_set_text(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set text of non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
element->set_text(_arg_string<1>(rdram, ctx));
|
||||
}
|
||||
|
||||
void recompui_set_font_size(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
float size = _arg_float_a1(rdram, ctx);
|
||||
@@ -696,6 +739,192 @@ void recompui_set_tab_index(uint8_t* rdram, recomp_context* ctx) {
|
||||
resource->set_tab_index(static_cast<TabIndex>(tab_index));
|
||||
}
|
||||
|
||||
// Values
|
||||
void recompui_get_input_value_u32(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to get value of non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
_return<uint32_t>(ctx, element->get_input_value_u32());
|
||||
}
|
||||
|
||||
void recompui_get_input_value_float(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to get value of non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
_return<float>(ctx, element->get_input_value_float());
|
||||
}
|
||||
|
||||
void recompui_get_input_text(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to get input text of non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
std::string ret = element->get_input_text();
|
||||
return_string(rdram, ctx, ret);
|
||||
}
|
||||
|
||||
void recompui_set_input_value_u32(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
uint32_t value = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set value of non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
element->set_input_value_u32(value);
|
||||
}
|
||||
|
||||
void recompui_set_input_value_float(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
float value = _arg_float_a1(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set value of non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
element->set_input_value_float(value);
|
||||
}
|
||||
|
||||
void recompui_set_input_text(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set input text of non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
element->set_input_text(_arg_string<1>(rdram, ctx));
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
void recompui_register_callback(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = recompui::get_current_context();
|
||||
|
||||
if (ui_context == ContextId::null()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to register callback with no active context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to register callback on non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
PTR(void) callback = _arg<1, PTR(void)>(rdram, ctx);
|
||||
PTR(void) userdata = _arg<2, PTR(void)>(rdram, ctx);
|
||||
|
||||
element->register_callback(ui_context, callback, userdata);
|
||||
}
|
||||
|
||||
// Navigation
|
||||
void recompui_set_nav_auto(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = recompui::get_current_context();
|
||||
|
||||
if (ui_context == ContextId::null()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
u32 nav_dir = _arg<1, u32>(rdram, ctx);
|
||||
|
||||
element->set_nav_auto(static_cast<recompui::NavDirection>(nav_dir));
|
||||
}
|
||||
|
||||
void recompui_set_nav_none(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = recompui::get_current_context();
|
||||
|
||||
if (ui_context == ContextId::null()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
u32 nav_dir = _arg<1, u32>(rdram, ctx);
|
||||
|
||||
element->set_nav_none(static_cast<recompui::NavDirection>(nav_dir));
|
||||
}
|
||||
|
||||
void recompui_set_nav(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = recompui::get_current_context();
|
||||
|
||||
if (ui_context == ContextId::null()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set element navigation with no active context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
|
||||
if (resource == nullptr || !resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set navigation on non-element or element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Style* target_resource = arg_style<2>(rdram, ctx);
|
||||
|
||||
if (target_resource == nullptr || !target_resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set element navigation to non-element or target element not found in context");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
Element* target_element = static_cast<Element*>(target_resource);
|
||||
u32 nav_dir = _arg<1, u32>(rdram, ctx);
|
||||
|
||||
element->set_nav(static_cast<recompui::NavDirection>(nav_dir), target_element);
|
||||
}
|
||||
|
||||
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
|
||||
|
||||
void recompui::register_ui_exports() {
|
||||
@@ -705,9 +934,19 @@ void recompui::register_ui_exports() {
|
||||
REGISTER_FUNC(recompui_context_root);
|
||||
REGISTER_FUNC(recompui_show_context);
|
||||
REGISTER_FUNC(recompui_hide_context);
|
||||
REGISTER_FUNC(recompui_set_context_captures_input);
|
||||
REGISTER_FUNC(recompui_set_context_captures_mouse);
|
||||
REGISTER_FUNC(recompui_create_style);
|
||||
REGISTER_FUNC(recompui_create_element);
|
||||
REGISTER_FUNC(recompui_destroy_element);
|
||||
REGISTER_FUNC(recompui_create_button);
|
||||
REGISTER_FUNC(recompui_create_label);
|
||||
// REGISTER_FUNC(recompui_create_span);
|
||||
REGISTER_FUNC(recompui_create_textinput);
|
||||
REGISTER_FUNC(recompui_create_passwordinput);
|
||||
REGISTER_FUNC(recompui_create_labelradio);
|
||||
REGISTER_FUNC(recompui_create_slider);
|
||||
REGISTER_FUNC(recompui_set_visibility);
|
||||
REGISTER_FUNC(recompui_set_position);
|
||||
REGISTER_FUNC(recompui_set_left);
|
||||
REGISTER_FUNC(recompui_set_top);
|
||||
@@ -766,6 +1005,7 @@ void recompui::register_ui_exports() {
|
||||
REGISTER_FUNC(recompui_set_overflow);
|
||||
REGISTER_FUNC(recompui_set_overflow_x);
|
||||
REGISTER_FUNC(recompui_set_overflow_y);
|
||||
REGISTER_FUNC(recompui_set_text);
|
||||
REGISTER_FUNC(recompui_set_font_size);
|
||||
REGISTER_FUNC(recompui_set_letter_spacing);
|
||||
REGISTER_FUNC(recompui_set_line_height);
|
||||
@@ -777,4 +1017,15 @@ void recompui::register_ui_exports() {
|
||||
REGISTER_FUNC(recompui_set_column_gap);
|
||||
REGISTER_FUNC(recompui_set_drag);
|
||||
REGISTER_FUNC(recompui_set_tab_index);
|
||||
REGISTER_FUNC(recompui_get_input_value_u32);
|
||||
REGISTER_FUNC(recompui_get_input_value_float);
|
||||
REGISTER_FUNC(recompui_get_input_text);
|
||||
REGISTER_FUNC(recompui_set_input_value_u32);
|
||||
REGISTER_FUNC(recompui_set_input_value_float);
|
||||
REGISTER_FUNC(recompui_set_input_text);
|
||||
REGISTER_FUNC(recompui_set_nav_auto);
|
||||
REGISTER_FUNC(recompui_set_nav_none);
|
||||
REGISTER_FUNC(recompui_set_nav);
|
||||
REGISTER_FUNC(recompui_register_callback);
|
||||
register_ui_image_exports();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "concurrentqueue.h"
|
||||
|
||||
#include "overloaded.h"
|
||||
#include "recomp_ui.h"
|
||||
|
||||
#include "core/ui_context.h"
|
||||
#include "core/ui_resource.h"
|
||||
|
||||
#include "elements/ui_element.h"
|
||||
#include "elements/ui_button.h"
|
||||
#include "elements/ui_clickable.h"
|
||||
#include "elements/ui_container.h"
|
||||
#include "elements/ui_image.h"
|
||||
#include "elements/ui_label.h"
|
||||
#include "elements/ui_radio.h"
|
||||
#include "elements/ui_scroll_container.h"
|
||||
#include "elements/ui_slider.h"
|
||||
#include "elements/ui_style.h"
|
||||
#include "elements/ui_text_input.h"
|
||||
#include "elements/ui_toggle.h"
|
||||
#include "elements/ui_types.h"
|
||||
|
||||
#include "librecomp/overlays.hpp"
|
||||
#include "librecomp/helpers.hpp"
|
||||
|
||||
#include "../patches/ui_funcs.h"
|
||||
|
||||
struct QueuedCallback {
|
||||
recompui::ResourceId resource;
|
||||
recompui::Event event;
|
||||
recompui::UICallback callback;
|
||||
};
|
||||
|
||||
moodycamel::ConcurrentQueue<QueuedCallback> queued_callbacks{};
|
||||
|
||||
void recompui::queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback) {
|
||||
queued_callbacks.enqueue(QueuedCallback{ .resource = resource, .event = e, .callback = callback });
|
||||
}
|
||||
|
||||
bool convert_event(const recompui::Event& in, RecompuiEventData& out) {
|
||||
bool skip = false;
|
||||
out = {};
|
||||
out.type = static_cast<RecompuiEventType>(in.type);
|
||||
|
||||
switch (in.type) {
|
||||
default:
|
||||
case recompui::EventType::None:
|
||||
case recompui::EventType::Count:
|
||||
skip = true;
|
||||
break;
|
||||
case recompui::EventType::Click:
|
||||
{
|
||||
const recompui::EventClick &click = std::get<recompui::EventClick>(in.variant);
|
||||
out.data.click.x = click.x;
|
||||
out.data.click.y = click.y;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Focus:
|
||||
{
|
||||
const recompui::EventFocus &focus = std::get<recompui::EventFocus>(in.variant);
|
||||
out.data.focus.active = focus.active;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Hover:
|
||||
{
|
||||
const recompui::EventHover &hover = std::get<recompui::EventHover>(in.variant);
|
||||
out.data.hover.active = hover.active;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Enable:
|
||||
{
|
||||
const recompui::EventEnable &enable = std::get<recompui::EventEnable>(in.variant);
|
||||
out.data.enable.active = enable.active;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Drag:
|
||||
{
|
||||
const recompui::EventDrag &drag = std::get<recompui::EventDrag>(in.variant);
|
||||
out.data.drag.phase = static_cast<RecompuiDragPhase>(drag.phase);
|
||||
out.data.drag.x = drag.x;
|
||||
out.data.drag.y = drag.y;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Text:
|
||||
skip = true; // Text events aren't supported in the UI mod API.
|
||||
break;
|
||||
case recompui::EventType::Update:
|
||||
// No data for an update event.
|
||||
break;
|
||||
}
|
||||
|
||||
return !skip;
|
||||
}
|
||||
|
||||
extern "C" void recomp_run_ui_callbacks(uint8_t* rdram, recomp_context* ctx) {
|
||||
// Allocate the event on the stack.
|
||||
gpr stack_frame = ctx->r29;
|
||||
ctx->r29 -= sizeof(RecompuiEventData);
|
||||
RecompuiEventData* event_data = TO_PTR(RecompuiEventData, stack_frame);
|
||||
|
||||
QueuedCallback cur_callback;
|
||||
|
||||
while (queued_callbacks.try_dequeue(cur_callback)) {
|
||||
if (convert_event(cur_callback.event, *event_data)) {
|
||||
recompui::ContextId cur_context = cur_callback.callback.context;
|
||||
cur_context.open();
|
||||
|
||||
ctx->r4 = static_cast<int32_t>(cur_callback.resource.slot_id);
|
||||
ctx->r5 = stack_frame;
|
||||
ctx->r6 = cur_callback.callback.userdata;
|
||||
|
||||
LOOKUP_FUNC(cur_callback.callback.callback)(rdram, ctx);
|
||||
cur_context.close();
|
||||
}
|
||||
}
|
||||
|
||||
ctx->r29 += sizeof(RecompuiEventData);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "recomp_ui.h"
|
||||
#include "librecomp/overlays.hpp"
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
|
||||
#include "ui_helpers.h"
|
||||
#include "ui_api_images.h"
|
||||
#include "elements/ui_image.h"
|
||||
|
||||
using namespace recompui;
|
||||
|
||||
struct {
|
||||
std::mutex mutex;
|
||||
std::unordered_set<uint32_t> textures{};
|
||||
uint32_t textures_created = 0;
|
||||
} TextureState;
|
||||
|
||||
const std::string mod_texture_prefix = "?/mod_api/";
|
||||
|
||||
static std::string get_texture_name(uint32_t texture_id) {
|
||||
return mod_texture_prefix + std::to_string(texture_id);
|
||||
}
|
||||
|
||||
static uint32_t get_new_texture_id() {
|
||||
std::lock_guard lock{TextureState.mutex};
|
||||
uint32_t cur_id = TextureState.textures_created++;
|
||||
TextureState.textures.emplace(cur_id);
|
||||
|
||||
return cur_id;
|
||||
}
|
||||
|
||||
static void release_texture(uint32_t texture_id) {
|
||||
std::string texture_name = get_texture_name(texture_id);
|
||||
std::lock_guard lock{TextureState.mutex};
|
||||
|
||||
if (TextureState.textures.erase(texture_id) == 0) {
|
||||
recompui::message_box("Fatal error in mod - attempted to destroy texture that doesn't exist!");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
recompui::release_image(texture_name);
|
||||
}
|
||||
|
||||
thread_local std::vector<char> swapped_image_bytes;
|
||||
|
||||
void recompui_create_texture_rgba32(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
|
||||
uint32_t width = _arg<1, uint32_t>(rdram, ctx);
|
||||
uint32_t height = _arg<2, uint32_t>(rdram, ctx);
|
||||
uint32_t cur_id = get_new_texture_id();
|
||||
|
||||
// The size in bytes of the image's pixel data.
|
||||
size_t size_bytes = width * height * 4 * sizeof(uint8_t);
|
||||
swapped_image_bytes.resize(size_bytes);
|
||||
|
||||
// Byteswap copy the pixel data.
|
||||
for (size_t i = 0; i < size_bytes; i++) {
|
||||
swapped_image_bytes[i] = MEM_B(i, data_in);
|
||||
}
|
||||
|
||||
// Create a texture name from the ID and queue its bytes.
|
||||
std::string texture_name = get_texture_name(cur_id);
|
||||
recompui::queue_image_from_bytes_rgba32(texture_name, swapped_image_bytes, width, height);
|
||||
|
||||
// Return the new texture ID.
|
||||
_return(ctx, cur_id);
|
||||
}
|
||||
|
||||
void recompui_create_texture_image_bytes(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
|
||||
uint32_t size_bytes = _arg<1, u32>(rdram, ctx);
|
||||
uint32_t cur_id = get_new_texture_id();
|
||||
|
||||
// The size in bytes of the image's data.
|
||||
swapped_image_bytes.resize(size_bytes);
|
||||
|
||||
// Byteswap copy the image's data.
|
||||
for (size_t i = 0; i < size_bytes; i++) {
|
||||
swapped_image_bytes[i] = MEM_B(i, data_in);
|
||||
}
|
||||
|
||||
// Create a texture name from the ID and queue its bytes.
|
||||
std::string texture_name = get_texture_name(cur_id);
|
||||
recompui::queue_image_from_bytes_file(texture_name, swapped_image_bytes);
|
||||
|
||||
// Return the new texture ID.
|
||||
_return(ctx, cur_id);
|
||||
}
|
||||
|
||||
void recompui_destroy_texture(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t texture_id = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
release_texture(texture_id);
|
||||
}
|
||||
|
||||
void recompui_create_imageview(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
uint32_t texture_id = _arg<2, uint32_t>(rdram, ctx);
|
||||
|
||||
Element* ret = ui_context.create_element<Image>(parent, get_texture_name(texture_id));
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_set_imageview_texture(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
uint32_t texture_id = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
if (!resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set texture of non-element");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
element->set_src(get_texture_name(texture_id));
|
||||
}
|
||||
|
||||
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
|
||||
|
||||
void recompui::register_ui_image_exports() {
|
||||
REGISTER_FUNC(recompui_create_texture_rgba32);
|
||||
REGISTER_FUNC(recompui_create_texture_image_bytes);
|
||||
REGISTER_FUNC(recompui_destroy_texture);
|
||||
REGISTER_FUNC(recompui_create_imageview);
|
||||
REGISTER_FUNC(recompui_set_imageview_texture);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef __UI_API_IMAGES_H__
|
||||
#define __UI_API_IMAGES_H__
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace recompui {
|
||||
void register_ui_image_exports();
|
||||
}
|
||||
|
||||
#endif
|
||||
+99
-60
@@ -4,6 +4,7 @@
|
||||
#include "banjo_config.h"
|
||||
#include "banjo_debug.h"
|
||||
#include "banjo_render.h"
|
||||
#include "banjo_support.h"
|
||||
#include "promptfont.h"
|
||||
#include "ultramodern/config.hpp"
|
||||
#include "ultramodern/ultramodern.hpp"
|
||||
@@ -21,6 +22,26 @@ Rml::DataModelHandle sound_options_model_handle;
|
||||
// True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
|
||||
bool configuring_controller = false;
|
||||
|
||||
int recompui::config_tab_to_index(recompui::ConfigTab tab) {
|
||||
switch (tab) {
|
||||
case recompui::ConfigTab::General:
|
||||
return 0;
|
||||
case recompui::ConfigTab::Controls:
|
||||
return 1;
|
||||
case recompui::ConfigTab::Graphics:
|
||||
return 2;
|
||||
case recompui::ConfigTab::Sound:
|
||||
return 3;
|
||||
case recompui::ConfigTab::Mods:
|
||||
return 4;
|
||||
case recompui::ConfigTab::Debug:
|
||||
return 5;
|
||||
default:
|
||||
assert(false && "Unknown config tab.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void get_option(const T& input, Rml::Variant& output) {
|
||||
std::string value = "";
|
||||
@@ -164,7 +185,7 @@ void apply_graphics_config(void) {
|
||||
|
||||
void close_config_menu() {
|
||||
if (ultramodern::renderer::get_graphics_config() != new_options) {
|
||||
recompui::open_prompt(
|
||||
recompui::open_choice_prompt(
|
||||
"Graphics options have changed",
|
||||
"Would you like to apply or discard the changes?",
|
||||
"Apply",
|
||||
@@ -191,7 +212,7 @@ void close_config_menu() {
|
||||
}
|
||||
|
||||
void banjo::open_quit_game_prompt() {
|
||||
recompui::open_prompt(
|
||||
recompui::open_choice_prompt(
|
||||
"Are you sure you want to quit?",
|
||||
"Any progress since your last save will be lost.",
|
||||
"Quit",
|
||||
@@ -376,7 +397,45 @@ recompui::ContextId recompui::get_config_context_id() {
|
||||
return config_context;
|
||||
}
|
||||
|
||||
// Helper copied from RmlUi to get a named child.
|
||||
Rml::Element* recompui::get_child_by_tag(Rml::Element* parent, const std::string& tag)
|
||||
{
|
||||
// Look for the existing child
|
||||
for (int i = 0; i < parent->GetNumChildren(); i++)
|
||||
{
|
||||
Rml::Element* child = parent->GetChild(i);
|
||||
if (child->GetTagName() == tag)
|
||||
return child;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
class ConfigTabsetListener : public Rml::EventListener {
|
||||
void ProcessEvent(Rml::Event& event) override {
|
||||
if (event.GetId() == Rml::EventId::Tabchange) {
|
||||
int tab_index = event.GetParameter<int>("tab_index", 0);
|
||||
bool in_mod_tab = (tab_index == recompui::config_tab_to_index(recompui::ConfigTab::Mods));
|
||||
if (in_mod_tab) {
|
||||
recompui::set_config_tabset_mod_nav();
|
||||
}
|
||||
else {
|
||||
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
|
||||
Rml::Element* tabs = recompui::get_child_by_tag(tabset, "tabs");
|
||||
if (tabs != nullptr) {
|
||||
size_t num_children = tabs->GetNumChildren();
|
||||
for (size_t i = 0; i < num_children; i++) {
|
||||
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ConfigMenu : public recompui::MenuController {
|
||||
private:
|
||||
ConfigTabsetListener config_tabset_listener;
|
||||
public:
|
||||
ConfigMenu() {
|
||||
|
||||
@@ -384,11 +443,10 @@ public:
|
||||
~ConfigMenu() override {
|
||||
|
||||
}
|
||||
Rml::ElementDocument* load_document(Rml::Context* context) override {
|
||||
(void)context;
|
||||
config_context = recompui::create_context("assets/config_menu.rml");
|
||||
Rml::ElementDocument* ret = config_context.get_document();
|
||||
return ret;
|
||||
void load_document() override {
|
||||
config_context = recompui::create_context(banjo::get_asset_path("config_menu.rml"));
|
||||
recompui::update_mod_list(false);
|
||||
recompui::get_config_tabset()->AddEventListener(Rml::EventId::Tabchange, &config_tabset_listener);
|
||||
}
|
||||
void register_events(recompui::UiEventListenerInstancer& listener) override {
|
||||
recompui::register_event(listener, "apply_options",
|
||||
@@ -563,7 +621,7 @@ public:
|
||||
throw std::runtime_error("Failed to make RmlUi data model for the controls config menu");
|
||||
}
|
||||
|
||||
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } );
|
||||
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = static_cast<uint64_t>(recomp::get_num_inputs()); } );
|
||||
constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } );
|
||||
|
||||
constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) {
|
||||
@@ -851,64 +909,45 @@ void recompui::toggle_fullscreen() {
|
||||
graphics_model_handle.DirtyVariable("wm_option");
|
||||
}
|
||||
|
||||
void recompui::open_prompt(
|
||||
const std::string& headerText,
|
||||
const std::string& contentText,
|
||||
const std::string& confirmLabelText,
|
||||
const std::string& cancelLabelText,
|
||||
std::function<void()> confirmCb,
|
||||
std::function<void()> cancelCb,
|
||||
ButtonVariant _confirmVariant,
|
||||
ButtonVariant _cancelVariant,
|
||||
bool _focusOnCancel,
|
||||
const std::string& _returnElementId
|
||||
) {
|
||||
printf("Prompt opened\n %s (%s): %s %s\n", contentText.c_str(), headerText.c_str(), confirmLabelText.c_str(), cancelLabelText.c_str());
|
||||
printf(" Autoselected %s\n", confirmLabelText.c_str());
|
||||
confirmCb();
|
||||
}
|
||||
|
||||
bool recompui::is_prompt_open() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void recompui::set_config_tab(ConfigTab tab) {
|
||||
get_config_tabset()->SetActiveTab(config_tab_to_index(tab));
|
||||
}
|
||||
|
||||
Rml::ElementTabSet* recompui::get_config_tabset() {
|
||||
ContextId config_context = recompui::get_config_context_id();
|
||||
|
||||
|
||||
ContextId old_context = recompui::try_close_current_context();
|
||||
|
||||
Rml::ElementDocument *doc = config_context.get_document();
|
||||
assert(doc != nullptr);
|
||||
|
||||
Rml::Element *tabset_el = doc->GetElementById("config_tabset");
|
||||
assert(tabset_el != nullptr);
|
||||
|
||||
Rml::ElementTabSet *tabset = rmlui_dynamic_cast<Rml::ElementTabSet *>(tabset_el);
|
||||
assert(tabset != nullptr);
|
||||
|
||||
if (old_context != ContextId::null()) {
|
||||
old_context.open();
|
||||
}
|
||||
|
||||
return tabset;
|
||||
}
|
||||
|
||||
Rml::Element* recompui::get_mod_tab() {
|
||||
ContextId config_context = recompui::get_config_context_id();
|
||||
|
||||
ContextId old_context = recompui::try_close_current_context();
|
||||
|
||||
Rml::ElementDocument* doc = config_context.get_document();
|
||||
assert(doc != nullptr);
|
||||
|
||||
Rml::Element* tabset_el = doc->GetElementById("config_tabset");
|
||||
assert(tabset_el != nullptr);
|
||||
Rml::Element* tab_el = doc->GetElementById("tab_mods");
|
||||
assert(tab_el != nullptr);
|
||||
|
||||
Rml::ElementTabSet* tabset = rmlui_dynamic_cast<Rml::ElementTabSet*>(tabset_el);
|
||||
assert(tabset != nullptr);
|
||||
|
||||
int tab_index = 0;
|
||||
|
||||
switch (tab) {
|
||||
case ConfigTab::General:
|
||||
tab_index = 0;
|
||||
break;
|
||||
case ConfigTab::Controls:
|
||||
tab_index = 1;
|
||||
break;
|
||||
case ConfigTab::Graphics:
|
||||
tab_index = 2;
|
||||
break;
|
||||
case ConfigTab::Sound:
|
||||
tab_index = 3;
|
||||
break;
|
||||
case ConfigTab::Mods:
|
||||
tab_index = 4;
|
||||
break;
|
||||
case ConfigTab::Debug:
|
||||
tab_index = 5;
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
return;
|
||||
if (old_context != ContextId::null()) {
|
||||
old_context.open();
|
||||
}
|
||||
|
||||
tabset->SetActiveTab(tab_index);
|
||||
return tab_el;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace recompui {
|
||||
void ConfigOptionElement::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Hover:
|
||||
if (hover_callback == nullptr) {
|
||||
break;
|
||||
}
|
||||
hover_callback(this, std::get<EventHover>(e.variant).active);
|
||||
break;
|
||||
case EventType::Update:
|
||||
@@ -36,8 +39,8 @@ ConfigOptionElement::~ConfigOptionElement() {
|
||||
|
||||
}
|
||||
|
||||
void ConfigOptionElement::set_id(std::string_view id) {
|
||||
this->id = id;
|
||||
void ConfigOptionElement::set_option_id(std::string_view id) {
|
||||
this->option_id = id;
|
||||
}
|
||||
|
||||
void ConfigOptionElement::set_name(std::string_view name) {
|
||||
@@ -53,6 +56,10 @@ void ConfigOptionElement::set_hover_callback(std::function<void(ConfigOptionElem
|
||||
hover_callback = callback;
|
||||
}
|
||||
|
||||
void ConfigOptionElement::set_focus_callback(std::function<void(const std::string &, bool)> callback) {
|
||||
focus_callback = callback;
|
||||
}
|
||||
|
||||
const std::string &ConfigOptionElement::get_description() const {
|
||||
return description;
|
||||
}
|
||||
@@ -60,45 +67,56 @@ const std::string &ConfigOptionElement::get_description() const {
|
||||
// ConfigOptionSlider
|
||||
|
||||
void ConfigOptionSlider::slider_value_changed(double v) {
|
||||
callback(id, v);
|
||||
callback(option_id, v);
|
||||
}
|
||||
|
||||
ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback) : ConfigOptionElement(parent) {
|
||||
this->callback = callback;
|
||||
|
||||
slider = get_current_context().create_element<Slider>(this, percent ? SliderType::Percent : SliderType::Double);
|
||||
slider->set_max_width(380.0f);
|
||||
slider->set_min_value(min_value);
|
||||
slider->set_max_value(max_value);
|
||||
slider->set_step_value(step_value);
|
||||
slider->set_value(value);
|
||||
slider->add_value_changed_callback(std::bind(&ConfigOptionSlider::slider_value_changed, this, std::placeholders::_1));
|
||||
slider->add_value_changed_callback([this](double v){ slider_value_changed(v); });
|
||||
slider->set_focus_callback([this](bool active) {
|
||||
focus_callback(option_id, active);
|
||||
});
|
||||
}
|
||||
|
||||
// ConfigOptionTextInput
|
||||
|
||||
void ConfigOptionTextInput::text_changed(const std::string &text) {
|
||||
callback(id, text);
|
||||
callback(option_id, text);
|
||||
}
|
||||
|
||||
ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) : ConfigOptionElement(parent) {
|
||||
this->callback = callback;
|
||||
|
||||
text_input = get_current_context().create_element<TextInput>(this);
|
||||
text_input->set_max_width(400.0f);
|
||||
text_input->set_text(value);
|
||||
text_input->add_text_changed_callback(std::bind(&ConfigOptionTextInput::text_changed, this, std::placeholders::_1));
|
||||
text_input->add_text_changed_callback([this](const std::string &text){ text_changed(text); });
|
||||
text_input->set_focus_callback([this](bool active) {
|
||||
focus_callback(option_id, active);
|
||||
});
|
||||
}
|
||||
|
||||
// ConfigOptionRadio
|
||||
|
||||
void ConfigOptionRadio::index_changed(uint32_t index) {
|
||||
callback(id, index);
|
||||
callback(option_id, index);
|
||||
}
|
||||
|
||||
ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) : ConfigOptionElement(parent) {
|
||||
this->callback = callback;
|
||||
|
||||
radio = get_current_context().create_element<Radio>(this);
|
||||
radio->add_index_changed_callback(std::bind(&ConfigOptionRadio::index_changed, this, std::placeholders::_1));
|
||||
radio->set_focus_callback([this](bool active) {
|
||||
focus_callback(option_id, active);
|
||||
});
|
||||
radio->add_index_changed_callback([this](uint32_t index){ index_changed(index); });
|
||||
for (std::string_view option : options) {
|
||||
radio->add_option(option);
|
||||
}
|
||||
@@ -117,21 +135,22 @@ void ConfigSubMenu::back_button_pressed() {
|
||||
|
||||
recompui::hide_context(sub_menu_context);
|
||||
recompui::show_context(config_context, "");
|
||||
recompui::focus_mod_configure_button();
|
||||
}
|
||||
|
||||
void ConfigSubMenu::option_hovered(ConfigOptionElement *option, bool active) {
|
||||
void ConfigSubMenu::set_description_option_element(ConfigOptionElement *option, bool active) {
|
||||
if (active) {
|
||||
hover_option_elements.emplace(option);
|
||||
description_option_element = option;
|
||||
}
|
||||
else {
|
||||
hover_option_elements.erase(option);
|
||||
else if (description_option_element == option) {
|
||||
description_option_element = nullptr;
|
||||
}
|
||||
|
||||
if (hover_option_elements.empty()) {
|
||||
if (description_option_element == nullptr) {
|
||||
description_label->set_text("");
|
||||
}
|
||||
else {
|
||||
description_label->set_text((*hover_option_elements.begin())->get_description());
|
||||
description_label->set_text(description_option_element->get_description());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +166,12 @@ ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
|
||||
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
|
||||
header_container->set_flex_grow(0.0f);
|
||||
header_container->set_align_items(AlignItems::Center);
|
||||
header_container->set_padding_left(12.0f);
|
||||
header_container->set_padding(12.0f);
|
||||
header_container->set_gap(24.0f);
|
||||
|
||||
{
|
||||
back_button = context.create_element<Button>(header_container, "Back", ButtonStyle::Secondary);
|
||||
back_button->add_pressed_callback(std::bind(&ConfigSubMenu::back_button_pressed, this));
|
||||
back_button->add_pressed_callback([this](){ back_button_pressed(); });
|
||||
title_label = context.create_element<Label>(header_container, "Title", LabelStyle::Large);
|
||||
}
|
||||
|
||||
@@ -167,9 +186,13 @@ ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
|
||||
config_scroll_container = context.create_element<ScrollContainer>(config_container, ScrollDirection::Vertical);
|
||||
}
|
||||
|
||||
description_label = context.create_element<Label>(body_container, "Description", LabelStyle::Small);
|
||||
description_label = context.create_element<Label>(body_container, "", LabelStyle::Small);
|
||||
description_label->set_min_width(800.0f);
|
||||
description_label->set_padding_left(16.0f);
|
||||
description_label->set_padding_right(16.0f);
|
||||
}
|
||||
|
||||
recompui::get_current_context().set_autofocus_element(back_button);
|
||||
}
|
||||
|
||||
ConfigSubMenu::~ConfigSubMenu() {
|
||||
@@ -183,14 +206,24 @@ void ConfigSubMenu::enter(std::string_view title) {
|
||||
void ConfigSubMenu::clear_options() {
|
||||
config_scroll_container->clear_children();
|
||||
config_option_elements.clear();
|
||||
hover_option_elements.clear();
|
||||
description_option_element = nullptr;
|
||||
}
|
||||
|
||||
void ConfigSubMenu::add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description) {
|
||||
option->set_id(id);
|
||||
option->set_option_id(id);
|
||||
option->set_name(name);
|
||||
option->set_description(description);
|
||||
option->set_hover_callback(std::bind(&ConfigSubMenu::option_hovered, this, std::placeholders::_1, std::placeholders::_2));
|
||||
option->set_hover_callback([this](ConfigOptionElement *option, bool active){ set_description_option_element(option, active); });
|
||||
option->set_focus_callback([this, option](const std::string &id, bool active) { set_description_option_element(option, active); });
|
||||
if (config_option_elements.empty()) {
|
||||
back_button->set_nav(NavDirection::Down, option->get_focus_element());
|
||||
option->set_nav(NavDirection::Up, back_button);
|
||||
}
|
||||
else {
|
||||
config_option_elements.back()->set_nav(NavDirection::Down, option->get_focus_element());
|
||||
option->set_nav(NavDirection::Up, config_option_elements.back()->get_focus_element());
|
||||
}
|
||||
|
||||
config_option_elements.emplace_back(option);
|
||||
}
|
||||
|
||||
@@ -233,4 +266,4 @@ ConfigSubMenu *ElementConfigSubMenu::get_config_sub_menu_element() const {
|
||||
return config_sub_menu;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,28 @@ namespace recompui {
|
||||
class ConfigOptionElement : public Element {
|
||||
protected:
|
||||
Label *name_label = nullptr;
|
||||
std::string id;
|
||||
std::string option_id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::function<void(ConfigOptionElement *, bool)> hover_callback = nullptr;
|
||||
std::function<void(const std::string &, bool)> focus_callback = nullptr;
|
||||
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "ConfigOptionElement"; }
|
||||
public:
|
||||
ConfigOptionElement(Element *parent);
|
||||
virtual ~ConfigOptionElement();
|
||||
void set_id(std::string_view id);
|
||||
void set_option_id(std::string_view id);
|
||||
void set_name(std::string_view name);
|
||||
void set_description(std::string_view description);
|
||||
void set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback);
|
||||
void set_focus_callback(std::function<void(const std::string &, bool)> callback);
|
||||
const std::string &get_description() const;
|
||||
void set_nav_auto(NavDirection dir) override { get_focus_element()->set_nav_auto(dir); }
|
||||
void set_nav_none(NavDirection dir) override { get_focus_element()->set_nav_none(dir); }
|
||||
void set_nav(NavDirection dir, Element* element) override { get_focus_element()->set_nav(dir, element); }
|
||||
void set_nav_manual(NavDirection dir, const std::string& target) override { get_focus_element()->set_nav_manual(dir, target); }
|
||||
virtual Element* get_focus_element() { return this; }
|
||||
};
|
||||
|
||||
class ConfigOptionSlider : public ConfigOptionElement {
|
||||
@@ -38,8 +46,10 @@ protected:
|
||||
std::function<void(const std::string &, double)> callback;
|
||||
|
||||
void slider_value_changed(double v);
|
||||
std::string_view get_type_name() override { return "ConfigOptionSlider"; }
|
||||
public:
|
||||
ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback);
|
||||
Element* get_focus_element() override { return slider; }
|
||||
};
|
||||
|
||||
class ConfigOptionTextInput : public ConfigOptionElement {
|
||||
@@ -48,8 +58,10 @@ protected:
|
||||
std::function<void(const std::string &, const std::string &)> callback;
|
||||
|
||||
void text_changed(const std::string &text);
|
||||
std::string_view get_type_name() override { return "ConfigOptionTextInput"; }
|
||||
public:
|
||||
ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
|
||||
Element* get_focus_element() override { return text_input; }
|
||||
};
|
||||
|
||||
class ConfigOptionRadio : public ConfigOptionElement {
|
||||
@@ -58,8 +70,10 @@ protected:
|
||||
std::function<void(const std::string &, uint32_t)> callback;
|
||||
|
||||
void index_changed(uint32_t index);
|
||||
std::string_view get_type_name() override { return "ConfigOptionRadio"; }
|
||||
public:
|
||||
ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
|
||||
Element* get_focus_element() override { return radio; }
|
||||
};
|
||||
|
||||
class ConfigSubMenu : public Element {
|
||||
@@ -72,12 +86,13 @@ private:
|
||||
Container *config_container = nullptr;
|
||||
ScrollContainer *config_scroll_container = nullptr;
|
||||
std::vector<ConfigOptionElement *> config_option_elements;
|
||||
std::unordered_set<ConfigOptionElement *> hover_option_elements;
|
||||
ConfigOptionElement * description_option_element = nullptr;
|
||||
|
||||
void back_button_pressed();
|
||||
void option_hovered(ConfigOptionElement *option, bool active);
|
||||
void set_description_option_element(ConfigOptionElement *option, bool active);
|
||||
void add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description);
|
||||
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ConfigSubMenu"; }
|
||||
public:
|
||||
ConfigSubMenu(Element *parent);
|
||||
virtual ~ConfigSubMenu();
|
||||
@@ -99,4 +114,4 @@ private:
|
||||
};
|
||||
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#ifndef __UI_HELPERS_H__
|
||||
#define __UI_HELPERS_H__
|
||||
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "librecomp/addresses.hpp"
|
||||
|
||||
#include "elements/ui_element.h"
|
||||
#include "elements/ui_types.h"
|
||||
#include "core/ui_context.h"
|
||||
#include "core/ui_resource.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
|
||||
|
||||
inline ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
|
||||
return ContextId{ .slot_id = context_id };
|
||||
}
|
||||
|
||||
inline float arg_float2(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = _arg<2, uint32_t>(rdram, ctx);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float3(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = _arg<3, uint32_t>(rdram, ctx);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float4(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = MEM_W(0x10, ctx->r29);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float5(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = MEM_W(0x14, ctx->r29);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float6(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = MEM_W(0x18, ctx->r29);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
|
||||
|
||||
return ResourceId{ .slot_id = slot_id };
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
|
||||
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
|
||||
|
||||
if (resource == ResourceId::null()) {
|
||||
return nullptr;
|
||||
}
|
||||
else if (resource == root_element_id) {
|
||||
return ui_context.get_root_element();
|
||||
}
|
||||
|
||||
return resource.as_element();
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
|
||||
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
|
||||
|
||||
if (resource == ResourceId::null()) {
|
||||
return nullptr;
|
||||
}
|
||||
else if (resource == root_element_id) {
|
||||
ContextId ui_context = recompui::get_current_context();
|
||||
return ui_context.get_root_element();
|
||||
}
|
||||
|
||||
return *resource;
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
|
||||
|
||||
Color ret{};
|
||||
|
||||
ret.r = MEM_B(0, color_arg);
|
||||
ret.g = MEM_B(1, color_arg);
|
||||
ret.b = MEM_B(2, color_arg);
|
||||
ret.a = MEM_B(3, color_arg);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline void return_resource(recomp_context* ctx, ResourceId resource) {
|
||||
_return<uint32_t>(ctx, resource.slot_id);
|
||||
}
|
||||
|
||||
inline void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) {
|
||||
gpr addr = (reinterpret_cast<uint8_t*>(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL;
|
||||
|
||||
for (size_t i = 0; i < ret.size(); i++) {
|
||||
MEM_B(i, addr) = ret[i];
|
||||
}
|
||||
MEM_B(ret.size(), addr) = '\x00';
|
||||
|
||||
_return<PTR(char)>(ctx, addr);
|
||||
}
|
||||
|
||||
inline std::string decode_string(uint8_t* rdram, PTR(char) str) {
|
||||
// Get the length of the byteswapped string.
|
||||
size_t len = 0;
|
||||
while (MEM_B(str, len) != 0x00) {
|
||||
len++;
|
||||
}
|
||||
|
||||
std::string ret{};
|
||||
ret.reserve(len + 1);
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
ret += (char)MEM_B(str, i);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
+32
-39
@@ -1,5 +1,6 @@
|
||||
#include "recomp_ui.h"
|
||||
#include "banjo_config.h"
|
||||
#include "banjo_support.h"
|
||||
#include "librecomp/game.hpp"
|
||||
#include "ultramodern/ultramodern.hpp"
|
||||
#include "RmlUi/Core.h"
|
||||
@@ -15,41 +16,36 @@ extern std::vector<recomp::GameEntry> supported_games;
|
||||
|
||||
void select_rom() {
|
||||
nfdnchar_t* native_path = nullptr;
|
||||
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
|
||||
|
||||
if (result == NFD_OKAY) {
|
||||
std::filesystem::path path{native_path};
|
||||
|
||||
NFD_FreePathN(native_path);
|
||||
native_path = nullptr;
|
||||
|
||||
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
|
||||
switch (rom_error) {
|
||||
case recomp::RomValidationError::Good:
|
||||
bk_rom_valid = true;
|
||||
model_handle.DirtyVariable("bk_rom_valid");
|
||||
break;
|
||||
case recomp::RomValidationError::FailedToOpen:
|
||||
recompui::message_box("Failed to open ROM file.");
|
||||
break;
|
||||
case recomp::RomValidationError::NotARom:
|
||||
recompui::message_box("This is not a valid ROM file.");
|
||||
break;
|
||||
case recomp::RomValidationError::IncorrectRom:
|
||||
recompui::message_box("This ROM is not the correct game.");
|
||||
break;
|
||||
case recomp::RomValidationError::NotYet:
|
||||
recompui::message_box("This game isn't supported yet.");
|
||||
break;
|
||||
case recomp::RomValidationError::IncorrectVersion:
|
||||
recompui::message_box(
|
||||
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
|
||||
break;
|
||||
case recomp::RomValidationError::OtherError:
|
||||
recompui::message_box("An unknown error has occurred.");
|
||||
break;
|
||||
banjo::open_file_dialog([](bool success, const std::filesystem::path& path) {
|
||||
if (success) {
|
||||
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
|
||||
switch (rom_error) {
|
||||
case recomp::RomValidationError::Good:
|
||||
bk_rom_valid = true;
|
||||
model_handle.DirtyVariable("bk_rom_valid");
|
||||
break;
|
||||
case recomp::RomValidationError::FailedToOpen:
|
||||
recompui::message_box("Failed to open ROM file.");
|
||||
break;
|
||||
case recomp::RomValidationError::NotARom:
|
||||
recompui::message_box("This is not a valid ROM file.");
|
||||
break;
|
||||
case recomp::RomValidationError::IncorrectRom:
|
||||
recompui::message_box("This ROM is not the correct game.");
|
||||
break;
|
||||
case recomp::RomValidationError::NotYet:
|
||||
recompui::message_box("This game isn't supported yet.");
|
||||
break;
|
||||
case recomp::RomValidationError::IncorrectVersion:
|
||||
recompui::message_box(
|
||||
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
|
||||
break;
|
||||
case recomp::RomValidationError::OtherError:
|
||||
recompui::message_box("An unknown error has occurred.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recompui::ContextId launcher_context;
|
||||
@@ -66,11 +62,8 @@ public:
|
||||
~LauncherMenu() override {
|
||||
|
||||
}
|
||||
Rml::ElementDocument* load_document(Rml::Context* context) override {
|
||||
(void)context;
|
||||
launcher_context = recompui::create_context("assets/launcher.rml");
|
||||
Rml::ElementDocument* ret = launcher_context.get_document();
|
||||
return ret;
|
||||
void load_document() override {
|
||||
launcher_context = recompui::create_context(banjo::get_asset_path("launcher.rml"));
|
||||
}
|
||||
void register_events(recompui::UiEventListenerInstancer& listener) override {
|
||||
recompui::register_event(listener, "select_rom",
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
namespace recompui {
|
||||
|
||||
extern const std::string mod_tab_id;
|
||||
|
||||
ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
|
||||
set_flex(1.0f, 1.0f, 200.0f);
|
||||
set_height(100.0f, Unit::Percent);
|
||||
set_display(Display::Flex);
|
||||
set_flex_direction(FlexDirection::Column);
|
||||
set_border_bottom_right_radius(16.0f);
|
||||
set_background_color(Color{ 190, 184, 219, 25 });
|
||||
|
||||
ContextId context = get_current_context();
|
||||
@@ -19,6 +20,8 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
|
||||
header_container->set_padding(16.0f);
|
||||
header_container->set_gap(16.0f);
|
||||
header_container->set_background_color(Color{ 0, 0, 0, 89 });
|
||||
header_container->set_border_bottom_width(1.1f);
|
||||
header_container->set_border_bottom_color(Color{ 255, 255, 255, 25 });
|
||||
{
|
||||
thumbnail_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
|
||||
thumbnail_container->set_flex(0.0f, 0.0f);
|
||||
@@ -39,42 +42,48 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
|
||||
}
|
||||
}
|
||||
|
||||
body_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
|
||||
body_container->set_flex(0.0f, 0.0f);
|
||||
body_container = context.create_element<ScrollContainer>(this, ScrollDirection::Vertical);
|
||||
body_container->set_text_align(TextAlign::Left);
|
||||
body_container->set_padding(16.0f);
|
||||
body_container->set_gap(16.0f);
|
||||
{
|
||||
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
|
||||
authors_label = context.create_element<Label>(body_container, LabelStyle::Normal);
|
||||
authors_label->set_margin_bottom(16.0f);
|
||||
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
|
||||
}
|
||||
|
||||
spacer_element = context.create_element<Element>(this);
|
||||
spacer_element->set_flex(1.0f, 0.0f);
|
||||
|
||||
buttons_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceAround);
|
||||
buttons_container->set_flex(0.0f, 0.0f);
|
||||
buttons_container->set_padding(16.0f);
|
||||
buttons_container->set_justify_content(JustifyContent::SpaceBetween);
|
||||
buttons_container->set_border_top_width(1.1f);
|
||||
buttons_container->set_border_top_color(Color{ 255, 255, 255, 25 });
|
||||
buttons_container->set_background_color(Color{ 0, 0, 0, 89 });
|
||||
{
|
||||
enable_container = context.create_element<Container>(buttons_container, FlexDirection::Row, JustifyContent::FlexStart);
|
||||
enable_container->set_align_items(AlignItems::Center);
|
||||
enable_container->set_gap(16.0f);
|
||||
{
|
||||
enable_toggle = context.create_element<Toggle>(enable_container);
|
||||
enable_toggle->add_checked_callback(std::bind(&ModDetailsPanel::enable_toggle_checked, this, std::placeholders::_1));
|
||||
enable_toggle->add_checked_callback([this](bool checked){ enable_toggle_checked(checked); });
|
||||
enable_toggle->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
|
||||
enable_label = context.create_element<Label>(enable_container, "A currently enabled mod requires this mod", LabelStyle::Annotation);
|
||||
}
|
||||
|
||||
configure_button = context.create_element<Button>(buttons_container, "Configure", recompui::ButtonStyle::Secondary);
|
||||
configure_button->add_pressed_callback(std::bind(&ModDetailsPanel::configure_button_pressed, this));
|
||||
configure_button->add_pressed_callback([this](){ configure_button_pressed(); });
|
||||
configure_button->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
}
|
||||
clear_mod_navigation();
|
||||
}
|
||||
|
||||
ModDetailsPanel::~ModDetailsPanel() {
|
||||
}
|
||||
|
||||
void ModDetailsPanel::disable_toggle() {
|
||||
enable_toggle->set_enabled(false);
|
||||
}
|
||||
|
||||
void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled) {
|
||||
cur_details = details;
|
||||
|
||||
@@ -83,7 +92,7 @@ void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, c
|
||||
title_label->set_text(cur_details.display_name);
|
||||
version_label->set_text(cur_details.version.to_string());
|
||||
|
||||
std::string authors_str = "<i>Authors</i>:";
|
||||
std::string authors_str = "Authors:";
|
||||
bool first = true;
|
||||
for (const std::string& author : details.authors) {
|
||||
authors_str += (first ? " " : ", ") + author;
|
||||
@@ -96,6 +105,13 @@ void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, c
|
||||
enable_toggle->set_enabled(toggle_enabled);
|
||||
configure_button->set_enabled(configure_enabled);
|
||||
enable_label->set_display(toggle_label_visible ? Display::Block : Display::None);
|
||||
|
||||
if (configure_enabled) {
|
||||
enable_toggle->set_nav(NavDirection::Right, configure_button);
|
||||
}
|
||||
else {
|
||||
enable_toggle->set_nav_none(NavDirection::Right);
|
||||
}
|
||||
}
|
||||
|
||||
void ModDetailsPanel::set_mod_toggled_callback(std::function<void(bool)> callback) {
|
||||
@@ -106,6 +122,22 @@ void ModDetailsPanel::set_mod_configure_pressed_callback(std::function<void()> c
|
||||
mod_configure_pressed_callback = callback;
|
||||
}
|
||||
|
||||
void ModDetailsPanel::setup_mod_navigation(Element* nav_target) {
|
||||
enable_toggle->set_nav(NavDirection::Left, nav_target);
|
||||
|
||||
if (enable_toggle->is_enabled()) {
|
||||
configure_button->set_nav(NavDirection::Left, enable_toggle);
|
||||
}
|
||||
else {
|
||||
configure_button->set_nav(NavDirection::Left, nav_target);
|
||||
}
|
||||
}
|
||||
|
||||
void ModDetailsPanel::clear_mod_navigation() {
|
||||
enable_toggle->set_nav_none(NavDirection::Left);
|
||||
configure_button->set_nav_none(NavDirection::Left);
|
||||
}
|
||||
|
||||
void ModDetailsPanel::enable_toggle_checked(bool checked) {
|
||||
if (mod_toggled_callback != nullptr) {
|
||||
mod_toggled_callback(checked);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "elements/ui_image.h"
|
||||
#include "elements/ui_label.h"
|
||||
#include "elements/ui_toggle.h"
|
||||
#include "elements/ui_scroll_container.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
@@ -17,6 +18,13 @@ public:
|
||||
void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled);
|
||||
void set_mod_toggled_callback(std::function<void(bool)> callback);
|
||||
void set_mod_configure_pressed_callback(std::function<void()> callback);
|
||||
void setup_mod_navigation(Element* nav_target);
|
||||
void clear_mod_navigation();
|
||||
Toggle* get_enable_toggle() { return enable_toggle; }
|
||||
Button* get_configure_button() { return configure_button; }
|
||||
void disable_toggle();
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ModDetailsPanel"; }
|
||||
private:
|
||||
recomp::mods::ModDetails cur_details;
|
||||
Container *thumbnail_container = nullptr;
|
||||
@@ -25,10 +33,9 @@ private:
|
||||
Container *header_details_container = nullptr;
|
||||
Label *title_label = nullptr;
|
||||
Label *version_label = nullptr;
|
||||
Container *body_container = nullptr;
|
||||
ScrollContainer *body_container = nullptr;
|
||||
Label *description_label = nullptr;
|
||||
Label *authors_label = nullptr;
|
||||
Element *spacer_element = nullptr;
|
||||
Container *buttons_container = nullptr;
|
||||
Container *enable_container = nullptr;
|
||||
Toggle *enable_toggle = nullptr;
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
#include "ui_mod_installer.h"
|
||||
|
||||
#include "librecomp/mods.hpp"
|
||||
|
||||
namespace recompui {
|
||||
static const std::string ManifestFilename = "mod.json";
|
||||
static const char *TextureDatabaseFilename = "rt64.json";
|
||||
static const std::u8string OldExtension = u8".old";
|
||||
static const std::u8string NewExtension = u8".new";
|
||||
|
||||
static bool is_dynamic_lib(const std::filesystem::path &file_path) {
|
||||
#if defined(_WIN32)
|
||||
return file_path.extension() == ".dll";
|
||||
#elif defined(__linux__)
|
||||
return file_path.extension() == ".so" || file_path.filename().string().find(".so.") != std::string::npos;
|
||||
#elif defined(__APPLE__)
|
||||
return file_path.extension() == ".dylib";
|
||||
#else
|
||||
static_assert(false, "Unimplemented for this platform.");
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t zip_write_func(void *opaque, mz_uint64 offset, const void *bytes, size_t count) {
|
||||
std::ofstream &stream = *(std::ofstream *)(opaque);
|
||||
stream.seekp(offset, std::ios::beg);
|
||||
stream.write((const char *)(bytes), count);
|
||||
return stream.bad() ? 0 : count;
|
||||
}
|
||||
|
||||
void start_single_mod_installation(const std::filesystem::path &file_path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
|
||||
// Check for the existence of the manifest file.
|
||||
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
|
||||
std::filesystem::path target_path = mods_directory / file_path.filename();
|
||||
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
|
||||
ModInstaller::Installation installation;
|
||||
bool exists = false;
|
||||
std::vector<char> manifest_bytes = file_handle.read_file(ManifestFilename, exists);
|
||||
if (exists) {
|
||||
// Parse the manifest file to check for its validity.
|
||||
std::string error;
|
||||
recomp::mods::ModManifest manifest;
|
||||
recomp::mods::ModOpenError open_error = parse_manifest(manifest, manifest_bytes, error);
|
||||
exists = (open_error == recomp::mods::ModOpenError::Good);
|
||||
|
||||
if (exists) {
|
||||
installation.mod_id = manifest.mod_id;
|
||||
installation.display_name = manifest.display_name;
|
||||
installation.mod_version = manifest.version;
|
||||
installation.mod_file = target_path;
|
||||
}
|
||||
}
|
||||
else if (file_path.extension() == ".rtz") {
|
||||
// When it's an rtz file, check if the texture database file exists.
|
||||
exists = mz_zip_reader_locate_file(file_handle.archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
|
||||
|
||||
if (exists) {
|
||||
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
|
||||
installation.display_name = installation.mod_id;
|
||||
installation.mod_version = recomp::Version{0, 0, 0, ""};
|
||||
installation.mod_file = target_path;
|
||||
}
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
if (exists) {
|
||||
std::filesystem::copy(file_path, target_write_path, ec);
|
||||
if (ec) {
|
||||
result.error_messages.emplace_back("Unable to install " + file_path.filename().string() + " to mod directory.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.error_messages.emplace_back(file_path.string() + " is not a mod.");
|
||||
std::filesystem::remove(target_write_path, ec);
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(installation.mod_file, ec)) {
|
||||
installation.needs_overwrite_confirmation = true;
|
||||
}
|
||||
if (!installation.needs_overwrite_confirmation) {
|
||||
// This check isn't really needed as additional_files will be empty for a single mod installation,
|
||||
// but it's good to have in case this logic ever changes.
|
||||
for (const std::filesystem::path &path : installation.additional_files) {
|
||||
if (std::filesystem::exists(path, ec)) {
|
||||
installation.needs_overwrite_confirmation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.pending_installations.emplace_back(installation);
|
||||
}
|
||||
|
||||
void start_package_mod_installation(const std::filesystem::path &path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
|
||||
std::error_code ec;
|
||||
char filename[1024];
|
||||
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
|
||||
mz_zip_archive *zip_archive = file_handle.archive.get();
|
||||
mz_uint num_files = mz_zip_reader_get_num_files(file_handle.archive.get());
|
||||
std::list<std::filesystem::path> dynamic_lib_files;
|
||||
std::list<ModInstaller::Installation>::iterator first_nrm_iterator = result.pending_installations.end();
|
||||
bool found_mod = false;
|
||||
for (mz_uint i = 0; i < num_files; i++) {
|
||||
mz_uint filename_length = mz_zip_reader_get_filename(zip_archive, i, filename, sizeof(filename));
|
||||
if (filename_length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::filesystem::path target_path = mods_directory / std::u8string_view((const char8_t *)(filename));
|
||||
if ((target_path.extension() == ".rtz") || (target_path.extension() == ".nrm")) {
|
||||
found_mod = true;
|
||||
ModInstaller::Installation installation;
|
||||
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
|
||||
std::ofstream output_stream(target_write_path, std::ios::binary);
|
||||
if (!output_stream.is_open()) {
|
||||
result.error_messages.emplace_back("Unable to write to mod directory.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
|
||||
output_stream.close();
|
||||
std::filesystem::remove(target_write_path, ec);
|
||||
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
|
||||
continue;
|
||||
}
|
||||
|
||||
output_stream.close();
|
||||
if (output_stream.bad()) {
|
||||
std::filesystem::remove(target_write_path, ec);
|
||||
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to load the extracted file as a mod file handle.
|
||||
recomp::mods::ModOpenError open_error;
|
||||
std::unique_ptr<recomp::mods::ZipModFileHandle> extracted_file_handle = std::make_unique<recomp::mods::ZipModFileHandle>(target_write_path, open_error);
|
||||
if (open_error != recomp::mods::ModOpenError::Good) {
|
||||
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
|
||||
extracted_file_handle.reset();
|
||||
std::filesystem::remove(target_write_path, ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for the existence of the manifest file.
|
||||
bool exists = false;
|
||||
std::vector<char> manifest_bytes = extracted_file_handle->read_file(ManifestFilename, exists);
|
||||
if (exists) {
|
||||
// Parse the manifest file to check for its validity.
|
||||
std::string error;
|
||||
recomp::mods::ModManifest manifest;
|
||||
open_error = parse_manifest(manifest, manifest_bytes, error);
|
||||
exists = (open_error == recomp::mods::ModOpenError::Good);
|
||||
|
||||
if (exists) {
|
||||
installation.mod_id = manifest.mod_id;
|
||||
installation.display_name = manifest.display_name;
|
||||
installation.mod_version = manifest.version;
|
||||
installation.mod_file = target_path;
|
||||
}
|
||||
}
|
||||
else if (target_path.extension() == ".rtz") {
|
||||
// When it's an rtz file, check if the texture database file exists.
|
||||
exists = mz_zip_reader_locate_file(extracted_file_handle->archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
|
||||
|
||||
if (exists) {
|
||||
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
|
||||
installation.display_name = installation.mod_id;
|
||||
installation.mod_version = recomp::Version();
|
||||
installation.mod_file = target_path;
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
|
||||
extracted_file_handle.reset();
|
||||
std::filesystem::remove(target_write_path, ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(installation.mod_file, ec)) {
|
||||
installation.needs_overwrite_confirmation = true;
|
||||
}
|
||||
if (!installation.needs_overwrite_confirmation) {
|
||||
// This check isn't really needed as additional_files will be empty at this point,
|
||||
// but it's good to have in case this logic ever changes.
|
||||
for (const std::filesystem::path &path : installation.additional_files) {
|
||||
if (std::filesystem::exists(path, ec)) {
|
||||
installation.needs_overwrite_confirmation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.pending_installations.emplace_back(installation);
|
||||
|
||||
// Store the first nrm found for any dynamic libraries that might be found.
|
||||
if ((first_nrm_iterator == result.pending_installations.end()) && (target_path.extension() == ".nrm")) {
|
||||
first_nrm_iterator = std::prev(result.pending_installations.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (is_dynamic_lib(target_path)) {
|
||||
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
|
||||
std::ofstream output_stream(target_write_path, std::ios::binary);
|
||||
if (!output_stream.is_open()) {
|
||||
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
|
||||
output_stream.close();
|
||||
std::filesystem::remove(target_write_path, ec);
|
||||
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
|
||||
continue;
|
||||
}
|
||||
|
||||
output_stream.close();
|
||||
if (output_stream.bad()) {
|
||||
std::filesystem::remove(target_write_path, ec);
|
||||
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
|
||||
continue;
|
||||
}
|
||||
|
||||
dynamic_lib_files.emplace_back(target_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dynamic_lib_files.empty()) {
|
||||
if (first_nrm_iterator != result.pending_installations.end()) {
|
||||
// Associate all these files to the first mod that is found.
|
||||
for (const std::filesystem::path &path : dynamic_lib_files) {
|
||||
first_nrm_iterator->additional_files.emplace_back(path);
|
||||
|
||||
// Run verification against for overwrite confirmations.
|
||||
if (std::filesystem::exists(path, ec)) {
|
||||
first_nrm_iterator->needs_overwrite_confirmation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// These library files were not required by any mod, just delete them.
|
||||
for (const std::filesystem::path &path : dynamic_lib_files) {
|
||||
std::filesystem::path new_path(path.u8string() + NewExtension);
|
||||
std::filesystem::remove(new_path, ec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_mod) {
|
||||
result.error_messages.emplace_back("No mods found in " + path.filename().string() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
void remove_and_rename(std::vector<std::string>& error_messages, const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
std::filesystem::path old_path(path.u8string() + OldExtension);
|
||||
std::filesystem::path new_path(path.u8string() + NewExtension);
|
||||
|
||||
// Rename the current path to a temporary old path, but only if the current path already exists.
|
||||
if (std::filesystem::exists(path, ec)) {
|
||||
std::filesystem::remove(old_path, ec);
|
||||
std::filesystem::rename(path, old_path, ec);
|
||||
if (ec) {
|
||||
// If it fails, remove the new path.
|
||||
std::filesystem::remove(new_path, ec);
|
||||
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Rename the new path to the current path.
|
||||
std::filesystem::rename(new_path, path, ec);
|
||||
if (ec) {
|
||||
// If it fails, remove the new path and also restore the temporary old path to the current path.
|
||||
std::filesystem::remove(new_path, ec);
|
||||
std::filesystem::rename(old_path, path, ec);
|
||||
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
// If nothing failed, just remove the temporary old path.
|
||||
std::filesystem::remove(old_path, ec);
|
||||
};
|
||||
|
||||
void ModInstaller::start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result) {
|
||||
result = Result();
|
||||
|
||||
for (const std::filesystem::path &path : file_paths) {
|
||||
if (is_dynamic_lib(path)) {
|
||||
result.error_messages.emplace_back("The provided mod(s) must be installed without extracting the ZIP file(s). Please install the mod ZIP file(s) directly.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::filesystem::path &path : file_paths) {
|
||||
recomp::mods::ModOpenError open_error;
|
||||
recomp::mods::ZipModFileHandle file_handle(path, open_error);
|
||||
if (open_error != recomp::mods::ModOpenError::Good) {
|
||||
result.error_messages.emplace_back(path.filename().string() + " is not a valid zip or mod.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// First we verify if the container itself isn't a mod already.
|
||||
// TODO hook into the runtime's container registration to check the extension instead of using hardcoded values.
|
||||
if ((path.extension() == ".rtz") || (path.extension() == ".nrm")) {
|
||||
start_single_mod_installation(path, file_handle, progress_callback, result);
|
||||
}
|
||||
else {
|
||||
// Scan the container for compatible mods instead. This is the case for packages made by users or how they're tipically uploaded to Thunderstore.
|
||||
start_package_mod_installation(path, file_handle, progress_callback, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModInstaller::cancel_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
|
||||
error_messages.clear();
|
||||
|
||||
std::error_code ec;
|
||||
// Delete all the files that were extracted for all mods.
|
||||
for (const Installation &installation : result.pending_installations) {
|
||||
std::filesystem::path new_path(installation.mod_file.u8string() + NewExtension);
|
||||
std::filesystem::remove(new_path, ec);
|
||||
for (const std::filesystem::path &path : installation.additional_files) {
|
||||
std::filesystem::path new_path(path.u8string() + NewExtension);
|
||||
std::filesystem::remove(new_path, ec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModInstaller::finish_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
|
||||
error_messages.clear();
|
||||
|
||||
std::error_code ec;
|
||||
for (const Installation &installation : result.pending_installations) {
|
||||
// Overwrite the mod files.
|
||||
remove_and_rename(error_messages, installation.mod_file);
|
||||
for (const std::filesystem::path &path : installation.additional_files) {
|
||||
remove_and_rename(error_messages, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef RECOMPUI_MOD_INSTALLER_H
|
||||
#define RECOMPUI_MOD_INSTALLER_H
|
||||
|
||||
#include <librecomp/game.hpp>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <list>
|
||||
|
||||
namespace recompui {
|
||||
struct ModInstaller {
|
||||
struct Installation {
|
||||
std::string mod_id;
|
||||
std::string display_name;
|
||||
recomp::Version mod_version;
|
||||
std::filesystem::path mod_file;
|
||||
std::list<std::filesystem::path> additional_files;
|
||||
bool needs_overwrite_confirmation = false;
|
||||
};
|
||||
|
||||
struct Confirmation {
|
||||
std::string old_display_name;
|
||||
std::string new_display_name;
|
||||
std::string old_mod_id;
|
||||
std::string new_mod_id;
|
||||
recomp::Version old_version;
|
||||
recomp::Version new_version;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
std::list<std::string> error_messages;
|
||||
std::list<Installation> pending_installations;
|
||||
};
|
||||
|
||||
static void start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result);
|
||||
static void cancel_mod_installation(const Result& result, std::vector<std::string>& errors);
|
||||
static void finish_mod_installation(const Result &result, std::vector<std::string>& errors);
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
+271
-44
@@ -1,5 +1,8 @@
|
||||
#include "ui_mod_menu.h"
|
||||
#include "ui_mod_menu.h"
|
||||
#include "ui_utils.h"
|
||||
#include "recomp_ui.h"
|
||||
#include "banjo_support.h"
|
||||
#include "banjo_render.h"
|
||||
|
||||
#include "librecomp/mods.hpp"
|
||||
|
||||
@@ -24,51 +27,65 @@ static bool is_mod_enabled_or_auto(const std::string &mod_id) {
|
||||
}
|
||||
|
||||
// ModEntryView
|
||||
#define COL_TEXT_DEFAULT 242, 242, 242
|
||||
#define COL_TEXT_DIM 204, 204, 204
|
||||
#define COL_SECONDARY 23, 214, 232
|
||||
constexpr float modEntryHeight = 120.0f;
|
||||
constexpr float modEntryPadding = 4.0f;
|
||||
|
||||
ModEntryView::ModEntryView(Element *parent) : Element(parent) {
|
||||
extern const std::string mod_tab_id;
|
||||
const std::string mod_tab_id = "#tab_mods";
|
||||
|
||||
ModEntryView::ModEntryView(Element *parent) : Element(parent, Events(EventType::Update)) {
|
||||
ContextId context = get_current_context();
|
||||
|
||||
set_display(Display::Flex);
|
||||
set_flex_direction(FlexDirection::Row);
|
||||
set_width(100.0f, Unit::Percent);
|
||||
set_height_auto();
|
||||
set_padding_top(4.0f);
|
||||
set_padding_right(8.0f);
|
||||
set_padding_bottom(4.0f);
|
||||
set_padding_left(8.0f);
|
||||
set_border_width(1.1f);
|
||||
set_border_color(Color{ 242, 242, 242, 12 });
|
||||
set_background_color(Color{ 242, 242, 242, 12 });
|
||||
set_padding(modEntryPadding);
|
||||
set_border_left_width(2.0f);
|
||||
set_border_color(Color{ COL_TEXT_DEFAULT, 12 });
|
||||
set_background_color(Color{ COL_TEXT_DEFAULT, 12 });
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_color(Color{ COL_TEXT_DEFAULT, 255 });
|
||||
|
||||
checked_style.set_border_color(Color{ 242, 242, 242, 160 });
|
||||
hover_style.set_border_color(Color{ 242, 242, 242, 64 });
|
||||
checked_hover_style.set_border_color(Color{ 242, 242, 242, 204 });
|
||||
checked_style.set_border_color(Color{ COL_TEXT_DEFAULT, 160 });
|
||||
checked_style.set_color(Color{ 255, 255, 255, 255 });
|
||||
checked_style.set_background_color(Color{ 26, 24, 32, 255 });
|
||||
hover_style.set_border_color(Color{ COL_TEXT_DEFAULT, 64 });
|
||||
checked_hover_style.set_border_color(Color{ COL_TEXT_DEFAULT, 255 });
|
||||
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
|
||||
|
||||
{
|
||||
thumbnail_image = context.create_element<Image>(this, "");
|
||||
thumbnail_image->set_width(100.0f);
|
||||
thumbnail_image->set_height(100.0f);
|
||||
thumbnail_image->set_min_width(100.0f);
|
||||
thumbnail_image->set_min_height(100.0f);
|
||||
thumbnail_image->set_width(modEntryHeight);
|
||||
thumbnail_image->set_height(modEntryHeight);
|
||||
thumbnail_image->set_min_width(modEntryHeight);
|
||||
thumbnail_image->set_min_height(modEntryHeight);
|
||||
thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 });
|
||||
|
||||
|
||||
body_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
|
||||
body_container = context.create_element<Element>(this);
|
||||
body_container->set_width_auto();
|
||||
body_container->set_height(100.0f);
|
||||
body_container->set_margin_left(16.0f);
|
||||
body_container->set_overflow(Overflow::Hidden);
|
||||
body_container->set_padding_top(8.0f);
|
||||
body_container->set_padding_bottom(8.0f);
|
||||
body_container->set_max_height(modEntryHeight);
|
||||
body_container->set_overflow_y(Overflow::Hidden);
|
||||
|
||||
{
|
||||
name_label = context.create_element<Label>(body_container, LabelStyle::Normal);
|
||||
description_label = context.create_element<Label>(body_container, LabelStyle::Small);
|
||||
description_label->set_margin_top(4.0f);
|
||||
description_label->set_color(Color{ COL_TEXT_DIM, 255 });
|
||||
} // body_container
|
||||
} // this
|
||||
|
||||
add_style(&checked_style, checked_state);
|
||||
add_style(&hover_style, hover_state);
|
||||
add_style(&checked_hover_style, { checked_state, hover_state });
|
||||
add_style(&pulsing_style, { focus_state });
|
||||
}
|
||||
|
||||
ModEntryView::~ModEntryView() {
|
||||
@@ -92,12 +109,32 @@ void ModEntryView::set_selected(bool selected) {
|
||||
set_style_enabled(checked_state, selected);
|
||||
}
|
||||
|
||||
void ModEntryView::set_focused(bool focused) {
|
||||
set_style_enabled(focus_state, focused);
|
||||
}
|
||||
|
||||
void ModEntryView::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Update:
|
||||
if (is_style_enabled(focus_state)) {
|
||||
pulsing_style.set_color(recompui::get_pulse_color(750));
|
||||
apply_styles();
|
||||
queue_update();
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ModEntryButton
|
||||
|
||||
ModEntryButton::ModEntryButton(Element *parent, uint32_t mod_index) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Focus, EventType::Drag)) {
|
||||
this->mod_index = mod_index;
|
||||
|
||||
set_drag(Drag::Drag);
|
||||
enable_focus();
|
||||
|
||||
ContextId context = get_current_context();
|
||||
view = context.create_element<ModEntryView>(this);
|
||||
@@ -131,16 +168,20 @@ void ModEntryButton::set_selected(bool selected) {
|
||||
view->set_selected(selected);
|
||||
}
|
||||
|
||||
void ModEntryButton::set_focused(bool focused) {
|
||||
view->set_focused(focused);
|
||||
view->queue_update();
|
||||
}
|
||||
|
||||
void ModEntryButton::process_event(const Event& e) {
|
||||
switch (e.type) {
|
||||
case EventType::Click:
|
||||
case EventType::Focus:
|
||||
selected_callback(mod_index);
|
||||
set_focused(std::get<EventFocus>(e.variant).active);
|
||||
break;
|
||||
case EventType::Hover:
|
||||
view->set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
|
||||
break;
|
||||
case EventType::Focus:
|
||||
break;
|
||||
case EventType::Drag:
|
||||
drag_callback(mod_index, std::get<EventDrag>(e.variant));
|
||||
break;
|
||||
@@ -205,13 +246,15 @@ void ModEntrySpacer::set_target_height(float target_height, bool animate_to_targ
|
||||
|
||||
// ModMenu
|
||||
|
||||
void ModMenu::refresh_mods() {
|
||||
void ModMenu::refresh_mods(bool scan_mods) {
|
||||
for (const std::string &thumbnail : loaded_thumbnails) {
|
||||
recompui::release_image(thumbnail);
|
||||
}
|
||||
|
||||
recomp::mods::scan_mods();
|
||||
mod_details = recomp::mods::get_mod_details(game_mod_id);
|
||||
if (scan_mods) {
|
||||
recomp::mods::scan_mods();
|
||||
}
|
||||
mod_details = recomp::mods::get_all_mod_details(game_mod_id);
|
||||
create_mod_list();
|
||||
}
|
||||
|
||||
@@ -221,13 +264,30 @@ void ModMenu::open_mods_folder() {
|
||||
std::wstring path_wstr = mods_directory.wstring();
|
||||
ShellExecuteW(NULL, L"open", path_wstr.c_str(), NULL, NULL, SW_SHOWDEFAULT);
|
||||
#elif defined(__linux__)
|
||||
std::string command = "xdg-open " + mods_directory.string() + " &";
|
||||
std::string command = "xdg-open \"" + mods_directory.string() + "\" &";
|
||||
std::system(command.c_str());
|
||||
#elif defined(__APPLE__)
|
||||
std::string command = "open \"" + mods_directory.string() + "\"";
|
||||
std::system(command.c_str());
|
||||
#else
|
||||
static_assert(false, "Not implemented for this platform.");
|
||||
#endif
|
||||
}
|
||||
|
||||
void ModMenu::open_install_dialog() {
|
||||
banjo::open_file_dialog_multiple([](bool success, const std::list<std::filesystem::path>& paths) {
|
||||
if (success) {
|
||||
ContextId old_context = recompui::try_close_current_context();
|
||||
|
||||
recompui::drop_files(paths);
|
||||
|
||||
if (old_context != ContextId::null()) {
|
||||
old_context.open();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ModMenu::mod_toggled(bool enabled) {
|
||||
if (active_mod_index >= 0) {
|
||||
recomp::mods::enable_mod(mod_details[active_mod_index].mod_id, enabled);
|
||||
@@ -255,11 +315,41 @@ void ModMenu::mod_selected(uint32_t mod_index) {
|
||||
bool configure_enabled = !config_schema.options.empty();
|
||||
mod_details_panel->set_mod_details(mod_details[mod_index], thumbnail_src, toggle_checked, toggle_enabled, auto_enabled, configure_enabled);
|
||||
mod_entry_buttons[active_mod_index]->set_selected(true);
|
||||
|
||||
mod_details_panel->setup_mod_navigation(mod_entry_buttons[mod_index]);
|
||||
|
||||
// Navigation from the bottom bar.
|
||||
Button *configure_button = mod_details_panel->get_configure_button();
|
||||
Toggle *enable_toggle = mod_details_panel->get_enable_toggle();
|
||||
if (configure_enabled) {
|
||||
refresh_button->set_nav(NavDirection::Up, configure_button);
|
||||
mods_folder_button->set_nav(NavDirection::Up, configure_button);
|
||||
}
|
||||
else if (toggle_enabled) {
|
||||
refresh_button->set_nav(NavDirection::Up, enable_toggle);
|
||||
mods_folder_button->set_nav(NavDirection::Up, enable_toggle);
|
||||
}
|
||||
else {
|
||||
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
}
|
||||
|
||||
// Navigation from the mod list.
|
||||
if (toggle_enabled) {
|
||||
mod_entry_buttons[active_mod_index]->set_nav(NavDirection::Right, enable_toggle);
|
||||
}
|
||||
else if (configure_enabled) {
|
||||
mod_entry_buttons[active_mod_index]->set_nav(NavDirection::Right, configure_button);
|
||||
}
|
||||
else {
|
||||
mod_entry_buttons[active_mod_index]->set_nav_none(NavDirection::Right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
|
||||
constexpr float spacer_height = 110.0f;
|
||||
constexpr float spacer_height = modEntryHeight + modEntryPadding * 2.0f;
|
||||
|
||||
switch (drag.phase) {
|
||||
case DragPhase::Start: {
|
||||
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
|
||||
@@ -274,6 +364,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
|
||||
float left = mod_entry_buttons[mod_index]->get_absolute_left() - get_absolute_left();
|
||||
float top = mod_entry_buttons[mod_index]->get_absolute_top() - (height / 2.0f); // TODO: Figure out why this adjustment is even necessary.
|
||||
mod_entry_buttons[mod_index]->set_display(Display::None);
|
||||
mod_entry_buttons[mod_index]->set_focused(false);
|
||||
mod_entry_floating_view->set_display(Display::Flex);
|
||||
mod_entry_floating_view->set_mod_details(mod_details[mod_index]);
|
||||
mod_entry_floating_view->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id));
|
||||
@@ -332,7 +423,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
|
||||
|
||||
// Re-order the mods and update all the details on the menu.
|
||||
recomp::mods::set_mod_index(game_mod_id, mod_details[mod_index].mod_id, mod_drag_target_index);
|
||||
mod_details = recomp::mods::get_mod_details(game_mod_id);
|
||||
mod_details = recomp::mods::get_all_mod_details(game_mod_id);
|
||||
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
|
||||
mod_entry_buttons[i]->set_mod_details(mod_details[i]);
|
||||
mod_entry_buttons[i]->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[i].mod_id));
|
||||
@@ -356,6 +447,22 @@ ContextId get_config_sub_menu_context_id() {
|
||||
return sub_menu_context;
|
||||
}
|
||||
|
||||
bool ModMenu::handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value) {
|
||||
if (banjo::renderer::is_texture_pack_enable_config_option(option, true)) {
|
||||
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
|
||||
|
||||
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options,
|
||||
[this](const std::string &id, uint32_t value) {
|
||||
mod_enum_option_changed(id, value);
|
||||
mod_hd_textures_enabled_changed(value);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ModMenu::mod_configure_requested() {
|
||||
if (active_mod_index >= 0) {
|
||||
// Record the context that was open when this function was called and close it.
|
||||
@@ -373,19 +480,26 @@ void ModMenu::mod_configure_requested() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handle_special_config_options(option, config_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (option.type) {
|
||||
case recomp::mods::ConfigOptionType::Enum: {
|
||||
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
|
||||
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options, std::bind(&ModMenu::mod_enum_option_changed, this, std::placeholders::_1, std::placeholders::_2));
|
||||
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options,
|
||||
[this](const std::string &id, uint32_t value){ mod_enum_option_changed(id, value); });
|
||||
break;
|
||||
}
|
||||
case recomp::mods::ConfigOptionType::Number: {
|
||||
const recomp::mods::ConfigOptionNumber &option_number = std::get<recomp::mods::ConfigOptionNumber>(option.variant);
|
||||
config_sub_menu->add_slider_option(option.id, option.name, option.description, std::get<double>(config_value), option_number.min, option_number.max, option_number.step, option_number.percent, std::bind(&ModMenu::mod_number_option_changed, this, std::placeholders::_1, std::placeholders::_2));
|
||||
config_sub_menu->add_slider_option(option.id, option.name, option.description, std::get<double>(config_value), option_number.min, option_number.max, option_number.step, option_number.percent,
|
||||
[this](const std::string &id, double value){ mod_number_option_changed(id, value); });
|
||||
break;
|
||||
}
|
||||
case recomp::mods::ConfigOptionType::String: {
|
||||
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value), std::bind(&ModMenu::mod_string_option_changed, this, std::placeholders::_1, std::placeholders::_2));
|
||||
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value),
|
||||
[this](const std::string &id, const std::string &value){ mod_string_option_changed(id, value); });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -424,6 +538,17 @@ void ModMenu::mod_number_option_changed(const std::string &id, double value) {
|
||||
}
|
||||
}
|
||||
|
||||
void ModMenu::mod_hd_textures_enabled_changed(uint32_t value) {
|
||||
if (active_mod_index >= 0) {
|
||||
if (value) {
|
||||
banjo::renderer::secondary_enable_texture_pack(mod_details[active_mod_index].mod_id);
|
||||
}
|
||||
else {
|
||||
banjo::renderer::secondary_disable_texture_pack(mod_details[active_mod_index].mod_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModMenu::create_mod_list() {
|
||||
ContextId context = get_current_context();
|
||||
|
||||
@@ -432,12 +557,14 @@ void ModMenu::create_mod_list() {
|
||||
mod_entry_buttons.clear();
|
||||
mod_entry_spacers.clear();
|
||||
|
||||
Toggle* enable_toggle = mod_details_panel->get_enable_toggle();
|
||||
|
||||
// Create the child elements for the list scroll.
|
||||
for (size_t mod_index = 0; mod_index < mod_details.size(); mod_index++) {
|
||||
const std::vector<char> &thumbnail = recomp::mods::get_mod_thumbnail(mod_details[mod_index].mod_id);
|
||||
std::string thumbnail_name = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id);
|
||||
if (!thumbnail.empty()) {
|
||||
recompui::queue_image_from_bytes(thumbnail_name, thumbnail);
|
||||
recompui::queue_image_from_bytes_file(thumbnail_name, thumbnail);
|
||||
loaded_thumbnails.emplace(thumbnail_name);
|
||||
}
|
||||
|
||||
@@ -445,14 +572,28 @@ void ModMenu::create_mod_list() {
|
||||
mod_entry_spacers.emplace_back(spacer);
|
||||
|
||||
ModEntryButton *mod_entry = context.create_element<ModEntryButton>(list_scroll_container, mod_index);
|
||||
mod_entry->set_mod_selected_callback(std::bind(&ModMenu::mod_selected, this, std::placeholders::_1));
|
||||
mod_entry->set_mod_drag_callback(std::bind(&ModMenu::mod_dragged, this, std::placeholders::_1, std::placeholders::_2));
|
||||
mod_entry->set_mod_selected_callback([this](uint32_t mod_index){ mod_selected(mod_index); });
|
||||
mod_entry->set_mod_drag_callback([this](uint32_t mod_index, recompui::EventDrag drag){ mod_dragged(mod_index, drag); });
|
||||
mod_entry->set_mod_details(mod_details[mod_index]);
|
||||
mod_entry->set_mod_thumbnail(thumbnail_name);
|
||||
mod_entry->set_mod_enabled(is_mod_enabled_or_auto(mod_details[mod_index].mod_id));
|
||||
mod_entry_buttons.emplace_back(mod_entry);
|
||||
}
|
||||
|
||||
if (!mod_entry_buttons.empty()) {
|
||||
mod_entry_buttons.front()->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
mod_entry_buttons.back()->set_nav(NavDirection::Down, install_mods_button);
|
||||
install_mods_button->set_nav(NavDirection::Up, mod_entry_buttons.back());
|
||||
}
|
||||
else {
|
||||
install_mods_button->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
}
|
||||
|
||||
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
|
||||
if (tabset && tabset->GetActiveTab() == recompui::config_tab_to_index(ConfigTab::Mods)) {
|
||||
recompui::set_config_tabset_mod_nav();
|
||||
}
|
||||
|
||||
// Add one extra spacer at the bottom.
|
||||
ModEntrySpacer *spacer = context.create_element<ModEntrySpacer>(list_scroll_container);
|
||||
mod_entry_spacers.emplace_back(spacer);
|
||||
@@ -467,6 +608,27 @@ void ModMenu::create_mod_list() {
|
||||
}
|
||||
}
|
||||
|
||||
void ModMenu::process_event(const Event &e) {
|
||||
if (e.type == EventType::Update) {
|
||||
if (mods_dirty) {
|
||||
refresh_mods(mod_scan_queued);
|
||||
mods_dirty = false;
|
||||
mod_scan_queued = false;
|
||||
}
|
||||
if (ultramodern::is_game_started()) {
|
||||
install_mods_button->set_enabled(false);
|
||||
refresh_button->set_enabled(false);
|
||||
}
|
||||
if (active_mod_index != -1) {
|
||||
bool auto_enabled = recomp::mods::is_mod_auto_enabled(mod_details[active_mod_index].mod_id);
|
||||
bool toggle_enabled = !auto_enabled && (mod_details[active_mod_index].runtime_toggleable || !ultramodern::is_game_started());
|
||||
if (!toggle_enabled) {
|
||||
mod_details_panel->disable_toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModMenu::ModMenu(Element *parent) : Element(parent) {
|
||||
game_mod_id = "bk";
|
||||
|
||||
@@ -498,8 +660,8 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
|
||||
} // list_container
|
||||
|
||||
mod_details_panel = context.create_element<ModDetailsPanel>(body_container);
|
||||
mod_details_panel->set_mod_toggled_callback(std::bind(&ModMenu::mod_toggled, this, std::placeholders::_1));
|
||||
mod_details_panel->set_mod_configure_pressed_callback(std::bind(&ModMenu::mod_configure_requested, this));
|
||||
mod_details_panel->set_mod_toggled_callback([this](bool enabled){ mod_toggled(enabled); });
|
||||
mod_details_panel->set_mod_configure_pressed_callback([this](){ mod_configure_requested(); });
|
||||
} // body_container
|
||||
|
||||
body_empty_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::SpaceBetween);
|
||||
@@ -511,23 +673,32 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
|
||||
context.create_element<Element>(body_empty_container);
|
||||
} // body_empty_container
|
||||
|
||||
footer_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceBetween);
|
||||
footer_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
|
||||
footer_container->set_width(100.0f, recompui::Unit::Percent);
|
||||
footer_container->set_align_items(recompui::AlignItems::Center);
|
||||
footer_container->set_background_color(Color{ 0, 0, 0, 89 });
|
||||
footer_container->set_border_top_width(1.1f);
|
||||
footer_container->set_border_top_color(Color{ 255, 255, 255, 25 });
|
||||
footer_container->set_padding(20.0f);
|
||||
footer_container->set_gap(20.0f);
|
||||
footer_container->set_border_bottom_left_radius(16.0f);
|
||||
footer_container->set_border_bottom_right_radius(16.0f);
|
||||
{
|
||||
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
|
||||
refresh_button->add_pressed_callback(std::bind(&ModMenu::refresh_mods, this));
|
||||
Button* configure_button = mod_details_panel->get_configure_button();
|
||||
install_mods_button = context.create_element<Button>(footer_container, "Install Mods", recompui::ButtonStyle::Primary);
|
||||
install_mods_button->add_pressed_callback([this](){ open_install_dialog(); });
|
||||
|
||||
context.create_element<Label>(footer_container, "⚠ UNDER CONSTRUCTION ⚠", LabelStyle::Small);
|
||||
Element* footer_spacer = context.create_element<Element>(footer_container);
|
||||
footer_spacer->set_flex(1.0f, 0.0f);
|
||||
|
||||
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
|
||||
refresh_button->add_pressed_callback([this](){ refresh_mods(true); });
|
||||
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
|
||||
mods_folder_button = context.create_element<Button>(footer_container, "Open Mods Folder", recompui::ButtonStyle::Primary);
|
||||
mods_folder_button->add_pressed_callback(std::bind(&ModMenu::open_mods_folder, this));
|
||||
mods_folder_button->add_pressed_callback([this](){ open_mods_folder(); });
|
||||
mods_folder_button->set_nav(NavDirection::Up, configure_button);
|
||||
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
} // footer_container
|
||||
} // this
|
||||
|
||||
@@ -536,11 +707,9 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
|
||||
mod_entry_floating_view->set_position(Position::Absolute);
|
||||
mod_entry_floating_view->set_selected(true);
|
||||
|
||||
refresh_mods();
|
||||
|
||||
context.close();
|
||||
|
||||
sub_menu_context = recompui::create_context("assets/config_sub_menu.rml");
|
||||
sub_menu_context = recompui::create_context(banjo::get_asset_path("config_sub_menu.rml"));
|
||||
sub_menu_context.open();
|
||||
Rml::ElementDocument* sub_menu_doc = sub_menu_context.get_document();
|
||||
Rml::Element* config_sub_menu_generic = sub_menu_doc->GetElementById("config_sub_menu");
|
||||
@@ -556,6 +725,64 @@ ModMenu::~ModMenu() {
|
||||
|
||||
// Placeholder class until the rest of the UI refactor is finished.
|
||||
|
||||
recompui::ModMenu* mod_menu;
|
||||
|
||||
void update_mod_list(bool scan_mods) {
|
||||
if (mod_menu) {
|
||||
recompui::ContextId ui_context = recompui::get_config_context_id();
|
||||
bool opened = ui_context.open_if_not_already();
|
||||
|
||||
mod_menu->set_mods_dirty(scan_mods);
|
||||
mod_menu->queue_update();
|
||||
|
||||
if (opened) {
|
||||
ui_context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void process_game_started() {
|
||||
if (mod_menu) {
|
||||
recompui::ContextId ui_context = recompui::get_config_context_id();
|
||||
bool opened = ui_context.open_if_not_already();
|
||||
|
||||
mod_menu->queue_update();
|
||||
|
||||
if (opened) {
|
||||
ui_context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void set_config_tabset_mod_nav() {
|
||||
if (mod_menu) {
|
||||
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
|
||||
Rml::Element* tabs = recompui::get_child_by_tag(tabset, "tabs");
|
||||
if (tabs != nullptr) {
|
||||
size_t num_children = tabs->GetNumChildren();
|
||||
Element* first_mod_entry = mod_menu->get_first_mod_entry();
|
||||
if (first_mod_entry != nullptr) {
|
||||
std::string id = "#" + first_mod_entry->get_id();
|
||||
for (size_t i = 0; i < num_children; i++) {
|
||||
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Property{ id, Rml::Unit::STRING });
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (size_t i = 0; i < num_children; i++) {
|
||||
tabs->GetChild(i)->SetProperty(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void focus_mod_configure_button() {
|
||||
Element* configure_button = mod_menu->get_mod_configure_button();
|
||||
if (configure_button) {
|
||||
configure_button->focus();
|
||||
}
|
||||
}
|
||||
|
||||
ElementModMenu::ElementModMenu(const Rml::String &tag) : Rml::Element(tag) {
|
||||
SetProperty("width", "100%");
|
||||
SetProperty("height", "100%");
|
||||
|
||||
+22
-4
@@ -18,14 +18,19 @@ public:
|
||||
void set_mod_thumbnail(const std::string &thumbnail);
|
||||
void set_mod_enabled(bool enabled);
|
||||
void set_selected(bool selected);
|
||||
void set_focused(bool focused);
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "ModEntryView"; }
|
||||
private:
|
||||
Image *thumbnail_image = nullptr;
|
||||
Container *body_container = nullptr;
|
||||
Element *body_container = nullptr;
|
||||
Label *name_label = nullptr;
|
||||
Label *description_label = nullptr;
|
||||
Style checked_style;
|
||||
Style hover_style;
|
||||
Style checked_hover_style;
|
||||
Style pulsing_style;
|
||||
};
|
||||
|
||||
class ModEntryButton : public Element {
|
||||
@@ -38,8 +43,10 @@ public:
|
||||
void set_mod_thumbnail(const std::string &thumbnail);
|
||||
void set_mod_enabled(bool enabled);
|
||||
void set_selected(bool selected);
|
||||
void set_focused(bool focused);
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "ModEntryButton"; }
|
||||
private:
|
||||
uint32_t mod_index = 0;
|
||||
ModEntryView *view = nullptr;
|
||||
@@ -56,6 +63,7 @@ private:
|
||||
void check_height_distance();
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "ModEntrySpacer"; }
|
||||
public:
|
||||
ModEntrySpacer(Element *parent);
|
||||
void set_target_height(float target_height, bool animate_to_target);
|
||||
@@ -65,17 +73,26 @@ class ModMenu : public Element {
|
||||
public:
|
||||
ModMenu(Element *parent);
|
||||
virtual ~ModMenu();
|
||||
void set_mods_dirty(bool scan_mods) { mods_dirty = true; mod_scan_queued = scan_mods; }
|
||||
Element* get_first_mod_entry() { return !mod_entry_buttons.empty() ? mod_entry_buttons[0] : nullptr; }
|
||||
Element* get_mod_configure_button() { return mod_details_panel != nullptr ? mod_details_panel->get_configure_button() : nullptr; }
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ModMenu"; }
|
||||
private:
|
||||
void refresh_mods();
|
||||
void refresh_mods(bool scan_mods);
|
||||
void open_mods_folder();
|
||||
void open_install_dialog();
|
||||
void mod_toggled(bool enabled);
|
||||
void mod_selected(uint32_t mod_index);
|
||||
void mod_dragged(uint32_t mod_index, EventDrag drag);
|
||||
void mod_configure_requested();
|
||||
bool handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value);
|
||||
void mod_enum_option_changed(const std::string &id, uint32_t value);
|
||||
void mod_string_option_changed(const std::string &id, const std::string &value);
|
||||
void mod_number_option_changed(const std::string &id, double value);
|
||||
void mod_hd_textures_enabled_changed(uint32_t value);
|
||||
void create_mod_list();
|
||||
void process_event(const Event &e) override;
|
||||
|
||||
Container *body_container = nullptr;
|
||||
Container *list_container = nullptr;
|
||||
@@ -83,6 +100,7 @@ private:
|
||||
ModDetailsPanel *mod_details_panel = nullptr;
|
||||
Container *body_empty_container = nullptr;
|
||||
Container *footer_container = nullptr;
|
||||
Button *install_mods_button = nullptr;
|
||||
Button *refresh_button = nullptr;
|
||||
Button *mods_folder_button = nullptr;
|
||||
int32_t active_mod_index = -1;
|
||||
@@ -96,6 +114,8 @@ private:
|
||||
std::vector<recomp::mods::ModDetails> mod_details{};
|
||||
std::unordered_set<std::string> loaded_thumbnails;
|
||||
std::string game_mod_id;
|
||||
bool mods_dirty = false;
|
||||
bool mod_scan_queued = false;
|
||||
|
||||
ConfigSubMenu *config_sub_menu;
|
||||
};
|
||||
@@ -104,8 +124,6 @@ class ElementModMenu : public Rml::Element {
|
||||
public:
|
||||
ElementModMenu(const Rml::String& tag);
|
||||
virtual ~ElementModMenu();
|
||||
private:
|
||||
ModMenu *mod_menu;
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
#include <mutex>
|
||||
|
||||
#include "recomp_ui.h"
|
||||
|
||||
#include "elements/ui_element.h"
|
||||
#include "elements/ui_label.h"
|
||||
#include "elements/ui_button.h"
|
||||
|
||||
struct {
|
||||
recompui::ContextId ui_context;
|
||||
recompui::Label* prompt_header;
|
||||
recompui::Label* prompt_label;
|
||||
recompui::Element* prompt_controls;
|
||||
recompui::Button* confirm_button;
|
||||
recompui::Button* cancel_button;
|
||||
std::function<void()> confirm_action;
|
||||
std::function<void()> cancel_action;
|
||||
std::string return_element_id;
|
||||
std::mutex mutex;
|
||||
} prompt_state;
|
||||
|
||||
void run_confirm_callback() {
|
||||
std::function<void()> confirm_action;
|
||||
{
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
confirm_action = std::move(prompt_state.confirm_action);
|
||||
}
|
||||
if (confirm_action) {
|
||||
confirm_action();
|
||||
}
|
||||
recompui::hide_context(prompt_state.ui_context);
|
||||
|
||||
// TODO nav: focus on return_element_id
|
||||
// or just remove it as the usage of the prompt can change now
|
||||
}
|
||||
|
||||
void run_cancel_callback() {
|
||||
std::function<void()> cancel_action;
|
||||
{
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
cancel_action = std::move(prompt_state.cancel_action);
|
||||
}
|
||||
if (cancel_action) {
|
||||
cancel_action();
|
||||
}
|
||||
recompui::hide_context(prompt_state.ui_context);
|
||||
|
||||
// TODO nav: focus on return_element_id
|
||||
// or just remove it as the usage of the prompt can change now
|
||||
}
|
||||
|
||||
void recompui::init_prompt_context() {
|
||||
ContextId context = create_context();
|
||||
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
|
||||
context.open();
|
||||
|
||||
prompt_state.ui_context = context;
|
||||
|
||||
Element* window = context.create_element<Element>(context.get_root_element());
|
||||
window->set_display(Display::Flex);
|
||||
window->set_flex_direction(FlexDirection::Column);
|
||||
window->set_background_color({0, 0, 0, 0});
|
||||
|
||||
Element* prompt_overlay = context.create_element<Element>(window);
|
||||
prompt_overlay->set_background_color(Color{ 190, 184, 219, 25 });
|
||||
prompt_overlay->set_position(Position::Absolute);
|
||||
prompt_overlay->set_top(0);
|
||||
prompt_overlay->set_right(0);
|
||||
prompt_overlay->set_bottom(0);
|
||||
prompt_overlay->set_left(0);
|
||||
|
||||
Element* prompt_content_wrapper = context.create_element<Element>(window);
|
||||
prompt_content_wrapper->set_display(Display::Flex);
|
||||
prompt_content_wrapper->set_position(Position::Absolute);
|
||||
prompt_content_wrapper->set_top(0);
|
||||
prompt_content_wrapper->set_right(0);
|
||||
prompt_content_wrapper->set_bottom(0);
|
||||
prompt_content_wrapper->set_left(0);
|
||||
prompt_content_wrapper->set_align_items(AlignItems::Center);
|
||||
prompt_content_wrapper->set_justify_content(JustifyContent::Center);
|
||||
|
||||
Element* prompt_content = context.create_element<Element>(prompt_content_wrapper);
|
||||
prompt_content->set_display(Display::Flex);
|
||||
prompt_content->set_position(Position::Relative);
|
||||
prompt_content->set_flex(1.0f, 1.0f);
|
||||
prompt_content->set_flex_basis(100, Unit::Percent);
|
||||
prompt_content->set_flex_direction(FlexDirection::Column);
|
||||
prompt_content->set_width(100, Unit::Percent);
|
||||
prompt_content->set_max_width(700, Unit::Dp);
|
||||
prompt_content->set_height_auto();
|
||||
prompt_content->set_margin_auto();
|
||||
prompt_content->set_border_width(1.1, Unit::Dp);
|
||||
prompt_content->set_border_radius(16, Unit::Dp);
|
||||
prompt_content->set_border_color(Color{ 255, 255, 255, 51 });
|
||||
prompt_content->set_background_color(Color{ 8, 7, 13, 229 });
|
||||
|
||||
prompt_state.prompt_header = context.create_element<Label>(prompt_content, "", LabelStyle::Large);
|
||||
prompt_state.prompt_header->set_margin(24, Unit::Dp);
|
||||
|
||||
prompt_state.prompt_label = context.create_element<Label>(prompt_content, "", LabelStyle::Small);
|
||||
prompt_state.prompt_label->set_margin(24, Unit::Dp);
|
||||
prompt_state.prompt_label->set_margin_top(0);
|
||||
|
||||
prompt_state.prompt_controls = context.create_element<Element>(prompt_content);
|
||||
|
||||
prompt_state.prompt_controls->set_display(Display::Flex);
|
||||
prompt_state.prompt_controls->set_flex_direction(FlexDirection::Row);
|
||||
prompt_state.prompt_controls->set_justify_content(JustifyContent::Center);
|
||||
prompt_state.prompt_controls->set_padding_top(24, Unit::Dp);
|
||||
prompt_state.prompt_controls->set_padding_bottom(24, Unit::Dp);
|
||||
prompt_state.prompt_controls->set_padding_left(12, Unit::Dp);
|
||||
prompt_state.prompt_controls->set_padding_right(12, Unit::Dp);
|
||||
prompt_state.prompt_controls->set_border_top_width(1.1, Unit::Dp);
|
||||
prompt_state.prompt_controls->set_border_top_color({ 255, 255, 255, 25 });
|
||||
|
||||
prompt_state.confirm_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
|
||||
prompt_state.confirm_button->set_min_width(185.0f, Unit::Dp);
|
||||
prompt_state.confirm_button->set_margin_top(0);
|
||||
prompt_state.confirm_button->set_margin_bottom(0);
|
||||
prompt_state.confirm_button->set_margin_left(12, Unit::Dp);
|
||||
prompt_state.confirm_button->set_margin_right(12, Unit::Dp);
|
||||
prompt_state.confirm_button->set_text_align(TextAlign::Center);
|
||||
prompt_state.confirm_button->set_color(Color{ 204, 204, 204, 255 });
|
||||
prompt_state.confirm_button->add_pressed_callback(run_confirm_callback);
|
||||
|
||||
Style* confirm_hover_style = prompt_state.confirm_button->get_hover_style();
|
||||
confirm_hover_style->set_border_color(Color{ 69, 208, 67, 255 });
|
||||
confirm_hover_style->set_background_color(Color{ 69, 208, 67, 76 });
|
||||
confirm_hover_style->set_color(Color{ 242, 242, 242, 255 });
|
||||
|
||||
Style* confirm_focus_style = prompt_state.confirm_button->get_focus_style();
|
||||
confirm_focus_style->set_border_color(Color{ 69, 208, 67, 255 });
|
||||
confirm_focus_style->set_background_color(Color{ 69, 208, 67, 76 });
|
||||
confirm_focus_style->set_color(Color{ 242, 242, 242, 255 });
|
||||
|
||||
prompt_state.cancel_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
|
||||
prompt_state.cancel_button->set_min_width(185.0f, Unit::Dp);
|
||||
prompt_state.cancel_button->set_margin_top(0);
|
||||
prompt_state.cancel_button->set_margin_bottom(0);
|
||||
prompt_state.cancel_button->set_margin_left(12, Unit::Dp);
|
||||
prompt_state.cancel_button->set_margin_right(12, Unit::Dp);
|
||||
prompt_state.cancel_button->set_text_align(TextAlign::Center);
|
||||
prompt_state.cancel_button->set_color(Color{ 204, 204, 204, 255 });
|
||||
prompt_state.cancel_button->add_pressed_callback(run_cancel_callback);
|
||||
|
||||
Style* cancel_hover_style = prompt_state.cancel_button->get_hover_style();
|
||||
cancel_hover_style->set_border_color(Color{ 248, 96, 57, 255 });
|
||||
cancel_hover_style->set_background_color(Color{ 248, 96, 57, 76 });
|
||||
cancel_hover_style->set_color(Color{ 242, 242, 242, 255 });
|
||||
|
||||
Style* cancel_focus_style = prompt_state.cancel_button->get_focus_style();
|
||||
cancel_focus_style->set_border_color(Color{ 248, 96, 57, 255 });
|
||||
cancel_focus_style->set_background_color(Color{ 248, 96, 57, 76 });
|
||||
cancel_focus_style->set_color(Color{ 242, 242, 242, 255 });
|
||||
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
void style_button(recompui::Button* button, recompui::ButtonVariant variant) {
|
||||
recompui::Color button_color{};
|
||||
|
||||
switch (variant) {
|
||||
case recompui::ButtonVariant::Primary:
|
||||
button_color = { 185, 125, 242, 255 };
|
||||
break;
|
||||
case recompui::ButtonVariant::Secondary:
|
||||
button_color = { 23, 214, 232, 255 };
|
||||
break;
|
||||
case recompui::ButtonVariant::Tertiary:
|
||||
button_color = { 242, 242, 242, 255 };
|
||||
break;
|
||||
case recompui::ButtonVariant::Success:
|
||||
button_color = { 69, 208, 67, 255 };
|
||||
break;
|
||||
case recompui::ButtonVariant::Error:
|
||||
button_color = { 248, 96, 57, 255 };
|
||||
break;
|
||||
case recompui::ButtonVariant::Warning:
|
||||
button_color = { 233, 205, 53, 255 };
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
|
||||
recompui::Color border_color = button_color;
|
||||
recompui::Color background_color = button_color;
|
||||
border_color.a = 0.8f * 255;
|
||||
background_color.a = 0.05f * 255;
|
||||
button->set_border_color(border_color);
|
||||
button->set_background_color(background_color);
|
||||
|
||||
recompui::Color hover_border_color = button_color;
|
||||
recompui::Color hover_background_color = button_color;
|
||||
hover_border_color.a = 255;
|
||||
hover_background_color.a = 0.3f * 255;
|
||||
recompui::Style* hover_style = button->get_hover_style();
|
||||
hover_style->set_border_color(hover_border_color);
|
||||
hover_style->set_background_color(hover_background_color);
|
||||
|
||||
recompui::Style* focus_style = button->get_focus_style();
|
||||
focus_style->set_border_color(hover_border_color);
|
||||
focus_style->set_background_color(hover_background_color);
|
||||
|
||||
recompui::Color disabled_color { 255, 255, 255, 0.6f * 255 };
|
||||
recompui::Style* disabled_style = button->get_disabled_style();
|
||||
disabled_style->set_color(disabled_color);
|
||||
}
|
||||
|
||||
// Must be called while prompt_state.mutex is locked.
|
||||
void show_prompt(std::function<void()>& prev_cancel_action, bool focus_on_cancel) {
|
||||
if (focus_on_cancel) {
|
||||
prompt_state.ui_context.set_autofocus_element(prompt_state.cancel_button);
|
||||
}
|
||||
else {
|
||||
prompt_state.ui_context.set_autofocus_element(prompt_state.confirm_button);
|
||||
}
|
||||
|
||||
if (!recompui::is_context_shown(prompt_state.ui_context)) {
|
||||
recompui::show_context(prompt_state.ui_context, "");
|
||||
}
|
||||
else {
|
||||
// Call the previous cancel action to effectively close the previous prompt.
|
||||
if (prev_cancel_action) {
|
||||
prev_cancel_action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void recompui::open_choice_prompt(
|
||||
const std::string& header_text,
|
||||
const std::string& content_text,
|
||||
const std::string& confirm_label_text,
|
||||
const std::string& cancel_label_text,
|
||||
std::function<void()> confirm_action,
|
||||
std::function<void()> cancel_action,
|
||||
ButtonVariant confirm_variant,
|
||||
ButtonVariant cancel_variant,
|
||||
bool focus_on_cancel,
|
||||
const std::string& return_element_id
|
||||
) {
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
|
||||
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
|
||||
|
||||
ContextId prev_context = try_close_current_context();
|
||||
|
||||
prompt_state.ui_context.open();
|
||||
|
||||
prompt_state.prompt_header->set_text(header_text);
|
||||
prompt_state.prompt_label->set_text(content_text);
|
||||
prompt_state.prompt_controls->set_display(Display::Flex);
|
||||
prompt_state.confirm_button->set_display(Display::Block);
|
||||
prompt_state.cancel_button->set_display(Display::Block);
|
||||
prompt_state.confirm_button->set_text(confirm_label_text);
|
||||
prompt_state.cancel_button->set_text(cancel_label_text);
|
||||
prompt_state.confirm_action = confirm_action;
|
||||
prompt_state.cancel_action = cancel_action;
|
||||
prompt_state.return_element_id = return_element_id;
|
||||
|
||||
style_button(prompt_state.confirm_button, confirm_variant);
|
||||
style_button(prompt_state.cancel_button, cancel_variant);
|
||||
|
||||
prompt_state.ui_context.close();
|
||||
|
||||
if (prev_context != ContextId::null()) {
|
||||
prev_context.open();
|
||||
}
|
||||
|
||||
show_prompt(prev_cancel_action, focus_on_cancel);
|
||||
}
|
||||
|
||||
void recompui::open_info_prompt(
|
||||
const std::string& header_text,
|
||||
const std::string& content_text,
|
||||
const std::string& okay_label_text,
|
||||
std::function<void()> okay_action,
|
||||
ButtonVariant okay_variant,
|
||||
const std::string& return_element_id
|
||||
) {
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
|
||||
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
|
||||
|
||||
ContextId prev_context = try_close_current_context();
|
||||
|
||||
prompt_state.ui_context.open();
|
||||
|
||||
prompt_state.prompt_header->set_text(header_text);
|
||||
prompt_state.prompt_label->set_text(content_text);
|
||||
prompt_state.prompt_controls->set_display(Display::Flex);
|
||||
prompt_state.confirm_button->set_display(Display::None);
|
||||
prompt_state.cancel_button->set_display(Display::Block);
|
||||
prompt_state.cancel_button->set_text(okay_label_text);
|
||||
prompt_state.confirm_action = {};
|
||||
prompt_state.cancel_action = okay_action;
|
||||
prompt_state.return_element_id = return_element_id;
|
||||
|
||||
style_button(prompt_state.cancel_button, okay_variant);
|
||||
|
||||
prompt_state.ui_context.close();
|
||||
|
||||
if (prev_context != ContextId::null()) {
|
||||
prev_context.open();
|
||||
}
|
||||
|
||||
show_prompt(prev_cancel_action, true);
|
||||
}
|
||||
|
||||
void recompui::open_notification(
|
||||
const std::string& header_text,
|
||||
const std::string& content_text,
|
||||
const std::string& return_element_id
|
||||
) {
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
|
||||
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
|
||||
|
||||
ContextId prev_context = try_close_current_context();
|
||||
|
||||
prompt_state.ui_context.open();
|
||||
|
||||
prompt_state.prompt_header->set_text(header_text);
|
||||
prompt_state.prompt_label->set_text(content_text);
|
||||
prompt_state.prompt_controls->set_display(Display::None);
|
||||
prompt_state.confirm_button->set_display(Display::None);
|
||||
prompt_state.cancel_button->set_display(Display::None);
|
||||
prompt_state.confirm_action = {};
|
||||
prompt_state.cancel_action = {};
|
||||
prompt_state.return_element_id = return_element_id;
|
||||
|
||||
prompt_state.ui_context.close();
|
||||
|
||||
if (prev_context != ContextId::null()) {
|
||||
prev_context.open();
|
||||
}
|
||||
|
||||
show_prompt(prev_cancel_action, false);
|
||||
}
|
||||
|
||||
void recompui::close_prompt() {
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
|
||||
if (recompui::is_context_shown(prompt_state.ui_context)) {
|
||||
if (prompt_state.cancel_action) {
|
||||
prompt_state.cancel_action();
|
||||
}
|
||||
|
||||
recompui::hide_context(prompt_state.ui_context);
|
||||
}
|
||||
}
|
||||
|
||||
bool recompui::is_prompt_open() {
|
||||
std::lock_guard lock{ prompt_state.mutex };
|
||||
|
||||
return recompui::is_context_shown(prompt_state.ui_context);
|
||||
}
|
||||
+80
-13
@@ -22,6 +22,9 @@
|
||||
#ifdef _WIN32
|
||||
# include "InterfaceVS.hlsl.dxil.h"
|
||||
# include "InterfacePS.hlsl.dxil.h"
|
||||
#elif defined(__APPLE__)
|
||||
# include "InterfaceVS.hlsl.metal.h"
|
||||
# include "InterfacePS.hlsl.metal.h"
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -31,6 +34,13 @@
|
||||
# define GET_SHADER_SIZE(name, format) \
|
||||
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
|
||||
(format) == RT64::RenderShaderFormat::DXIL ? std::size(name##BlobDXIL) : 0)
|
||||
#elif defined(__APPLE__)
|
||||
# define GET_SHADER_BLOB(name, format) \
|
||||
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : \
|
||||
(format) == RT64::RenderShaderFormat::METAL ? name##BlobMSL : nullptr)
|
||||
# define GET_SHADER_SIZE(name, format) \
|
||||
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
|
||||
(format) == RT64::RenderShaderFormat::METAL ? std::size(name##BlobMSL) : 0)
|
||||
#else
|
||||
# define GET_SHADER_BLOB(name, format) \
|
||||
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : nullptr)
|
||||
@@ -62,7 +72,19 @@ T from_bytes_le(const char* input) {
|
||||
return *reinterpret_cast<const T*>(input);
|
||||
}
|
||||
|
||||
typedef std::pair<std::string, std::vector<char>> ImageFromBytes;
|
||||
enum class ImageType {
|
||||
File,
|
||||
RGBA32
|
||||
};
|
||||
|
||||
struct ImageFromBytes {
|
||||
ImageType type;
|
||||
// Dimensions only used for RGBA32 data. Files pull the size from the file data.
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
std::string name;
|
||||
std::vector<char> bytes;
|
||||
};
|
||||
|
||||
namespace recompui {
|
||||
class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
|
||||
@@ -128,7 +150,7 @@ class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
|
||||
bool scissor_enabled_ = false;
|
||||
std::vector<std::unique_ptr<RT64::RenderBuffer>> stale_buffers_{};
|
||||
moodycamel::ConcurrentQueue<ImageFromBytes> image_from_bytes_queue;
|
||||
std::unordered_map<std::string, std::vector<char>> image_from_bytes_map;
|
||||
std::unordered_map<std::string, ImageFromBytes> image_from_bytes_map;
|
||||
public:
|
||||
RmlRenderInterface_RT64_impl(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
|
||||
interface_ = interface;
|
||||
@@ -201,7 +223,19 @@ public:
|
||||
|
||||
// Create the pipeline description
|
||||
RT64::RenderGraphicsPipelineDesc pipeline_desc{};
|
||||
pipeline_desc.renderTargetBlend[0] = RT64::RenderBlendDesc::AlphaBlend();
|
||||
// Set up alpha blending for non-premultiplied alpha. RmlUi recommends using premultiplied alpha normally,
|
||||
// but that would require preprocessing all input files, which would be difficult for user-provided content (such as mods).
|
||||
// This blending setup produces similar results as premultipled alpha but for normal assets as it multiplies during blending and
|
||||
// computes the output alpha value the same way that a premultipled alpha blender would.
|
||||
pipeline_desc.renderTargetBlend[0] = RT64::RenderBlendDesc {
|
||||
.blendEnabled = true,
|
||||
.srcBlend = RT64::RenderBlend::SRC_ALPHA,
|
||||
.dstBlend = RT64::RenderBlend::INV_SRC_ALPHA,
|
||||
.blendOp = RT64::RenderBlendOperation::ADD,
|
||||
.srcBlendAlpha = RT64::RenderBlend::ONE,
|
||||
.dstBlendAlpha = RT64::RenderBlend::INV_SRC_ALPHA,
|
||||
.blendOpAlpha = RT64::RenderBlendOperation::ADD,
|
||||
};
|
||||
pipeline_desc.renderTargetFormat[0] = SwapChainFormat; // TODO: Use whatever format the swap chain was created with.
|
||||
pipeline_desc.renderTargetCount = 1;
|
||||
pipeline_desc.cullMode = RT64::RenderCullMode::NONE;
|
||||
@@ -236,7 +270,7 @@ public:
|
||||
}
|
||||
|
||||
copy_command_queue_ = device->createCommandQueue(RT64::RenderCommandListType::COPY);
|
||||
copy_command_list_ = device->createCommandList(RT64::RenderCommandListType::COPY);
|
||||
copy_command_list_ = copy_command_queue_->createCommandList(RT64::RenderCommandListType::COPY);
|
||||
copy_command_fence_ = device->createCommandFence();
|
||||
}
|
||||
|
||||
@@ -393,11 +427,30 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input.
|
||||
std::vector<uint8_t> data_copy(it->second.data(), it->second.data() + it->second.size());
|
||||
RT64::Texture* texture = nullptr;
|
||||
std::unique_ptr<RT64::RenderBuffer> texture_buffer;
|
||||
ImageFromBytes& img = it->second;
|
||||
copy_command_list_->begin();
|
||||
RT64::Texture *texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer);
|
||||
|
||||
switch (img.type) {
|
||||
case ImageType::RGBA32:
|
||||
{
|
||||
// Read the image header (two 32-bit values for width and height respectively).
|
||||
uint32_t rowPitch = img.width * 4;
|
||||
size_t byteCount = img.height * rowPitch;
|
||||
texture = new RT64::Texture();
|
||||
RT64::TextureCache::setRGBA32(texture, device_, copy_command_list_.get(), reinterpret_cast<const uint8_t*>(img.bytes.data()), byteCount, img.width, img.height, rowPitch, texture_buffer, nullptr);
|
||||
}
|
||||
break;
|
||||
case ImageType::File:
|
||||
{
|
||||
// TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input.
|
||||
std::vector<uint8_t> data_copy(img.bytes.data(), img.bytes.data() + img.bytes.size());
|
||||
texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
copy_command_list_->end();
|
||||
copy_command_queue_->executeCommandLists(copy_command_list_.get(), copy_command_fence_.get());
|
||||
copy_command_queue_->waitForCommandFence(copy_command_fence_.get());
|
||||
@@ -585,6 +638,7 @@ public:
|
||||
list->setGraphicsPipelineLayout(layout_.get());
|
||||
list->setGraphicsDescriptorSet(sampler_set_.get(), 0);
|
||||
list->setGraphicsDescriptorSet(screen_descriptor_set_.get(), 1);
|
||||
list->setScissors(RT64::RenderRect{ 0, 0, window_width_, window_height_ });
|
||||
RT64::RenderVertexBufferView vertex_view(screen_vertex_buffer_.get(), screen_vertex_buffer_size_);
|
||||
list->setVertexBuffers(0, &vertex_view, 1, &vertex_slot_);
|
||||
|
||||
@@ -604,14 +658,21 @@ public:
|
||||
list_ = nullptr;
|
||||
}
|
||||
|
||||
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
|
||||
image_from_bytes_queue.enqueue(ImageFromBytes(src, bytes));
|
||||
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
|
||||
// Width and height aren't used for file images, so set them to 0.
|
||||
image_from_bytes_queue.enqueue(ImageFromBytes{ .type = ImageType::File, .width = 0, .height = 0, .name = src, .bytes = bytes });
|
||||
}
|
||||
|
||||
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
|
||||
image_from_bytes_queue.enqueue(ImageFromBytes{ .type = ImageType::RGBA32, .width = width, .height = height, .name = src, .bytes = bytes });
|
||||
}
|
||||
|
||||
void flush_image_from_bytes_queue() {
|
||||
ImageFromBytes image_from_bytes;
|
||||
while (image_from_bytes_queue.try_dequeue(image_from_bytes)) {
|
||||
image_from_bytes_map.emplace(image_from_bytes.first, std::move(image_from_bytes.second));
|
||||
// We can move the name into the map since the name in the actual entry is no longer needed.
|
||||
// After that, move the entry itself into the map.
|
||||
image_from_bytes_map.emplace(std::move(image_from_bytes.name), std::move(image_from_bytes));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -647,8 +708,14 @@ void recompui::RmlRenderInterface_RT64::end(RT64::RenderCommandList* list, RT64:
|
||||
impl->end(list, framebuffer);
|
||||
}
|
||||
|
||||
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
|
||||
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
|
||||
assert(static_cast<bool>(impl));
|
||||
|
||||
impl->queue_image_from_bytes(src, bytes);
|
||||
}
|
||||
impl->queue_image_from_bytes_file(src, bytes);
|
||||
}
|
||||
|
||||
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
|
||||
assert(static_cast<bool>(impl));
|
||||
|
||||
impl->queue_image_from_bytes_rgba32(src, bytes, width, height);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define __UI_RENDERER_H__
|
||||
|
||||
#include <memory>
|
||||
#include "recomp_ui.h"
|
||||
|
||||
namespace RT64 {
|
||||
struct RenderInterface;
|
||||
@@ -29,7 +30,8 @@ namespace recompui {
|
||||
|
||||
void start(RT64::RenderCommandList* list, int image_width, int image_height);
|
||||
void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer);
|
||||
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes);
|
||||
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes);
|
||||
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height);
|
||||
};
|
||||
} // namespace recompui
|
||||
|
||||
|
||||
+252
-34
@@ -4,6 +4,7 @@
|
||||
#else
|
||||
#include <SDL2/SDL_video.h>
|
||||
#endif
|
||||
#include <chrono>
|
||||
|
||||
#include "rt64_render_hooks.h"
|
||||
|
||||
@@ -19,9 +20,11 @@
|
||||
#include "recomp_input.h"
|
||||
#include "librecomp/game.hpp"
|
||||
#include "banjo_config.h"
|
||||
#include "banjo_support.h"
|
||||
#include "ui_rml_hacks.hpp"
|
||||
#include "ui_elements.h"
|
||||
#include "ui_mod_menu.h"
|
||||
#include "ui_mod_installer.h"
|
||||
#include "ui_renderer.h"
|
||||
|
||||
bool can_focus(Rml::Element* element) {
|
||||
@@ -150,7 +153,6 @@ Rml::Element* find_autofocus_element(Rml::Element* start) {
|
||||
struct ContextDetails {
|
||||
recompui::ContextId context;
|
||||
Rml::ElementDocument* document;
|
||||
bool takes_input;
|
||||
};
|
||||
|
||||
class UIState {
|
||||
@@ -209,8 +211,6 @@ public:
|
||||
|
||||
Rml::Debugger::Initialise(context);
|
||||
{
|
||||
const Rml::String directory = "assets/";
|
||||
|
||||
struct FontFace {
|
||||
const char* filename;
|
||||
bool fallback_face;
|
||||
@@ -225,14 +225,17 @@ public:
|
||||
};
|
||||
|
||||
for (const FontFace& face : font_faces) {
|
||||
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
|
||||
auto font = banjo::get_asset_path(face.filename);
|
||||
Rml::LoadFontFace(font.string(), face.fallback_face);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void load_documents() {
|
||||
launcher_menu_controller->load_document(context);
|
||||
config_menu_controller->load_document(context);
|
||||
void create_menus() {
|
||||
recompui::init_styling(banjo::get_asset_path("recomp.rcss"));
|
||||
launcher_menu_controller->load_document();
|
||||
config_menu_controller->load_document();
|
||||
recompui::init_prompt_context();
|
||||
}
|
||||
|
||||
void unload() {
|
||||
@@ -264,7 +267,7 @@ public:
|
||||
recompui::set_cursor_visible(mouse_is_active);
|
||||
}
|
||||
|
||||
Rml::ElementDocument* current_document = top_input_document();
|
||||
Rml::ElementDocument* current_document = top_mouse_document();
|
||||
if (current_document == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -284,7 +287,7 @@ public:
|
||||
}
|
||||
|
||||
void update_focus(bool mouse_moved, bool non_mouse_interacted) {
|
||||
Rml::ElementDocument* current_document = top_input_document();
|
||||
Rml::ElementDocument* current_document = top_mouse_document();
|
||||
|
||||
if (current_document == nullptr) {
|
||||
return;
|
||||
@@ -339,12 +342,10 @@ public:
|
||||
recompui::message_box("Attemped to show the same context twice");
|
||||
assert(false);
|
||||
}
|
||||
bool takes_input = context.takes_input();
|
||||
Rml::ElementDocument* document = context.get_document();
|
||||
shown_contexts.push_back(ContextDetails{
|
||||
.context = context,
|
||||
.document = document,
|
||||
.takes_input = takes_input
|
||||
.document = document
|
||||
});
|
||||
|
||||
// auto& on_show = context.on_show;
|
||||
@@ -356,6 +357,10 @@ public:
|
||||
|
||||
document->PullToFront();
|
||||
document->Show();
|
||||
recompui::Element* default_element = context.get_autofocus_element();
|
||||
if (default_element) {
|
||||
default_element->focus();
|
||||
}
|
||||
}
|
||||
|
||||
void hide_context(recompui::ContextId context) {
|
||||
@@ -381,8 +386,12 @@ public:
|
||||
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [context](auto& c){ return c.context == context; }) != shown_contexts.end();
|
||||
}
|
||||
|
||||
bool is_context_taking_input() {
|
||||
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.takes_input; }) != shown_contexts.end();
|
||||
bool is_context_capturing_input() {
|
||||
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.context.captures_input(); }) != shown_contexts.end();
|
||||
}
|
||||
|
||||
bool is_context_capturing_mouse() {
|
||||
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.context.captures_mouse(); }) != shown_contexts.end();
|
||||
}
|
||||
|
||||
bool is_any_context_shown() {
|
||||
@@ -392,7 +401,17 @@ public:
|
||||
Rml::ElementDocument* top_input_document() {
|
||||
// Iterate backwards and stop at the first context that takes input.
|
||||
for (auto it = shown_contexts.rbegin(); it != shown_contexts.rend(); it++) {
|
||||
if (it->takes_input) {
|
||||
if (it->context.captures_input()) {
|
||||
return it->document;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Rml::ElementDocument* top_mouse_document() {
|
||||
// Iterate backwards and stop at the first context that takes input.
|
||||
for (auto it = shown_contexts.rbegin(); it != shown_contexts.rend(); it++) {
|
||||
if (it->context.captures_mouse()) {
|
||||
return it->document;
|
||||
}
|
||||
}
|
||||
@@ -430,7 +449,7 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
|
||||
std::locale::global(std::locale::classic());
|
||||
#endif
|
||||
ui_state = std::make_unique<UIState>(window, interface, device);
|
||||
ui_state->load_documents();
|
||||
ui_state->create_menus();
|
||||
}
|
||||
|
||||
moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
|
||||
@@ -553,9 +572,28 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
|
||||
|
||||
bool config_was_open = recompui::is_context_shown(recompui::get_config_context_id()) || recompui::is_context_shown(recompui::get_config_sub_menu_context_id());
|
||||
|
||||
using clock = std::chrono::system_clock;
|
||||
|
||||
// TODO move these into a more appropriate place.
|
||||
constexpr clock::duration start_repeat_delay = std::chrono::milliseconds{500};
|
||||
constexpr clock::duration repeat_rate = std::chrono::milliseconds{50};
|
||||
static clock::time_point next_repeat_time = {};
|
||||
static int latest_controller_key_pressed = SDLK_UNKNOWN;
|
||||
|
||||
while (recompui::try_deque_event(cur_event)) {
|
||||
bool context_taking_input = recompui::is_context_taking_input();
|
||||
bool context_capturing_input = recompui::is_context_capturing_input();
|
||||
bool context_capturing_mouse = recompui::is_context_capturing_mouse();
|
||||
|
||||
// Handle up button events even when input is disabled to avoid missing them during binding.
|
||||
if (cur_event.type == SDL_EventType::SDL_CONTROLLERBUTTONUP) {
|
||||
int sdl_key = cont_button_to_key(cur_event.cbutton);
|
||||
if (sdl_key == latest_controller_key_pressed) {
|
||||
latest_controller_key_pressed = SDLK_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recomp::all_input_disabled()) {
|
||||
bool is_mouse_input = false;
|
||||
// Implement some additional behavior for specific events on top of what RmlUi normally does with them.
|
||||
switch (cur_event.type) {
|
||||
case SDL_EventType::SDL_MOUSEMOTION: {
|
||||
@@ -580,12 +618,20 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
|
||||
case SDL_EventType::SDL_MOUSEBUTTONDOWN:
|
||||
mouse_moved = true;
|
||||
mouse_clicked = true;
|
||||
is_mouse_input = true;
|
||||
break;
|
||||
|
||||
case SDL_EventType::SDL_MOUSEBUTTONUP:
|
||||
case SDL_EventType::SDL_MOUSEWHEEL:
|
||||
is_mouse_input = true;
|
||||
break;
|
||||
|
||||
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
|
||||
int rml_key = cont_button_to_key(cur_event.cbutton);
|
||||
if (context_taking_input && rml_key) {
|
||||
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
|
||||
int sdl_key = cont_button_to_key(cur_event.cbutton);
|
||||
if (context_capturing_input && sdl_key) {
|
||||
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
|
||||
latest_controller_key_pressed = sdl_key;
|
||||
next_repeat_time = clock::now() + start_repeat_delay;
|
||||
}
|
||||
non_mouse_interacted = true;
|
||||
cont_interacted = true;
|
||||
@@ -594,6 +640,11 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
|
||||
case SDL_EventType::SDL_KEYDOWN:
|
||||
non_mouse_interacted = true;
|
||||
kb_interacted = true;
|
||||
if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_F8) {
|
||||
if (banjo::get_debug_mode_enabled()) {
|
||||
Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SDL_EventType::SDL_USEREVENT:
|
||||
if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
|
||||
@@ -616,9 +667,11 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
|
||||
if (!*await_stick_return) {
|
||||
*await_stick_return = true;
|
||||
non_mouse_interacted = true;
|
||||
int rml_key = cont_axis_to_key(cur_event.caxis, axis_value);
|
||||
if (context_taking_input && rml_key) {
|
||||
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
|
||||
int sdl_key = cont_axis_to_key(cur_event.caxis, axis_value);
|
||||
if (context_capturing_input && sdl_key) {
|
||||
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
|
||||
latest_controller_key_pressed = sdl_key;
|
||||
next_repeat_time = clock::now() + start_repeat_delay;
|
||||
}
|
||||
}
|
||||
non_mouse_interacted = true;
|
||||
@@ -626,12 +679,25 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
|
||||
}
|
||||
else if (*await_stick_return && fabsf(axis_value) < 0.15f) {
|
||||
*await_stick_return = false;
|
||||
// Stop pressing the current key if the axis that was released was the one triggering key presses.
|
||||
int sdl_key = cont_axis_to_key(cur_event.caxis, axis_value);
|
||||
if (sdl_key == latest_controller_key_pressed) {
|
||||
latest_controller_key_pressed = SDLK_UNKNOWN;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (context_taking_input) {
|
||||
RmlSDL::InputEventHandler(ui_state->context, cur_event);
|
||||
// Send the event to RmlUi if this type of event is being captured.
|
||||
if (is_mouse_input) {
|
||||
if (context_capturing_mouse) {
|
||||
RmlSDL::InputEventHandler(ui_state->context, cur_event);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (context_capturing_input) {
|
||||
RmlSDL::InputEventHandler(ui_state->context, cur_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,6 +728,15 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
|
||||
}
|
||||
} // end dequeue event loop
|
||||
|
||||
// Handle controller key repeats.
|
||||
if (latest_controller_key_pressed != SDLK_UNKNOWN) {
|
||||
clock::time_point now = clock::now();
|
||||
if (now >= next_repeat_time) {
|
||||
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(latest_controller_key_pressed), 0);
|
||||
next_repeat_time += repeat_rate;
|
||||
}
|
||||
}
|
||||
|
||||
if (cont_interacted || kb_interacted || mouse_clicked) {
|
||||
recompui::set_cont_active(cont_interacted);
|
||||
}
|
||||
@@ -721,16 +796,28 @@ void recompui::message_box(const char* msg) {
|
||||
}
|
||||
|
||||
void recompui::show_context(ContextId context, std::string_view param) {
|
||||
std::lock_guard lock{ui_state_mutex};
|
||||
ContextId prev_context = recompui::try_close_current_context();
|
||||
{
|
||||
std::lock_guard lock{ ui_state_mutex };
|
||||
|
||||
// TODO call the context's on_show callback with the param.
|
||||
ui_state->show_context(context);
|
||||
// TODO call the context's on_show callback with the param.
|
||||
ui_state->show_context(context);
|
||||
}
|
||||
if (prev_context != ContextId::null()) {
|
||||
prev_context.open();
|
||||
}
|
||||
}
|
||||
|
||||
void recompui::hide_context(ContextId context) {
|
||||
std::lock_guard lock{ui_state_mutex};
|
||||
ContextId prev_context = recompui::try_close_current_context();
|
||||
{
|
||||
std::lock_guard lock{ ui_state_mutex };
|
||||
|
||||
ui_state->hide_context(context);
|
||||
ui_state->hide_context(context);
|
||||
}
|
||||
if (prev_context != ContextId::null()) {
|
||||
prev_context.open();
|
||||
}
|
||||
}
|
||||
|
||||
void recompui::hide_all_contexts() {
|
||||
@@ -751,14 +838,24 @@ bool recompui::is_context_shown(ContextId context) {
|
||||
return ui_state->is_context_shown(context);
|
||||
}
|
||||
|
||||
bool recompui::is_context_taking_input() {
|
||||
bool recompui::is_context_capturing_input() {
|
||||
std::lock_guard lock{ui_state_mutex};
|
||||
|
||||
if (!ui_state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ui_state->is_context_taking_input();
|
||||
return ui_state->is_context_capturing_input();
|
||||
}
|
||||
|
||||
bool recompui::is_context_capturing_mouse() {
|
||||
std::lock_guard lock{ui_state_mutex};
|
||||
|
||||
if (!ui_state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ui_state->is_context_capturing_mouse();
|
||||
}
|
||||
|
||||
bool recompui::is_any_context_shown() {
|
||||
@@ -783,10 +880,131 @@ Rml::ElementDocument* recompui::create_empty_document() {
|
||||
return ui_state->context->CreateDocument();
|
||||
}
|
||||
|
||||
void recompui::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
|
||||
ui_state->render_interface.queue_image_from_bytes(src, bytes);
|
||||
void recompui::queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes) {
|
||||
ui_state->render_interface.queue_image_from_bytes_file(src, bytes);
|
||||
}
|
||||
|
||||
void recompui::queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height) {
|
||||
ui_state->render_interface.queue_image_from_bytes_rgba32(src, bytes, width, height);
|
||||
}
|
||||
|
||||
void recompui::release_image(const std::string &src) {
|
||||
Rml::ReleaseTexture(src);
|
||||
}
|
||||
|
||||
void recompui::drop_files(const std::list<std::filesystem::path> &file_list) {
|
||||
// Prevent mod installation after the game has started.
|
||||
if (ultramodern::is_game_started()) {
|
||||
return;
|
||||
}
|
||||
|
||||
recompui::open_notification("Installing Mods", "Please Wait");
|
||||
// TODO: Needs a progress callback and a prompt for every mod that needs to be confirmed to be overwritten.
|
||||
// TODO: Run this on a background thread and use the callbacks to advance the state instead of blocking.
|
||||
ModInstaller::Result result;
|
||||
ModInstaller::start_mod_installation(file_list, nullptr, result);
|
||||
|
||||
recompui::close_prompt();
|
||||
|
||||
if (!result.error_messages.empty()) {
|
||||
std::string error_label = std::accumulate(result.error_messages.begin(), result.error_messages.end(), std::string{},
|
||||
[](const std::string &lhs, const std::string &rhs)
|
||||
{
|
||||
return lhs.empty() ? rhs : lhs + '\n' + rhs;
|
||||
});
|
||||
|
||||
recompui::open_info_prompt("Error Installing Mods", error_label, "OK", {}, recompui::ButtonVariant::Tertiary);
|
||||
std::vector<std::string> dummy_error_messages{};
|
||||
ModInstaller::cancel_mod_installation(result, dummy_error_messages);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<ModInstaller::Confirmation> confirmations{};
|
||||
|
||||
for (const ModInstaller::Installation& pending_install : result.pending_installations) {
|
||||
if (pending_install.needs_overwrite_confirmation) {
|
||||
// Get the mod details for the current mod at this file path.
|
||||
std::string old_mod_id = recomp::mods::get_mod_id_from_filename(pending_install.mod_file.filename());
|
||||
std::optional<recomp::mods::ModDetails> old_mod_details = {};
|
||||
|
||||
if (!old_mod_id.empty()) {
|
||||
old_mod_details = recomp::mods::get_details_for_mod(old_mod_id);
|
||||
}
|
||||
|
||||
if (old_mod_details) {
|
||||
confirmations.emplace_back(ModInstaller::Confirmation {
|
||||
.old_display_name = old_mod_details->display_name,
|
||||
.new_display_name = pending_install.display_name,
|
||||
.old_mod_id = old_mod_details->mod_id,
|
||||
.new_mod_id = pending_install.mod_id,
|
||||
.old_version = old_mod_details->version,
|
||||
.new_version = pending_install.mod_version
|
||||
});
|
||||
}
|
||||
else {
|
||||
confirmations.emplace_back(ModInstaller::Confirmation {
|
||||
.old_display_name = "?",
|
||||
.new_display_name = pending_install.display_name,
|
||||
.old_mod_id = "",
|
||||
.new_mod_id = pending_install.mod_id,
|
||||
.old_version = recomp::Version{0, 0, 0, ""},
|
||||
.new_version = pending_install.mod_version
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmations.empty()) {
|
||||
std::vector<std::string> error_messages{};
|
||||
ModInstaller::finish_mod_installation(result, error_messages);
|
||||
ContextId old_context = recompui::try_close_current_context();
|
||||
recompui::update_mod_list();
|
||||
if (old_context != ContextId::null()) {
|
||||
old_context.open();
|
||||
}
|
||||
// TODO show errors
|
||||
}
|
||||
else {
|
||||
std::string prompt_text = std::accumulate(confirmations.begin(), confirmations.end(), std::string{},
|
||||
[](const std::string &cur_text, const ModInstaller::Confirmation &confirmation)
|
||||
{
|
||||
std::string new_text{};
|
||||
if (confirmation.old_display_name == confirmation.new_display_name) {
|
||||
new_text = confirmation.old_display_name + " (" + confirmation.old_version.to_string() + " -> " + confirmation.new_version.to_string() + ")";
|
||||
}
|
||||
else {
|
||||
new_text =
|
||||
confirmation.old_display_name + " (" + confirmation.old_version.to_string() + ") -> " +
|
||||
confirmation.new_display_name + " (" + confirmation.new_version.to_string() + ")";
|
||||
}
|
||||
return cur_text.empty() ? new_text : cur_text + '\n' + new_text;
|
||||
});
|
||||
|
||||
// open prompt where confirm finishes the mod installation with the overwritten files
|
||||
recompui::open_choice_prompt("Overwrite Mods?",
|
||||
prompt_text,
|
||||
"Overwrite",
|
||||
"Cancel",
|
||||
[result]() {
|
||||
std::vector<std::string> error_messages{};
|
||||
recomp::mods::close_mods();
|
||||
ModInstaller::finish_mod_installation(result, error_messages);
|
||||
ContextId old_context = recompui::try_close_current_context();
|
||||
recompui::update_mod_list();
|
||||
if (old_context != ContextId::null()) {
|
||||
old_context.open();
|
||||
}
|
||||
// TODO show errors
|
||||
},
|
||||
[result]() {
|
||||
std::vector<std::string> error_messages{};
|
||||
ModInstaller::cancel_mod_installation(result, error_messages);
|
||||
// TODO show errors
|
||||
},
|
||||
recompui::ButtonVariant::Success,
|
||||
recompui::ButtonVariant::Error,
|
||||
true,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "ultramodern/ultramodern.hpp"
|
||||
|
||||
#include "ui_utils.h"
|
||||
|
||||
recompui::Color recompui::lerp_color(const recompui::Color& a, const recompui::Color& b, float factor) {
|
||||
return recompui::Color{
|
||||
static_cast<uint8_t>(std::lerp(float(a.r), float(b.r), factor)),
|
||||
static_cast<uint8_t>(std::lerp(float(a.g), float(b.g), factor)),
|
||||
static_cast<uint8_t>(std::lerp(float(a.b), float(b.b), factor)),
|
||||
static_cast<uint8_t>(std::lerp(float(a.a), float(b.a), factor)),
|
||||
};
|
||||
}
|
||||
|
||||
recompui::Color recompui::get_pulse_color(uint32_t pulse_milliseconds) {
|
||||
uint64_t millis = std::chrono::duration_cast<std::chrono::milliseconds>(ultramodern::time_since_start()).count();
|
||||
uint32_t anim_offset = millis % pulse_milliseconds;
|
||||
|
||||
float factor = std::abs((2.0f * anim_offset / pulse_milliseconds) - 1.0f);
|
||||
return lerp_color(Color{ 23, 214, 232, 255 }, Color{ 162, 239, 246, 255 }, factor);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef __UI_UTILS_H__
|
||||
#define __UI_UTILS_H__
|
||||
|
||||
#include "elements/ui_types.h"
|
||||
|
||||
namespace recompui {
|
||||
Color lerp_color(const Color& a, const Color& b, float factor);
|
||||
Color get_pulse_color(uint32_t millisecond_period);
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user