mirror of
https://github.com/Zelda64Recomp/Zelda64Recomp
synced 2025-10-05 16:12:49 +02:00
Modding menu and UI and additional mod exports (#535)
* init config opt system w/ 3 types and description support * Move config registry/option to librecomp + added Color conf opt type * Updated color option type styling * Added dropdown option type * Added TextField option type * Button config type + callback wip * init mod menu + bem class + button presets * WIP mod menu, fix some warnings * Rewrite mod details under new UI system. * Refactored mods menu entirely. * Remove ModMenu.scss. * Take ownership of created pointers on Element class. * Add styles. * Multi-style state and disabled state propagation. * Switch to string views. * Convert to spaces, hook up mod enabled to toggle. * Mod menu progress. * Layout for mod details panel, add gap property setters * Update RmlUi for gap property in flexbox * Add slot_map and begin ui context * Implement context and resource storage slotmaps * Config submenu. * Refactored to account for context changes. * Turn off tab searching when submenu is open. * Revert accidental RmlUi downgrade * Upgrade RmlUi to 6.0 release * Text input. * Radio option. * Cleanup. * Refactor Rml document handling to use new ContextId system (prompts currently unimplemented) * Add support for config schema. * Split config sub menu into separate context and fix configure button, prevent infinite loop when looking for autofocus element * Reimplement mechanism to open the config menu to a specific tab * Begin implementing mod UI API * Link storage to mod menu. * Proper enum parsing. * Enable mod reordering. * Draggable improvements to mod menu and runtime update. * Adjust styling of submenu. * Mods folder button. * Linux build fixes. * Hook up new manifest fields to mod UI * Add basic thumbnail parsing functionality. * More style changes. * Implement update event for elements * Use RT64's texture laoding instead. * Restore spacer animations. * Animation API begone. * Auto-enabled mods. * Update runtime submodule and N64Recomp commit in CI for mod config API, remove unnecessary extern C * Sub menu display name, assert on text input. * Clamp delta time to fix UI disappearing on OS with timestamps that don't always increase. * Add a state for when no mods are installed. * Unify API function naming scheme and export relevant API functions * Add actor update/init events and save init event (#536) * Expose remaining property setters to mod UI API * Implemented mod UI callbacks * Implement actor extension data and use it for transform tagging * Zero the memory allocated to hold extended actor data * Implement label and textinput in mod UI API * Patch virtual address translation to support entire extended RAM address space (#533) * Download full target build of llvm in CI Windows runners to fix missing MIPS support and update N64Recomp CI commit * Enable triple buffering in RT64 (#546) * Implement controlling input capturing for mod UI contexts * Created mod UI API functions for setting visibility, setting text, and destroying elements * Fix errant RML tag in mod menu and insert breaks for newlines when setting element text * Fix compilation after rebase * Fixes for macOS * Set the blender description manually for the UI renderer * Created mod UI API functions for imageview elements * Switch to designated initializers to work around missing aggregate initialization compiler support * Update RT64 for driver bug workarounds and misc fixes * Update RT64 to fix native sampler issues with tile copies * Update RT64 for depth clear optimization and more native sampler changes * Update RT64 and allow it to choose the graphics API when set to Auto * Update runtime to allow renderers to choose the graphics API * Update RT64 to enable early Z test shader optimization * Implement data structure mod APIs * Update lunasvg to increase its minimum cmake version * Switch to runtime concatenation of function name in data API error reporting to fix Linux compilation issue * Add missing typename to fix compilation on some compilers * Update RT64 to fix failed assert with MSAA off * Reimplement prompts as a separate UI context and make it so the quit game prompt doesn't bring up the config menu * DnD prototype. * Fix to dynamic lib path and runtime commit. * Finish drag and drop mod installation, disable mod refresh button and code mod toggle when game starts * Remove std::format usage and add missing <list> includes to fix Linux/MacOS compilation * Switch to aggregate initialization for Version to work around missing implicit constructor on some compilers * Replace use of std::bind with lambdas * Add mod install button, put mod description in scroll container, minor mod menu tweaks * Update runtime to fix renderer shutdown race condition * Implement texture pack reordering * Add mod UI API exports for slider, password input, and label radio and expose RmlUi debugger on F8 * Update runtime for mod version export * Update runtime for save swapping mod API * Apply recomp.rcss to mod UI contexts (fixes scrolls) * Updated mod list styling (#561) * Updated mod list styling * mod entry max height * Update RT64 for v5 texture hash * Update runtime for mod API to get save file path * Add special config option id to control texture pack state for code mods * Update runtime for mod default enabled state * Add exports for stars' display lists (#563) * Update runtime to fix default value of enabled_by_default * Update runtime to allow NULL recomp_free * Implement navigation and focus styling for new UI framework (no manual overrides yet) * Fix the previous scissor state bleeding when drawing the RmlUi output onto the swapchain buffer * Use a multiple file select dialog for mod install button * Add mod export for loading UI image from bytes (png/dds) * Manual navigation in UI framework and WIP mod menu navigation * Repeat key events when holding down controller inputs for UI navigation * Patch AnimationContext_SetLoadFrame to allow custom animations (#564) * Close context when showing or hiding a context and reopen afterwards to prevent deadlocks * Add quotes around xdg-open and open commands to support paths with spaces * Update RT64 for high precision texture coordinates when using texture replacements * Add support for built-in mods and convert D-Pad to a built-in mod (#567) * Add embedded mod (using mm_recomp_draw_distance as an example). * Update runtime after merge * Experiment with removing the D-Pad. * Add event needed for dpad as mod, revert remaining changes in built-in patches for dpad * Add built-in dpad mod, add remaining event calls to input.c * Add built-in mods readme --------- Co-authored-by: Dario <dariosamo@gmail.com> * Fixing navigation of mods menu. * Focused state for mod entry. * Prevent hover styling and focus on input elements when disabled * Fix up/down navigation on text input elements * Set mod tab to navigate down to first mod, fix redundant mod scanning * Remove more redundant mod scanning and fix mods being scanned during gameplay * Update runtime for mod folder export * Improve radio navigation and setup mod config submenu navigation setup * Restore fd anywhere export functionality (#570) * fix fd * add comment back in * Make config tabset navigate down to first mod entry when mod menu is open, make mod configure screen focus on configure button after closing * Add navigation exports to mod UI API * Fix opening the config menu via keyboard/controller causing a double animation warning in RmlUi --------- Co-authored-by: Dario <dariosamo@gmail.com> Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com> Co-authored-by: Garrett Cox <garrettjcox@gmail.com> Co-authored-by: David Chavez <david@dcvz.io> Co-authored-by: danielryb <59661841+danielryb@users.noreply.github.com> Co-authored-by: Reonu <danileon95@gmail.com> Co-authored-by: LittleCube <littlecubehax@gmail.com>
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -19,3 +19,6 @@
|
||||
[submodule "Zelda64RecompSyms"]
|
||||
path = Zelda64RecompSyms
|
||||
url = https://github.com/Zelda64Recomp/Zelda64RecompSyms
|
||||
[submodule "lib/slot_map"]
|
||||
path = lib/slot_map
|
||||
url = https://github.com/SergeyMakeev/slot_map
|
||||
|
@@ -10,6 +10,7 @@ set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
# set(CMAKE_CXX_VISIBILITY_PRESET hidden)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-everything /W4")
|
||||
@@ -38,6 +39,8 @@ endif()
|
||||
|
||||
set(RT64_STATIC TRUE)
|
||||
set(RT64_SDL_WINDOW_VULKAN TRUE)
|
||||
add_compile_definitions(HLSL_CPU)
|
||||
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/rt64 ${CMAKE_BINARY_DIR}/rt64)
|
||||
|
||||
# set(BUILD_SHARED_LIBS_SAVED "${BUILD_SHARED_LIBS}")
|
||||
@@ -45,10 +48,10 @@ set(BUILD_SHARED_LIBS OFF)
|
||||
SET(LUNASVG_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/lunasvg)
|
||||
# set(BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS_SAVED}")
|
||||
SET(ENABLE_SVG_PLUGIN ON CACHE BOOL "" FORCE)
|
||||
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(RmlCore PRIVATE LUNASVG_BUILD_STATIC)
|
||||
target_compile_definitions(rmlui_core PRIVATE LUNASVG_BUILD_STATIC)
|
||||
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime)
|
||||
|
||||
@@ -150,13 +153,41 @@ set (SOURCES
|
||||
${CMAKE_SOURCE_DIR}/src/game/debug.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/quicksaving.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/game/recomp_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
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_clickable.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_container.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_element.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_image.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_label.cpp
|
||||
${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
|
||||
|
||||
${CMAKE_SOURCE_DIR}/rsp/aspMain.cpp
|
||||
${CMAKE_SOURCE_DIR}/rsp/njpgdspMain.cpp
|
||||
@@ -183,6 +214,7 @@ target_include_directories(Zelda64Recompiled PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/lib/rt64/src/render
|
||||
${CMAKE_SOURCE_DIR}/lib/freetype-windows-binaries/include
|
||||
${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/nativefiledialog-extended/src/include
|
||||
${CMAKE_SOURCE_DIR}/lib/slot_map/slot_map
|
||||
${CMAKE_BINARY_DIR}/shaders
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
@@ -315,8 +347,8 @@ target_link_libraries(Zelda64Recompiled PRIVATE
|
||||
librecomp
|
||||
ultramodern
|
||||
rt64
|
||||
RmlCore
|
||||
RmlDebugger
|
||||
RmlUi::Core
|
||||
RmlUi::Debugger
|
||||
nfd
|
||||
lunasvg
|
||||
)
|
||||
@@ -356,6 +388,27 @@ endif()
|
||||
build_vertex_shader(Zelda64Recompiled "shaders/InterfaceVS.hlsl" "shaders/InterfaceVS.hlsl")
|
||||
build_pixel_shader(Zelda64Recompiled "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(Zelda64Recompiled PRIVATE ${GENERATED_NRM_SOURCES})
|
||||
|
||||
target_sources(Zelda64Recompiled PRIVATE ${SOURCES})
|
||||
|
||||
set_property(TARGET Zelda64Recompiled PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
|
@@ -1,30 +0,0 @@
|
||||
<template name="prompt">
|
||||
<head>
|
||||
</head>
|
||||
<body class="prompt">
|
||||
<div class="prompt__overlay" />
|
||||
<div class="prompt__content-wrapper" data-if="promptOpen">
|
||||
<div class="prompt__content">
|
||||
<h3>{{ promptHeader }}</h3>
|
||||
<p>{{ promptContent }}</p>
|
||||
<div class="prompt__controls">
|
||||
<button
|
||||
autofocus="true"
|
||||
id="prompt__confirm-button"
|
||||
class="button button--success"
|
||||
style="nav-left: none; nav-right: #prompt__cancel-button"
|
||||
>
|
||||
<div class="button__label" id="prompt__confirm-button-label">{{ promptConfirmLabel }}</div>
|
||||
</button>
|
||||
<button
|
||||
id="prompt__cancel-button"
|
||||
class="button button--error"
|
||||
style="nav-right: none; nav-left: #prompt__confirm-button"
|
||||
>
|
||||
<div class="button__label" id="prompt__cancel-button-label">{{ promptCancelLabel }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</template>
|
@@ -27,12 +27,12 @@
|
||||
<link type="text/template" href="config_menu/controls.rml" />
|
||||
<link type="text/template" href="config_menu/graphics.rml" />
|
||||
<link type="text/template" href="config_menu/sound.rml" />
|
||||
<link type="text/template" href="config_menu/mods.rml" />
|
||||
<link type="text/template" href="config_menu/debug.rml" />
|
||||
<link type="text/template" href="components/prompt.rml" />
|
||||
</head>
|
||||
<body class="window">
|
||||
<!-- <handle move_target="#document"> -->
|
||||
<div id="window" class="rmlui-window rmlui-window--hidden" style="display:flex; flex-flow: column; background-color:rgba(0,0,0,0)" onkeydown="config_keydown">
|
||||
<div id="window" class="rmlui-window" style="display:flex; flex-flow: column; background-color:rgba(0,0,0,0)" onkeydown="config_keydown">
|
||||
<div class="centered-page" onclick="close_config_menu_backdrop">
|
||||
<div class="centered-page__modal">
|
||||
<tabset class="tabs" id="config_tabset">
|
||||
@@ -64,6 +64,13 @@
|
||||
<panel class="config" data-model="sound_options_model">
|
||||
<template src="config-menu__sound" />
|
||||
</panel>
|
||||
<tab class="tab" id="tab_mods">
|
||||
<div>Mods</div>
|
||||
<div class="tab__indicator"></div>
|
||||
</tab>
|
||||
<panel class="config">
|
||||
<template src="config-menu__mods" />
|
||||
</panel>
|
||||
<tab class="tab" data-model="debug_model" data-if="debug_enabled" id="tab_debug">
|
||||
<div>Debug</div>
|
||||
<div class="tab__indicator"></div>
|
||||
@@ -109,19 +116,6 @@
|
||||
<label><span style="font-family:promptfont;">↧</span> Accept</label> -->
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="prompt-root"
|
||||
data-model="prompt_model"
|
||||
data-if="prompt__open"
|
||||
data-alias-promptOpen="prompt__open"
|
||||
data-alias-promptHeader="prompt__header"
|
||||
data-alias-promptContent="prompt__content"
|
||||
data-alias-promptConfirmLabel="prompt__confirmLabel"
|
||||
data-alias-promptCancelLabel="prompt__cancelLabel"
|
||||
data-event-click="prompt__on_click"
|
||||
>
|
||||
<template src="prompt"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </handle> -->
|
||||
<!-- <handle size_target="#document" style="position: absolute; width: 16dp; height: 16dp; bottom: 0px; right: 0px; cursor: resize;"></handle> -->
|
||||
|
@@ -71,7 +71,7 @@
|
||||
data-event-blur="set_input_row_focus(-1)"
|
||||
data-event-focus="set_input_row_focus(i)"
|
||||
data-event-click="clear_input_bindings(i)"
|
||||
class="icon-button icon-button--danger"
|
||||
class="icon-button icon-button--error"
|
||||
data-attr-style="i == 0 ? 'nav-up:#cont_kb_toggle' : 'nav-up:auto'"
|
||||
>
|
||||
<svg src="icons/Trash.svg" />
|
||||
@@ -81,7 +81,7 @@
|
||||
data-event-blur="set_input_row_focus(-1)"
|
||||
data-event-focus="set_input_row_focus(i)"
|
||||
data-event-click="reset_single_input_binding_to_default(i)"
|
||||
class="icon-button icon-button--danger"
|
||||
class="icon-button icon-button--error"
|
||||
data-attr-style="i == 0 ? 'nav-up:#cont_kb_toggle' : 'nav-up:auto'"
|
||||
>
|
||||
<svg src="icons/Reset.svg" />
|
||||
|
9
assets/config_menu/mods.rml
Normal file
9
assets/config_menu/mods.rml
Normal file
@@ -0,0 +1,9 @@
|
||||
<template name="config-menu__mods">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<form class="config__form">
|
||||
<recomp-mod-menu id="menu_mods" />
|
||||
</form>
|
||||
</body>
|
||||
</template>
|
70
assets/config_sub_menu.rml
Normal file
70
assets/config_sub_menu.rml
Normal file
@@ -0,0 +1,70 @@
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="rml.rcss"/>
|
||||
<link type="text/rcss" href="recomp.rcss"/>
|
||||
<title>Inventory</title>
|
||||
<style>
|
||||
body
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Hide the window icon. */
|
||||
div#title_bar div#icon
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
.flex-grid {
|
||||
display: flex;
|
||||
}
|
||||
.col {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="window">
|
||||
<!-- <handle move_target="#document"> -->
|
||||
<div id="window" class="rmlui-window" style="display:flex; flex-flow: column; background-color:rgba(0,0,0,0)" onkeydown="config_keydown">
|
||||
<div class="centered-page" onclick="close_config_menu_backdrop">
|
||||
<div class="centered-page__modal">
|
||||
<div class="config__icon-buttons">
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick="open_quit_game_prompt"
|
||||
id="config__quit-game-button"
|
||||
>
|
||||
<svg src="icons/Quit.svg" />
|
||||
</button>
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick="close_config_menu"
|
||||
id="config__close-menu-button"
|
||||
>
|
||||
<svg src="icons/X.svg" />
|
||||
</button>
|
||||
</div>
|
||||
<recomp-config-sub-menu id="config_sub_menu" />
|
||||
</div>
|
||||
<div
|
||||
class="centered-page__controls"
|
||||
data-model="nav_help_model"
|
||||
>
|
||||
<label>
|
||||
<span>Navigate</span>
|
||||
<span class="prompt-font-sm">{{nav_help__navigate}}</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>Accept</span>
|
||||
<span class="prompt-font-sm">{{nav_help__accept}}</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>Exit</span>
|
||||
<span class="prompt-font-sm">{{nav_help__exit}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
@@ -51,6 +51,10 @@
|
||||
<div class="menu-list-item__bullet">•</div>
|
||||
<div class="menu-list-item__label">Settings</div>
|
||||
</button>
|
||||
<button onclick="open_mods" class="menu-list-item menu-list-item--right">
|
||||
<div class="menu-list-item__bullet">•</div>
|
||||
<div class="menu-list-item__label">Mods</div>
|
||||
</button>
|
||||
<button onclick="exit_game" class="menu-list-item menu-list-item--right">
|
||||
<div class="menu-list-item__bullet">•</div>
|
||||
<div class="menu-list-item__label">Exit</div>
|
||||
|
2512
assets/recomp.rcss
2512
assets/recomp.rcss
File diff suppressed because one or more lines are too long
22
assets/scss/package-lock.json
generated
22
assets/scss/package-lock.json
generated
@@ -307,11 +307,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -638,9 +638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -1192,12 +1192,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
@@ -99,159 +99,3 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.config-option {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
margin: space(16) space(0) space(24);
|
||||
|
||||
&:last-child {
|
||||
margin-top: space(16);
|
||||
}
|
||||
}
|
||||
|
||||
.config-option__title {
|
||||
@extend %label-md;
|
||||
padding: 0 space(12);
|
||||
}
|
||||
|
||||
.config-option__list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
|
||||
input:first-of-type {
|
||||
nav-left: none;
|
||||
}
|
||||
|
||||
input:last-of-type {
|
||||
nav-right: none;
|
||||
}
|
||||
|
||||
.config-option__tab-label {
|
||||
@extend %label-sm;
|
||||
@include trans-colors-opa;
|
||||
display: block;
|
||||
position: relative;
|
||||
height: auto;
|
||||
margin: space(4) space(12) 0;
|
||||
padding: space(8) 0;
|
||||
color: $color-text-inactive;
|
||||
tab-index: none;
|
||||
|
||||
&:hover {
|
||||
color: $color-text;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input.radio {
|
||||
@extend %nav-all;
|
||||
@include trans-colors-opa;
|
||||
visibility: visible;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:not(:disabled) {
|
||||
&:checked + .config-option__tab-label {
|
||||
border-bottom: 1dp;
|
||||
border-color: $color-text;
|
||||
color: $color-text;
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.rmlui-window:not([mouse-active]) &:focus + .config-option__tab-label {
|
||||
transition: none;
|
||||
animation: $focus-anim-border;
|
||||
border-color: $color-secondary;
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
&:focus + .config-option__tab-label,
|
||||
&:hover + .config-option__tab-label {
|
||||
color: $color-text;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled + .config-option__tab-label {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.range slidertrack {
|
||||
@include trans-colors;
|
||||
height: 2dp;
|
||||
margin-top: space(8);
|
||||
background-color: $color-border;
|
||||
}
|
||||
|
||||
input.range sliderbar {
|
||||
@include trans-colors;
|
||||
width: space(16);
|
||||
height: space(16);
|
||||
margin-top: space(1);
|
||||
margin-right: space(-8);
|
||||
margin-left: space(-8);
|
||||
transition: background-color $transition-quick;
|
||||
border-radius: 8dp;
|
||||
background-color: $color-text-dim;
|
||||
|
||||
.rmlui-window:not([mouse-active]) &:focus {
|
||||
@include border($color-a);
|
||||
animation: $focus-anim-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-text;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input.range sliderbar:active,
|
||||
input.range slidertrack:active + sliderbar {
|
||||
background-color: $color-secondary;
|
||||
}
|
||||
|
||||
input.range sliderarrowdec,
|
||||
input.range sliderarrowinc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.config-option__details {
|
||||
@extend %label-xs;
|
||||
height: space(18);
|
||||
margin: space(14) space(12) 0;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.config-option__range-wrapper {
|
||||
max-width: space(360);
|
||||
margin-top: space(4);
|
||||
}
|
||||
|
||||
.config-option__range-label {
|
||||
@extend %label-sm;
|
||||
|
||||
display: block;
|
||||
// flex: 0 0 space(32);
|
||||
width: space(56);
|
||||
margin: 0 12dp;
|
||||
margin-right: space(16);
|
||||
padding: 0;
|
||||
color: $color-text;
|
||||
tab-index: none;
|
||||
}
|
||||
|
27
assets/scss/styles/components/ConfigDescription.scss
Normal file
27
assets/scss/styles/components/ConfigDescription.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.config-description {
|
||||
flex: 1 1 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: space(16);
|
||||
border-radius: 0dp;
|
||||
border-bottom-right-radius: $border-radius-modal;
|
||||
border-bottom-left-radius: $border-radius-modal;
|
||||
background-color: $color-bg-shadow;
|
||||
text-align: left;
|
||||
|
||||
&__contents {
|
||||
@extend %body;
|
||||
padding: space(16);
|
||||
line-height: space(28);
|
||||
white-space: pre-line;
|
||||
|
||||
b {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
i {
|
||||
color: $color-warning;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
}
|
29
assets/scss/styles/components/ConfigGroup.scss
Normal file
29
assets/scss/styles/components/ConfigGroup.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
.config-group {
|
||||
position: relative;
|
||||
|
||||
&--scrollable {
|
||||
flex: 1 1 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 0 0 0 space(16);
|
||||
|
||||
.config-group__wrapper {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
@extend %label-md;
|
||||
color: $color-primary;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
padding: space(16) 0;
|
||||
}
|
||||
}
|
413
assets/scss/styles/components/ConfigOption.scss
Normal file
413
assets/scss/styles/components/ConfigOption.scss
Normal file
@@ -0,0 +1,413 @@
|
||||
.config-option {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
margin: space(16) space(0) space(24);
|
||||
|
||||
&--hz {
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
margin-top: space(4);
|
||||
margin-bottom: space(4);
|
||||
|
||||
.config-option__title {
|
||||
@extend %label-md;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.config-option__list {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-option__title {
|
||||
@extend %label-md;
|
||||
padding: 0 space(12);
|
||||
}
|
||||
|
||||
.config-option__radio-tabs,
|
||||
.config-option__list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
|
||||
input:first-of-type {
|
||||
nav-left: none;
|
||||
}
|
||||
|
||||
input:last-of-type {
|
||||
nav-right: none;
|
||||
}
|
||||
|
||||
.config-option__tab-label {
|
||||
@extend %label-sm;
|
||||
@include trans-colors-opa;
|
||||
display: block;
|
||||
position: relative;
|
||||
height: auto;
|
||||
margin: space(4) space(12) 0;
|
||||
padding: space(8) 0;
|
||||
color: $color-text-inactive;
|
||||
tab-index: none;
|
||||
|
||||
&:hover {
|
||||
color: $color-text;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.config-option__checkbox-wrapper {
|
||||
@include trans-colors-opa;
|
||||
width: space(32);
|
||||
height: space(32);
|
||||
margin: space(4) space(12) 0;
|
||||
border-radius: $border-radius-sm;
|
||||
opacity: 0.5;
|
||||
background-color: $color-bg-overlay;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[checked] {
|
||||
background-color: $color-a;
|
||||
}
|
||||
}
|
||||
|
||||
.config-option__checkbox {
|
||||
@extend %nav-all;
|
||||
@include trans-colors-opa;
|
||||
visibility: visible;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
// TODO: Remove & Replace old stylings
|
||||
input.radio {
|
||||
@extend %nav-all;
|
||||
@include trans-colors-opa;
|
||||
visibility: visible;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:not(:disabled) {
|
||||
&:checked + .config-option__tab-label {
|
||||
border-bottom: 1dp;
|
||||
border-color: $color-text;
|
||||
color: $color-text;
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.rmlui-window:not([mouse-active]) &:focus + .config-option__tab-label {
|
||||
transition: none;
|
||||
animation: $focus-anim-border;
|
||||
border-color: $color-secondary;
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
&:focus + .config-option__tab-label,
|
||||
&:hover + .config-option__tab-label {
|
||||
color: $color-text;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled + .config-option__tab-label {
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.range slidertrack {
|
||||
@include trans-colors;
|
||||
height: 2dp;
|
||||
margin-top: space(8);
|
||||
background-color: $color-border;
|
||||
}
|
||||
|
||||
input.range sliderbar {
|
||||
@include trans-colors;
|
||||
width: space(16);
|
||||
height: space(16);
|
||||
margin-top: space(1);
|
||||
margin-right: space(-8);
|
||||
margin-left: space(-8);
|
||||
transition: background-color $transition-quick;
|
||||
border-radius: 8dp;
|
||||
background-color: $color-text-dim;
|
||||
|
||||
.rmlui-window:not([mouse-active]) &:focus {
|
||||
@include border($color-a);
|
||||
animation: $focus-anim-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-text;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input.range sliderbar:active,
|
||||
input.range slidertrack:active + sliderbar {
|
||||
background-color: $color-secondary;
|
||||
}
|
||||
|
||||
input.range sliderarrowdec,
|
||||
input.range sliderarrowinc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.config-option__details {
|
||||
@extend %label-xs;
|
||||
height: space(18);
|
||||
margin: space(14) space(12) 0;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.config-option-color {
|
||||
width: 100%;
|
||||
max-width: space(360);
|
||||
height: auto;
|
||||
margin-top: space(4);
|
||||
margin-left: space(12);
|
||||
padding: 0;
|
||||
|
||||
&__preview-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: space(8 * 9);
|
||||
}
|
||||
|
||||
&__preview-block {
|
||||
display: block;
|
||||
width: space(8 * 11);
|
||||
height: 100%;
|
||||
border-width: $border-width-thickness;
|
||||
border-radius: $border-radius-lg;
|
||||
border-color: $color-border;
|
||||
}
|
||||
|
||||
&__hsv-wrapper {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding-left: space(8);
|
||||
|
||||
.config-option-range {
|
||||
flex: 1 1 auto;
|
||||
|
||||
label {
|
||||
min-width: space(72);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-option-range {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
max-width: space(360);
|
||||
height: auto;
|
||||
margin-top: space(4);
|
||||
padding: 0;
|
||||
|
||||
&__label {
|
||||
@extend %label-sm;
|
||||
|
||||
display: block;
|
||||
width: space(56);
|
||||
margin: 0 12dp;
|
||||
margin-right: space(16);
|
||||
padding: 0;
|
||||
color: $color-text;
|
||||
tab-index: none;
|
||||
}
|
||||
|
||||
&__range-input {
|
||||
flex: 1;
|
||||
|
||||
slidertrack {
|
||||
@include trans-colors;
|
||||
height: 2dp;
|
||||
margin-top: space(8);
|
||||
background-color: $color-border;
|
||||
}
|
||||
|
||||
sliderbar {
|
||||
@include trans-colors;
|
||||
width: space(16);
|
||||
height: space(16);
|
||||
margin-top: space(1);
|
||||
margin-right: space(-8);
|
||||
margin-left: space(-8);
|
||||
transition: background-color $transition-quick;
|
||||
border-radius: 8dp;
|
||||
background-color: $color-text-dim;
|
||||
|
||||
.rmlui-window:not([mouse-active]) &:focus {
|
||||
@include border($color-a);
|
||||
animation: $focus-anim-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-text;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
sliderbar:active,
|
||||
slidertrack:active + sliderbar {
|
||||
background-color: $color-secondary;
|
||||
}
|
||||
|
||||
sliderarrowdec,
|
||||
sliderarrowinc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.config-option__range-wrapper {
|
||||
max-width: space(360);
|
||||
margin-top: space(4);
|
||||
}
|
||||
|
||||
.config-option__range-label {
|
||||
@extend %label-sm;
|
||||
|
||||
display: block;
|
||||
// flex: 0 0 space(32);
|
||||
width: space(56);
|
||||
margin: 0 12dp;
|
||||
margin-right: space(16);
|
||||
padding: 0;
|
||||
color: $color-text;
|
||||
tab-index: none;
|
||||
}
|
||||
|
||||
.config-option-dropdown, .config-option-textfield {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: space(8) space(24) space(8) space(12);
|
||||
|
||||
&__select {
|
||||
display: block;
|
||||
height: space(48);
|
||||
padding: space(14);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
// Cursed guess & check so that this appears to be the same height as the select
|
||||
$extra-space: 2;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: space(0 + $extra-space) 0 space(10 + $extra-space);
|
||||
cursor: text;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&__select, &__wrapper {
|
||||
@extend %body;
|
||||
@extend %nav-all;
|
||||
@include trans-colors-border;
|
||||
@include border($color-white-a50);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 100%;
|
||||
|
||||
width: auto;
|
||||
border-radius: $border-radius-md;
|
||||
background-color: $color-white-a5;
|
||||
|
||||
&:hover, &:focus {
|
||||
@include border($color-white-a80);
|
||||
background-color: $color-white-a20;
|
||||
}
|
||||
|
||||
selectvalue {
|
||||
display: inline;
|
||||
height: auto;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
selectbox {
|
||||
@include border($color-border);
|
||||
margin-top: space(2);
|
||||
padding: space(4) 0;
|
||||
border-radius: $border-radius-md;
|
||||
background-color: $color-background-3;
|
||||
|
||||
option {
|
||||
@extend %nav-all;
|
||||
@include trans-colors;
|
||||
padding: space(8) space(12);
|
||||
background-color: $color-transparent;
|
||||
color: $color-text-dim;
|
||||
font-weight: 400;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: $color-white-a20;
|
||||
}
|
||||
|
||||
&:hover:not(:checked) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: $color-white-a5;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
|
||||
/*
|
||||
<button
|
||||
class="icon-button icon-button--danger"
|
||||
class="icon-button icon-button--error"
|
||||
>
|
||||
<svg src="icons/Trash.svg" />
|
||||
</button>
|
||||
@@ -82,7 +82,7 @@ $icon-button-size: 56 - ($border-width-thickness-num * 2);
|
||||
@include create-icon-button-variation($color-success);
|
||||
}
|
||||
|
||||
&--danger {
|
||||
&--error {
|
||||
@include create-icon-button-variation($color-error);
|
||||
}
|
||||
|
||||
|
@@ -20,7 +20,6 @@
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: space(20) space(24);
|
||||
transition: color $transition-quick;
|
||||
opacity: 0.9;
|
||||
background-color: rgba(0,0,0,0);
|
||||
color: $color-text-inactive;
|
||||
|
@@ -2,6 +2,9 @@
|
||||
@import "./ControlOption";
|
||||
@import "./Tabs";
|
||||
@import "./Config";
|
||||
@import "./ConfigGroup";
|
||||
@import "./ConfigOption";
|
||||
@import "./ConfigDescription";
|
||||
@import "./InputConfig";
|
||||
@import "./Button";
|
||||
@import "./IconButton";
|
||||
|
@@ -189,8 +189,7 @@ select {
|
||||
// background: rgb(150,150,150)
|
||||
// }
|
||||
|
||||
input.radio,
|
||||
input.checkbox {
|
||||
input.radio {
|
||||
flex: 0;
|
||||
width:0dp;
|
||||
nav-up:auto;
|
||||
@@ -200,3 +199,14 @@ input.checkbox {
|
||||
tab-index:auto;
|
||||
focus:auto;
|
||||
}
|
||||
|
||||
input.checkbox {
|
||||
width: space(20);
|
||||
height: space(20);
|
||||
nav-up:auto;
|
||||
nav-right:auto;
|
||||
nav-down:auto;
|
||||
nav-left:auto;
|
||||
tab-index:auto;
|
||||
focus:auto;
|
||||
}
|
||||
|
38
config_example.cheats.en_us.json
Normal file
38
config_example.cheats.en_us.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"consumables": "Consumables",
|
||||
"consumables/infinite_magic": "Infinite Magic",
|
||||
"consumables/infinite_magic:description": "You get sooo much magic lol!!",
|
||||
"consumables/infinite_rupees": "Infinite Rupees",
|
||||
"consumables/infinite_arrows": "Infinite Arrows",
|
||||
"consumables/infinite_bombs": "Infinite Bombs",
|
||||
"consumables/infinite_health": "Infinite Health",
|
||||
|
||||
"consumable_actions": "Consumable Actions",
|
||||
"consumable_actions/refill_all": "primary",
|
||||
"consumable_actions/refill_all:description": "Refills anything that can be refilled, like magic, rupees, arrows, bombs, health, etc.",
|
||||
|
||||
"gameplay": "Gameplay",
|
||||
|
||||
"gameplay/movement": "Movement",
|
||||
"gameplay/movement/L_for_fast": "Hold L to move fast",
|
||||
"gameplay/movement/L_for_fast/values/off": "Off",
|
||||
"gameplay/movement/L_for_fast/values/x2": "X2",
|
||||
"gameplay/movement/L_for_fast/values/x4": "X4",
|
||||
"gameplay/movement/L_for_fast/values/x6": "X6",
|
||||
"gameplay/movement/L_for_fast2": "Hold L to move fast the sequel",
|
||||
"gameplay/movement/L_for_fast2/values/off": "Off",
|
||||
"gameplay/movement/L_for_fast2/values/x2": "X2",
|
||||
"gameplay/movement/L_for_fast2/values/x4": "X4",
|
||||
"gameplay/movement/L_for_fast2/values/x6": "X6",
|
||||
"gameplay/movement/L_to_levitate": "Hold L to levitate",
|
||||
"gameplay/movement/always_quickspin": "Always quickspin",
|
||||
"gameplay/movement/always_quickspin:description": "Always <b>quickspin</b> whenever using your <i>sword</i> and in a <i>state</i> where <i>you</i> can <b>quickspin.</b><br /><br />yeah...",
|
||||
"gameplay/movement/heart_color": "Hearts color",
|
||||
"gameplay/movement/link_size": "Link's Size",
|
||||
"gameplay/movement/link_name": "Link's Name",
|
||||
"gameplay/movement/link_name:description": "Change Link's name to something silly!",
|
||||
"gameplay/abilities": "Abilities",
|
||||
"gameplay/abilities/fd_anywhere": "Fierce Deity Anywhere",
|
||||
"gameplay/abilities/permanent_razor_sword": "Permanent Razor Sword",
|
||||
"gameplay/abilities/permanent_razor_sword2": "MORE Permanent Razor Sword"
|
||||
}
|
135
config_example.cheats.json
Normal file
135
config_example.cheats.json
Normal file
@@ -0,0 +1,135 @@
|
||||
[
|
||||
{
|
||||
"type": "CheckboxGroup",
|
||||
"key": "consumables",
|
||||
"toggle": true,
|
||||
"toggleDefault": false,
|
||||
"options": [
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "infinite_magic",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "infinite_rupees",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "infinite_arrows",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "infinite_bombs",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "infinite_health",
|
||||
"default": false,
|
||||
"callback": "on_update_health"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Group",
|
||||
"key": "consumable_actions",
|
||||
"options": [
|
||||
{
|
||||
"type": "Button",
|
||||
"key": "refill_all",
|
||||
"variant": "primary",
|
||||
"callback": "on_refill_all"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Group",
|
||||
"key": "gameplay",
|
||||
"toggle": true,
|
||||
"toggleDefault": true,
|
||||
"options": [
|
||||
{
|
||||
"type": "Group",
|
||||
"key": "movement",
|
||||
"options": [
|
||||
{
|
||||
"type": "Dropdown",
|
||||
"key": "L_for_fast",
|
||||
"default": "x2",
|
||||
"values": [
|
||||
"off",
|
||||
"x2",
|
||||
"x4",
|
||||
"x6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "RadioTabs",
|
||||
"key": "L_for_fast2",
|
||||
"default": "x2",
|
||||
"values": [
|
||||
"off",
|
||||
"x2",
|
||||
"x4",
|
||||
"x6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "L_to_levitate",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "always_quickspin",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "Color",
|
||||
"key": "heart_color",
|
||||
"default": [255, 50, 50]
|
||||
},
|
||||
{
|
||||
"type": "Range",
|
||||
"key": "link_size",
|
||||
"default": 100,
|
||||
"suffix": "%",
|
||||
"min": 20,
|
||||
"max": 400,
|
||||
"step": 20
|
||||
},
|
||||
{
|
||||
"type": "TextField",
|
||||
"key": "link_name",
|
||||
"default": "George",
|
||||
"maxlength": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Group",
|
||||
"key": "abilities",
|
||||
"options": [
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "fd_anywhere",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "permanent_razor_sword",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"type": "Checkbox",
|
||||
"key": "permanent_razor_sword2",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
10
include/overloaded.h
Normal file
10
include/overloaded.h
Normal file
@@ -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
|
11
include/recomp_data.h
Normal file
11
include/recomp_data.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef __RECOMP_DATA_H__
|
||||
#define __RECOMP_DATA_H__
|
||||
|
||||
namespace recomputil {
|
||||
void init_extended_actor_data();
|
||||
void reset_actor_data();
|
||||
|
||||
void register_data_api_exports();
|
||||
}
|
||||
|
||||
#endif
|
@@ -3,10 +3,19 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <list>
|
||||
|
||||
// TODO move this file into src/ui
|
||||
|
||||
#include "SDL.h"
|
||||
#include "RmlUi/Core.h"
|
||||
|
||||
#include "../src/ui/util/hsv.h"
|
||||
#include "../src/ui/util/bem.h"
|
||||
|
||||
#include "../src/ui/core/ui_context.h"
|
||||
|
||||
namespace Rml {
|
||||
class ElementDocument;
|
||||
class EventListenerInstancer;
|
||||
@@ -17,10 +26,11 @@ namespace Rml {
|
||||
namespace recompui {
|
||||
class UiEventListenerInstancer;
|
||||
|
||||
// TODO remove this once the UI has been ported over to the new system.
|
||||
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;
|
||||
};
|
||||
@@ -36,24 +46,34 @@ namespace recompui {
|
||||
std::unique_ptr<UiEventListenerInstancer> make_event_listener_instancer();
|
||||
void register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler);
|
||||
|
||||
enum class Menu {
|
||||
Launcher,
|
||||
Config,
|
||||
None
|
||||
};
|
||||
void show_context(ContextId context, std::string_view param);
|
||||
void hide_context(ContextId context);
|
||||
void hide_all_contexts();
|
||||
bool is_context_shown(ContextId context);
|
||||
bool is_context_capturing_input();
|
||||
bool is_context_capturing_mouse();
|
||||
bool is_any_context_shown();
|
||||
ContextId try_close_current_context();
|
||||
|
||||
void set_current_menu(Menu menu);
|
||||
Menu get_current_menu();
|
||||
ContextId get_launcher_context_id();
|
||||
ContextId get_config_context_id();
|
||||
ContextId get_config_sub_menu_context_id();
|
||||
|
||||
enum class ConfigSubmenu {
|
||||
enum class ConfigTab {
|
||||
General,
|
||||
Controls,
|
||||
Graphics,
|
||||
Audio,
|
||||
Sound,
|
||||
Mods,
|
||||
Debug,
|
||||
Count
|
||||
};
|
||||
|
||||
void set_config_tab(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,
|
||||
Secondary,
|
||||
@@ -64,56 +84,43 @@ namespace recompui {
|
||||
NumVariants,
|
||||
};
|
||||
|
||||
void set_config_submenu(ConfigSubmenu submenu);
|
||||
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 destroy_ui();
|
||||
void apply_color_hack();
|
||||
void get_window_size(int& width, int& height);
|
||||
void set_cursor_visible(bool visible);
|
||||
void update_supported_options();
|
||||
void toggle_fullscreen();
|
||||
void update_rml_display_refresh_rate();
|
||||
|
||||
extern const std::unordered_map<ButtonVariant, std::string> button_variants;
|
||||
|
||||
struct PromptContext {
|
||||
Rml::DataModelHandle model_handle;
|
||||
std::string header = "";
|
||||
std::string content = "";
|
||||
std::string confirmLabel = "Confirm";
|
||||
std::string cancelLabel = "Cancel";
|
||||
ButtonVariant confirmVariant = ButtonVariant::Success;
|
||||
ButtonVariant cancelVariant = ButtonVariant::Error;
|
||||
std::function<void()> onConfirm;
|
||||
std::function<void()> onCancel;
|
||||
|
||||
std::string returnElementId = "";
|
||||
|
||||
bool open = false;
|
||||
bool shouldFocus = false;
|
||||
bool focusOnCancel = true;
|
||||
|
||||
PromptContext() = default;
|
||||
|
||||
void close_prompt();
|
||||
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 on_confirm(void);
|
||||
void on_cancel(void);
|
||||
void on_click(Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs);
|
||||
};
|
||||
|
||||
PromptContext *get_prompt_context(void);
|
||||
|
||||
bool get_cont_active(void);
|
||||
void set_cont_active(bool active);
|
||||
@@ -122,6 +129,17 @@ namespace recompui {
|
||||
void message_box(const char* msg);
|
||||
|
||||
void set_render_hooks();
|
||||
|
||||
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();
|
||||
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,7 +1,7 @@
|
||||
#ifndef __ZELDA_RENDER_H__
|
||||
#define __ZELDA_RENDER_H__
|
||||
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <filesystem>
|
||||
|
||||
#include "common/rt64_user_configuration.h"
|
||||
@@ -14,6 +14,8 @@ namespace RT64 {
|
||||
|
||||
namespace zelda64 {
|
||||
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 zelda64 {
|
||||
uint32_t get_display_framerate() const override;
|
||||
float get_resolution_scale() const override;
|
||||
|
||||
protected:
|
||||
private:
|
||||
std::unique_ptr<RT64::Application> app;
|
||||
std::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 zelda64 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,11 +3,14 @@
|
||||
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <list>
|
||||
|
||||
namespace zelda64 {
|
||||
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.
|
||||
|
@@ -1,2 +1,9 @@
|
||||
set(FREETYPE_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/lib/freetype-windows-binaries/include)
|
||||
set(FREETYPE_LIBRARIES "${CMAKE_SOURCE_DIR}/lib/freetype-windows-binaries/release static/vs2015-2022/win64/freetype.lib")
|
||||
add_library(Freetype::Freetype STATIC IMPORTED)
|
||||
set_target_properties(Freetype::Freetype PROPERTIES
|
||||
IMPORTED_LOCATION ${FREETYPE_LIBRARIES}
|
||||
)
|
||||
target_include_directories(Freetype::Freetype INTERFACE
|
||||
${FREETYPE_INCLUDE_DIRS}
|
||||
)
|
||||
|
Submodule lib/N64ModernRuntime updated: ec56fb39b0...0aa75b98ba
Submodule lib/RmlUi updated: a893ea6386...7a06f27db0
2
lib/rt64
2
lib/rt64
Submodule lib/rt64 updated: ca146dbd31...793abe0bc3
1
lib/slot_map
Submodule
1
lib/slot_map
Submodule
Submodule lib/slot_map added at b8ac8ebd89
6
mods/BUILTIN_MODS.md
Normal file
6
mods/BUILTIN_MODS.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Built-in Mods
|
||||
|
||||
This folder contains mods that are built into the Zelda 64: Recompiled project. Built-in mods behave like normal mods but are present in a clean installation of the project. They can be updated or downgraded by placing the .nrm file for the mod in the appdata mods folder in the same way a normal mod would be, which overrides the built-in version with the manually installed version.
|
||||
|
||||
The list of built-in mods is as follows:
|
||||
* Majora's Mask: Recompiled D-Pad Mod - https://github.com/Zelda64Recomp/MMRecompDpadMod
|
BIN
mods/mm_recomp_dpad_builtin.nrm
Normal file
BIN
mods/mm_recomp_dpad_builtin.nrm
Normal file
Binary file not shown.
91
patches/actor_data.c
Normal file
91
patches/actor_data.c
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "patches.h"
|
||||
#include "extended_actors.h"
|
||||
#include "transform_ids.h"
|
||||
#include "actor_funcs.h"
|
||||
|
||||
// Use 32 bits of compiler-inserted padding to hold the actor's slot.
|
||||
// 0x22 between halfDaysBits and world
|
||||
#define actorIdByte0(actor) ((u8*)(actor))[0x22]
|
||||
// 0x23 between halfDaysBits and world
|
||||
#define actorIdByte1(actor) ((u8*)(actor))[0x23]
|
||||
// 0x3A between audioFlags and focus
|
||||
#define actorIdByte2(actor) ((u8*)(actor))[0x3A]
|
||||
// 0x3B between audioFlags and focus
|
||||
#define actorIdByte3(actor) ((u8*)(actor))[0x3B]
|
||||
|
||||
u32 actor_get_slot(Actor* actor) {
|
||||
return (actorIdByte0(actor) << 24) | (actorIdByte1(actor) << 16) | (actorIdByte2(actor) << 8) | (actorIdByte3(actor) << 0);
|
||||
}
|
||||
|
||||
void actor_set_slot(Actor* actor, ActorExtensionId slot) {
|
||||
u32 b0 = (slot >> 24) & 0xFF;
|
||||
u32 b1 = (slot >> 16) & 0xFF;
|
||||
u32 b2 = (slot >> 8) & 0xFF;
|
||||
u32 b3 = (slot >> 0) & 0xFF;
|
||||
|
||||
actorIdByte0(actor) = b0;
|
||||
actorIdByte1(actor) = b1;
|
||||
actorIdByte2(actor) = b2;
|
||||
actorIdByte3(actor) = b3;
|
||||
}
|
||||
|
||||
RECOMP_EXPORT ActorExtensionId z64recomp_extend_actor(s16 actor_id, u32 size) {
|
||||
return recomp_register_actor_extension(actor_id, size);
|
||||
}
|
||||
|
||||
RECOMP_EXPORT ActorExtensionId z64recomp_extend_actor_all(u32 size) {
|
||||
return recomp_register_actor_extension_generic(size);
|
||||
}
|
||||
|
||||
RECOMP_EXPORT void* z64recomp_get_extended_actor_data(Actor* actor, ActorExtensionId extension) {
|
||||
return recomp_get_actor_data(actor_get_slot(actor), extension, actor->id);
|
||||
}
|
||||
|
||||
RECOMP_EXPORT u32 z64recomp_get_actor_spawn_index(Actor* actor) {
|
||||
return recomp_get_actor_spawn_index(actor_get_slot(actor));
|
||||
}
|
||||
|
||||
RECOMP_EXPORT u32 actor_transform_id(Actor* actor) {
|
||||
u32 spawn_index = z64recomp_get_actor_spawn_index(actor);
|
||||
|
||||
return (spawn_index * ACTOR_TRANSFORM_ID_COUNT) + ACTOR_TRANSFORM_ID_START;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED = 1 << 0,
|
||||
ACTOR_CUSTOM_FLAG_1 = 1 << 1,
|
||||
} CustomActorFlags;
|
||||
|
||||
typedef struct {
|
||||
CustomActorFlags flags;
|
||||
} BaseActorExtensionData;
|
||||
|
||||
ActorExtensionId base_actor_extension_handle;
|
||||
|
||||
void register_base_actor_extensions() {
|
||||
base_actor_extension_handle = z64recomp_extend_actor_all(sizeof(BaseActorExtensionData));
|
||||
}
|
||||
|
||||
BaseActorExtensionData* get_base_extension_data(Actor* actor) {
|
||||
return (BaseActorExtensionData*)z64recomp_get_extended_actor_data(actor, base_actor_extension_handle);
|
||||
}
|
||||
|
||||
RECOMP_EXPORT u32 actor_get_interpolation_skipped(Actor* actor) {
|
||||
return (get_base_extension_data(actor)->flags & ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED) != 0;
|
||||
}
|
||||
|
||||
RECOMP_EXPORT void actor_set_interpolation_skipped(Actor* actor) {
|
||||
get_base_extension_data(actor)->flags |= ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED;
|
||||
}
|
||||
|
||||
RECOMP_EXPORT void actor_clear_interpolation_skipped(Actor* actor) {
|
||||
get_base_extension_data(actor)->flags &= ~ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED;
|
||||
}
|
||||
|
||||
void actor_set_custom_flag_1(Actor* actor) {
|
||||
get_base_extension_data(actor)->flags |= ACTOR_CUSTOM_FLAG_1;
|
||||
}
|
||||
|
||||
bool actor_get_custom_flag_1(Actor* actor) {
|
||||
return (get_base_extension_data(actor)->flags & ACTOR_CUSTOM_FLAG_1) != 0;
|
||||
}
|
14
patches/actor_funcs.h
Normal file
14
patches/actor_funcs.h
Normal file
@@ -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,12 +1,16 @@
|
||||
#include "patches.h"
|
||||
#include "fault.h"
|
||||
#include "transform_ids.h"
|
||||
|
||||
u16 next_actor_transform = 0;
|
||||
#include "extended_actors.h"
|
||||
#include "z64actor.h"
|
||||
#include "actor_funcs.h"
|
||||
|
||||
extern FaultClient sActorFaultClient;
|
||||
void Actor_Destroy(Actor* actor, PlayState* play);
|
||||
Actor* Actor_Delete(ActorContext* actorCtx, Actor* actor, PlayState* play);
|
||||
void ZeldaArena_Free(void* ptr);
|
||||
Actor* Actor_RemoveFromCategory(PlayState* play, ActorContext* actorCtx, Actor* actorToRemove);
|
||||
void Actor_FreeOverlay(ActorOverlay* entry);
|
||||
|
||||
RECOMP_PATCH void Actor_CleanupContext(ActorContext* actorCtx, PlayState* play) {
|
||||
s32 i;
|
||||
@@ -33,21 +37,22 @@ RECOMP_PATCH void Actor_CleanupContext(ActorContext* actorCtx, PlayState* play)
|
||||
actorCtx->absoluteSpace = NULL;
|
||||
}
|
||||
|
||||
// @recomp Reset the actor transform IDs as all actors have been deleted.
|
||||
next_actor_transform = 0;
|
||||
// @recomp Reset the actor extension data.
|
||||
recomp_clear_all_actor_data();
|
||||
|
||||
Play_SaveCycleSceneFlags(&play->state);
|
||||
ActorOverlayTable_Cleanup();
|
||||
}
|
||||
|
||||
u32 create_actor_transform_id() {
|
||||
u32 ret = next_actor_transform;
|
||||
next_actor_transform++;
|
||||
|
||||
return ret;
|
||||
}
|
||||
RECOMP_DECLARE_EVENT(recomp_should_actor_init(PlayState* play, Actor* actor, bool* should));
|
||||
RECOMP_DECLARE_EVENT(recomp_after_actor_init(PlayState* play, Actor* actor));
|
||||
RECOMP_DECLARE_EVENT(recomp_should_actor_update(PlayState* play, Actor* actor, bool* should));
|
||||
RECOMP_DECLARE_EVENT(recomp_after_actor_update(PlayState* play, Actor* actor));
|
||||
|
||||
RECOMP_PATCH void Actor_Init(Actor* actor, PlayState* play) {
|
||||
// @recomp Allocate the actor's extension data.
|
||||
actor_set_slot(actor, recomp_create_actor_data(actor->id));
|
||||
|
||||
Actor_SetWorldToHome(actor);
|
||||
Actor_SetShapeRotToWorld(actor);
|
||||
Actor_SetFocus(actor, 0.0f);
|
||||
@@ -69,14 +74,160 @@ RECOMP_PATCH void Actor_Init(Actor* actor, PlayState* play) {
|
||||
ActorShape_Init(&actor->shape, 0.0f, NULL, 0.0f);
|
||||
if (Object_IsLoaded(&play->objectCtx, actor->objectSlot)) {
|
||||
Actor_SetObjectDependency(play, actor);
|
||||
|
||||
// @recomp Augmented, allowing us to prevent actor init and hook into after init
|
||||
bool shouldInit = true;
|
||||
recomp_should_actor_init(play, actor, &shouldInit);
|
||||
if (shouldInit) {
|
||||
actor->init(actor, play);
|
||||
actor->init = NULL;
|
||||
recomp_after_actor_init(play, actor);
|
||||
} else {
|
||||
actor->init = NULL;
|
||||
Actor_Kill(actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RECOMP_PATCH Actor* Actor_Delete(ActorContext* actorCtx, Actor* actor, PlayState* play) {
|
||||
s32 pad;
|
||||
Player* player = GET_PLAYER(play);
|
||||
Actor* newHead;
|
||||
ActorOverlay* overlayEntry = actor->overlayEntry;
|
||||
|
||||
if ((player != NULL) && (actor == player->lockOnActor)) {
|
||||
Player_Untarget(player);
|
||||
Camera_ChangeMode(Play_GetCamera(play, Play_GetActiveCamId(play)), CAM_MODE_NORMAL);
|
||||
}
|
||||
|
||||
// @recomp Pick a transform ID for this actor and encode it into struct padding
|
||||
u32 cur_transform_id = create_actor_transform_id();
|
||||
actorIdByte0(actor) = (cur_transform_id >> 0) & 0xFF;
|
||||
actorIdByte1(actor) = (cur_transform_id >> 8) & 0xFF;;
|
||||
if (actor == actorCtx->targetCtx.fairyActor) {
|
||||
actorCtx->targetCtx.fairyActor = NULL;
|
||||
}
|
||||
|
||||
if (actor == actorCtx->targetCtx.forcedTargetActor) {
|
||||
actorCtx->targetCtx.forcedTargetActor = NULL;
|
||||
}
|
||||
|
||||
if (actor == actorCtx->targetCtx.bgmEnemy) {
|
||||
actorCtx->targetCtx.bgmEnemy = NULL;
|
||||
}
|
||||
|
||||
AudioSfx_StopByPos(&actor->projectedPos);
|
||||
Actor_Destroy(actor, play);
|
||||
|
||||
newHead = Actor_RemoveFromCategory(play, actorCtx, actor);
|
||||
|
||||
// @recomp Destroy the actor's extension data.
|
||||
recomp_destroy_actor_data(actor_get_slot(actor));
|
||||
|
||||
ZeldaArena_Free(actor);
|
||||
|
||||
if (overlayEntry->vramStart != NULL) {
|
||||
overlayEntry->numLoaded--;
|
||||
Actor_FreeOverlay(overlayEntry);
|
||||
}
|
||||
|
||||
return newHead;
|
||||
}
|
||||
|
||||
// @recomp Copied from z_actor.c
|
||||
typedef struct {
|
||||
/* 0x00 */ PlayState* play;
|
||||
/* 0x04 */ Actor* actor;
|
||||
/* 0x08 */ u32 freezeExceptionFlag;
|
||||
/* 0x0C */ u32 canFreezeCategory;
|
||||
/* 0x10 */ Actor* talkActor;
|
||||
/* 0x14 */ Player* player;
|
||||
/* 0x18 */ u32 updateActorFlagsMask; // Actor will update only if at least 1 actor flag is set in this bitmask
|
||||
} UpdateActor_Params; // size = 0x1C
|
||||
|
||||
RECOMP_PATCH Actor* Actor_UpdateActor(UpdateActor_Params* params) {
|
||||
PlayState* play = params->play;
|
||||
Actor* actor = params->actor;
|
||||
Actor* nextActor;
|
||||
|
||||
if (actor->world.pos.y < -25000.0f) {
|
||||
actor->world.pos.y = -25000.0f;
|
||||
}
|
||||
|
||||
actor->sfxId = 0;
|
||||
actor->audioFlags &= ~(((1 << 4) | (1 << 3) | (1 << 2) | (1 << 1) | (1 << 0)) | ((1 << 6) | (1 << 5))); // ACTOR_AUDIO_FLAG_ALL
|
||||
|
||||
if (actor->init != NULL) {
|
||||
if (Object_IsLoaded(&play->objectCtx, actor->objectSlot)) {
|
||||
Actor_SetObjectDependency(play, actor);
|
||||
|
||||
// @recomp Augmented, allowing us to prevent actor init and hook into after init
|
||||
bool shouldInit = true;
|
||||
recomp_should_actor_init(play, actor, &shouldInit);
|
||||
if (shouldInit) {
|
||||
actor->init(actor, play);
|
||||
actor->init = NULL;
|
||||
recomp_after_actor_init(play, actor);
|
||||
} else {
|
||||
actor->init = NULL;
|
||||
Actor_Kill(actor);
|
||||
}
|
||||
}
|
||||
nextActor = actor->next;
|
||||
} else if (actor->update == NULL) {
|
||||
if (!actor->isDrawn) {
|
||||
nextActor = Actor_Delete(&play->actorCtx, actor, play);
|
||||
} else {
|
||||
Actor_Destroy(actor, play);
|
||||
nextActor = actor->next;
|
||||
}
|
||||
} else {
|
||||
if (!Object_IsLoaded(&play->objectCtx, actor->objectSlot)) {
|
||||
Actor_Kill(actor);
|
||||
} else if (((params->freezeExceptionFlag != 0) && !(actor->flags & params->freezeExceptionFlag)) ||
|
||||
(((!params->freezeExceptionFlag) != 0) &&
|
||||
(!(actor->flags & (1 << 20)) ||
|
||||
((actor->category == ACTORCAT_EXPLOSIVES) && (params->player->stateFlags1 & PLAYER_STATE1_200))) &&
|
||||
params->canFreezeCategory && (actor != params->talkActor) && (actor != params->player->heldActor) &&
|
||||
(actor->parent != ¶ms->player->actor))) {
|
||||
CollisionCheck_ResetDamage(&actor->colChkInfo);
|
||||
} else {
|
||||
Math_Vec3f_Copy(&actor->prevPos, &actor->world.pos);
|
||||
actor->xzDistToPlayer = Actor_WorldDistXZToActor(actor, ¶ms->player->actor);
|
||||
actor->playerHeightRel = Actor_HeightDiff(actor, ¶ms->player->actor);
|
||||
actor->xyzDistToPlayerSq = SQ(actor->xzDistToPlayer) + SQ(actor->playerHeightRel);
|
||||
|
||||
actor->yawTowardsPlayer = Actor_WorldYawTowardActor(actor, ¶ms->player->actor);
|
||||
actor->flags &= ~(1 << 24);
|
||||
|
||||
if ((DECR(actor->freezeTimer) == 0) && (actor->flags & params->updateActorFlagsMask)) {
|
||||
if (actor == params->player->lockOnActor) {
|
||||
actor->isLockedOn = true;
|
||||
} else {
|
||||
actor->isLockedOn = false;
|
||||
}
|
||||
|
||||
if ((actor->targetPriority != 0) && (params->player->lockOnActor == NULL)) {
|
||||
actor->targetPriority = 0;
|
||||
}
|
||||
|
||||
Actor_SetObjectDependency(play, actor);
|
||||
|
||||
if (actor->colorFilterTimer != 0) {
|
||||
actor->colorFilterTimer--;
|
||||
}
|
||||
|
||||
// @recomp Augmented, allowing us to prevent actor update and hook into after update
|
||||
bool shouldUpdate = true;
|
||||
recomp_should_actor_update(play, actor, &shouldUpdate);
|
||||
if (shouldUpdate) {
|
||||
actor->update(actor, play);
|
||||
recomp_after_actor_update(play, actor);
|
||||
}
|
||||
DynaPoly_UnsetAllInteractFlags(play, &play->colCtx.dyna, actor);
|
||||
}
|
||||
|
||||
CollisionCheck_ResetDamage(&actor->colChkInfo);
|
||||
}
|
||||
nextActor = actor->next;
|
||||
}
|
||||
return nextActor;
|
||||
}
|
||||
|
||||
// Extract the transform ID for this actor, add the limb index and write that as the matrix group to POLY_OPA_DISP.
|
||||
@@ -1285,3 +1436,10 @@ RECOMP_PATCH void Actor_Draw(PlayState* play, Actor* actor) {
|
||||
|
||||
CLOSE_DISPS(play->state.gfxCtx);
|
||||
}
|
||||
|
||||
ActorExtensionId z64recomp_extend_actor(s16 actor_id, u32 size);
|
||||
ActorExtensionId z64recomp_extend_actor_all(u32 size);
|
||||
|
||||
void* z64recomp_get_extended_actor_data(Actor* actor, ActorExtensionId extension);
|
||||
u32 z64recomp_get_actor_spawn_index(Actor* actor);
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 955 B |
14
patches/extended_actors.h
Normal file
14
patches/extended_actors.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef __EXTENDED_ACTORS_H__
|
||||
#define __EXTENDED_ACTORS_H__
|
||||
|
||||
#include "global.h"
|
||||
|
||||
typedef u32 ActorExtensionId;
|
||||
|
||||
ActorExtensionId z64recomp_extend_actor(s16 actor_id, u32 size);
|
||||
ActorExtensionId z64recomp_extend_actor_all(u32 size);
|
||||
|
||||
void* z64recomp_get_extended_actor_data(Actor* actor, ActorExtensionId extension);
|
||||
u32 z64recomp_get_actor_spawn_index(Actor* actor);
|
||||
|
||||
#endif
|
14
patches/gamestate_patches.c
Normal file
14
patches/gamestate_patches.c
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "patches.h"
|
||||
#include "ui_funcs.h"
|
||||
|
||||
// @recomp Patched to run UI callbacks.
|
||||
RECOMP_PATCH void Graph_UpdateGame(GameState* gameState) {
|
||||
recomp_run_ui_callbacks();
|
||||
|
||||
GameState_GetInput(gameState);
|
||||
GameState_IncrementFrameCount(gameState);
|
||||
if (SREG(20) < 3) {
|
||||
Audio_Update();
|
||||
}
|
||||
}
|
||||
|
1637
patches/input.c
1637
patches/input.c
File diff suppressed because it is too large
Load Diff
42
patches/memory_patches.c
Normal file
42
patches/memory_patches.c
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "patches.h"
|
||||
|
||||
// @recomp Leave the entire KSEG0 range unmodified when translating to a virtual address. This will allow
|
||||
// using the entirety of the extended RAM address space for custom assets.
|
||||
RECOMP_PATCH void* Lib_SegmentedToVirtual(void* ptr) {
|
||||
if (IS_KSEG0(ptr)) {
|
||||
return ptr;
|
||||
}
|
||||
else {
|
||||
return SEGMENTED_TO_K0(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
AnimationEntry* AnimationContext_AddEntry(AnimationContext* animationCtx, AnimationType type);
|
||||
#define LINK_ANIMETION_OFFSET(addr, offset) \
|
||||
(SEGMENT_ROM_START(link_animetion) + ((uintptr_t)addr & 0xFFFFFF) + ((u32)offset))
|
||||
|
||||
// @recomp Skip the DMA if the animation is already in RAM. Allows mods to play custom animations.
|
||||
RECOMP_PATCH void AnimationContext_SetLoadFrame(PlayState* play, PlayerAnimationHeader* animation, s32 frame, s32 limbCount,
|
||||
Vec3s* frameTable) {
|
||||
AnimationEntry* task = AnimationContext_AddEntry(&play->animationCtx, ANIMATION_LINKANIMETION);
|
||||
|
||||
if (task != NULL) {
|
||||
PlayerAnimationHeader* playerAnimHeader = Lib_SegmentedToVirtual(animation);
|
||||
s32 pad;
|
||||
|
||||
osCreateMesgQueue(&task->data.load.msgQueue, task->data.load.msg,
|
||||
ARRAY_COUNT(task->data.load.msg));
|
||||
|
||||
if (IS_KSEG0(playerAnimHeader->linkAnimSegment)) {
|
||||
osSendMesg(&task->data.load.msgQueue, NULL, OS_MESG_NOBLOCK);
|
||||
Lib_MemCpy(frameTable, ((u8*)playerAnimHeader->segmentVoid) + (sizeof(Vec3s) * limbCount + sizeof(s16)) * frame,
|
||||
sizeof(Vec3s) * limbCount + sizeof(s16));
|
||||
}
|
||||
else {
|
||||
DmaMgr_SendRequestImpl(
|
||||
&task->data.load.req, frameTable,
|
||||
LINK_ANIMETION_OFFSET(playerAnimHeader->linkAnimSegment, (sizeof(Vec3s) * limbCount + sizeof(s16)) * frame),
|
||||
sizeof(Vec3s) * limbCount + sizeof(s16), 0, &task->data.load.msgQueue, NULL);
|
||||
}
|
||||
}
|
||||
}
|
@@ -92,9 +92,6 @@ typedef enum {
|
||||
"\t.popsection\n"); \
|
||||
extern u8 identifier[]
|
||||
|
||||
void draw_dpad(PlayState* play);
|
||||
void draw_dpad_icons(PlayState* play);
|
||||
|
||||
void View_ApplyInterpolate(View* view, s32 mask, bool reset_interpolation_state);
|
||||
|
||||
void set_camera_skipped(bool skipped);
|
||||
|
@@ -179,7 +179,7 @@ RECOMP_PATCH void Play_Init(GameState* thisx) {
|
||||
if (CHECK_EVENTINF(EVENTINF_TRIGGER_DAYTELOP)) {
|
||||
CLEAR_EVENTINF(EVENTINF_TRIGGER_DAYTELOP);
|
||||
STOP_GAMESTATE(&this->state);
|
||||
// Use non-relocatable reference to DayTelop_Init instead.
|
||||
// @recomp Use non-relocatable reference to DayTelop_Init instead.
|
||||
SET_NEXT_GAMESTATE(&this->state, DayTelop_Init_NORELOCATE, sizeof(DayTelopState));
|
||||
return;
|
||||
}
|
||||
@@ -195,7 +195,7 @@ RECOMP_PATCH void Play_Init(GameState* thisx) {
|
||||
if (gSaveContext.save.entrance == -1) {
|
||||
gSaveContext.save.entrance = 0;
|
||||
STOP_GAMESTATE(&this->state);
|
||||
// Use non-relocatable reference to TitleSetup_Init instead.
|
||||
// @recomp Use non-relocatable reference to TitleSetup_Init instead.
|
||||
SET_NEXT_GAMESTATE(&this->state, TitleSetup_Init_NORELOCATE, sizeof(TitleSetupState));
|
||||
return;
|
||||
}
|
||||
|
52
patches/recompui_event_structs.h
Normal file
52
patches/recompui_event_structs.h
Normal file
@@ -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
|
@@ -1,5 +1,6 @@
|
||||
#include "patches.h"
|
||||
#include "misc_funcs.h"
|
||||
#include "transform_ids.h"
|
||||
#include "loadfragment.h"
|
||||
|
||||
void Main_ClearMemory(void* begin, void* end);
|
||||
@@ -16,6 +17,9 @@ RECOMP_PATCH void Main_Init(void) {
|
||||
OSMesg msg[1];
|
||||
size_t prevSize;
|
||||
|
||||
// @recomp Register base actor extensions.
|
||||
register_base_actor_extensions();
|
||||
|
||||
// @recomp_event recomp_on_init(): Allow mods to initialize themselves once.
|
||||
recomp_on_init();
|
||||
|
||||
|
@@ -2,10 +2,14 @@
|
||||
#include "sys_flashrom.h"
|
||||
#include "PR/os_internal_flash.h"
|
||||
#include "fault.h"
|
||||
#include "overlays/gamestates/ovl_file_choose/z_file_select.h"
|
||||
#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h"
|
||||
|
||||
extern OSMesgQueue sFlashromMesgQueue;
|
||||
s32 SysFlashrom_IsInit(void);
|
||||
void Sleep_Msec(u32 ms);
|
||||
extern u16 D_801F6AF0;
|
||||
extern u8 D_801F6AF2;
|
||||
|
||||
// @recomp Patched to not wait a hardcoded amount of time for the save to complete.
|
||||
RECOMP_PATCH void Sram_UpdateWriteToFlashDefault(SramContext* sramCtx) {
|
||||
@@ -56,3 +60,72 @@ RECOMP_PATCH void Sram_UpdateWriteToFlashOwlSave(SramContext* sramCtx) {
|
||||
Lib_MemCpy(&gSaveContext, sramCtx->saveBuf, offsetof(SaveContext, fileNum));
|
||||
}
|
||||
}
|
||||
|
||||
RECOMP_DECLARE_EVENT(recomp_after_init_save(FileSelectState* fileSelect, SramContext* sramCtx));
|
||||
|
||||
// @recomp Patched to expose recomp_on_save_init event
|
||||
RECOMP_PATCH void Sram_InitSave(FileSelectState* fileSelect2, SramContext* sramCtx) {
|
||||
s32 phi_v0;
|
||||
u16 i;
|
||||
FileSelectState* fileSelect = fileSelect2;
|
||||
s16 maskCount;
|
||||
|
||||
if (gSaveContext.flashSaveAvailable) {
|
||||
Sram_InitNewSave();
|
||||
if (fileSelect->buttonIndex == 0) {
|
||||
gSaveContext.save.cutsceneIndex = 0xFFF0;
|
||||
}
|
||||
|
||||
for (phi_v0 = 0; phi_v0 < ARRAY_COUNT(gSaveContext.save.saveInfo.playerData.playerName); phi_v0++) {
|
||||
gSaveContext.save.saveInfo.playerData.playerName[phi_v0] =
|
||||
fileSelect->fileNames[fileSelect->buttonIndex][phi_v0];
|
||||
}
|
||||
|
||||
gSaveContext.save.saveInfo.playerData.newf[0] = 'Z';
|
||||
gSaveContext.save.saveInfo.playerData.newf[1] = 'E';
|
||||
gSaveContext.save.saveInfo.playerData.newf[2] = 'L';
|
||||
gSaveContext.save.saveInfo.playerData.newf[3] = 'D';
|
||||
gSaveContext.save.saveInfo.playerData.newf[4] = 'A';
|
||||
gSaveContext.save.saveInfo.playerData.newf[5] = '3';
|
||||
|
||||
recomp_after_init_save(fileSelect, sramCtx);
|
||||
|
||||
gSaveContext.save.saveInfo.checksum = Sram_CalcChecksum(&gSaveContext.save, sizeof(Save));
|
||||
|
||||
Lib_MemCpy(sramCtx->saveBuf, &gSaveContext.save, sizeof(Save));
|
||||
Lib_MemCpy(&sramCtx->saveBuf[0x2000], &gSaveContext.save, sizeof(Save));
|
||||
|
||||
for (i = 0; i < ARRAY_COUNT(gSaveContext.save.saveInfo.playerData.newf); i++) {
|
||||
fileSelect->newf[fileSelect->buttonIndex][i] = gSaveContext.save.saveInfo.playerData.newf[i];
|
||||
}
|
||||
|
||||
fileSelect->threeDayResetCount[fileSelect->buttonIndex] =
|
||||
gSaveContext.save.saveInfo.playerData.threeDayResetCount;
|
||||
|
||||
for (i = 0; i < ARRAY_COUNT(gSaveContext.save.saveInfo.playerData.playerName); i++) {
|
||||
fileSelect->fileNames[fileSelect->buttonIndex][i] = gSaveContext.save.saveInfo.playerData.playerName[i];
|
||||
}
|
||||
|
||||
fileSelect->healthCapacity[fileSelect->buttonIndex] = gSaveContext.save.saveInfo.playerData.healthCapacity;
|
||||
fileSelect->health[fileSelect->buttonIndex] = gSaveContext.save.saveInfo.playerData.health;
|
||||
fileSelect->defenseHearts[fileSelect->buttonIndex] = gSaveContext.save.saveInfo.inventory.defenseHearts;
|
||||
fileSelect->questItems[fileSelect->buttonIndex] = gSaveContext.save.saveInfo.inventory.questItems;
|
||||
fileSelect->time[fileSelect->buttonIndex] = CURRENT_TIME;
|
||||
fileSelect->day[fileSelect->buttonIndex] = gSaveContext.save.day;
|
||||
fileSelect->isOwlSave[fileSelect->buttonIndex] = gSaveContext.save.isOwlSave;
|
||||
fileSelect->rupees[fileSelect->buttonIndex] = gSaveContext.save.saveInfo.playerData.rupees;
|
||||
fileSelect->walletUpgrades[fileSelect->buttonIndex] = CUR_UPG_VALUE(UPG_WALLET);
|
||||
|
||||
for (i = 0, maskCount = 0; i < MASK_NUM_SLOTS; i++) {
|
||||
if (gSaveContext.save.saveInfo.inventory.items[i + ITEM_NUM_SLOTS] != ITEM_NONE) {
|
||||
maskCount++;
|
||||
}
|
||||
}
|
||||
|
||||
fileSelect->maskCount[fileSelect->buttonIndex] = maskCount;
|
||||
fileSelect->heartPieceCount[fileSelect->buttonIndex] = GET_QUEST_HEART_PIECE_COUNT;
|
||||
}
|
||||
|
||||
gSaveContext.save.time = D_801F6AF0;
|
||||
gSaveContext.flashSaveAvailable = D_801F6AF2;
|
||||
}
|
||||
|
@@ -45,3 +45,11 @@ recomp_high_precision_fb_enabled = 0x8F0000A8;
|
||||
recomp_get_resolution_scale = 0x8F0000AC;
|
||||
recomp_get_analog_inverted_axes = 0x8F0000B0;
|
||||
recomp_get_window_resolution = 0x8F0000B4;
|
||||
recomp_run_ui_callbacks = 0x8F0000B8;
|
||||
recomp_register_actor_extension = 0x8F0000BC;
|
||||
recomp_register_actor_extension_generic = 0x8F0000C0;
|
||||
recomp_clear_all_actor_data = 0x8F0000C4;
|
||||
recomp_create_actor_data = 0x8F0000C8;
|
||||
recomp_destroy_actor_data = 0x8F0000CC;
|
||||
recomp_get_actor_data = 0x8F0000D0;
|
||||
recomp_get_actor_spawn_index = 0x8F0000D4;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
#ifndef __TRANSFORM_IDS_H__
|
||||
#define __TRANSFORM_IDS_H__
|
||||
|
||||
#include "extended_actors.h"
|
||||
|
||||
#define CAMERA_TRANSFORM_ID 0x10U
|
||||
#define CIRCLE_OVERLAY_TRANSFORM_ID 0x11U
|
||||
#define CIRCLE_OVERLAY_TRANSFORM_PROJECTION_ID 0x12U
|
||||
@@ -40,55 +42,18 @@
|
||||
#define ACTOR_TRANSFORM_ID_COUNT (ACTOR_TRANSFORM_LIMB_COUNT * 2) // One ID for each limb and another for each post-draw
|
||||
#define ACTOR_TRANSFORM_ID_START 0x1000000U
|
||||
|
||||
// Use 16 bits of compiler-inserted padding to hold the actor's transform ID.
|
||||
// 0x22 between halfDaysBits and world
|
||||
#define actorIdByte0(actor) ((u8*)(actor))[0x22]
|
||||
// 0x23 between halfDaysBits and world
|
||||
#define actorIdByte1(actor) ((u8*)(actor))[0x23]
|
||||
// 0x3A between audioFlags and focus
|
||||
#define actorIdByte2(actor) ((u8*)(actor))[0x3A]
|
||||
|
||||
// Other unused padding:
|
||||
// 0x3B between audioFlags and focus
|
||||
|
||||
static inline u32 actor_transform_id(Actor* actor) {
|
||||
u32 actor_id =
|
||||
(actorIdByte0(actor) << 0) |
|
||||
(actorIdByte1(actor) << 8);
|
||||
|
||||
return (actor_id * ACTOR_TRANSFORM_ID_COUNT) + ACTOR_TRANSFORM_ID_START;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED = 1 << 0,
|
||||
ACTOR_CUSTOM_FLAG_1 = 1 << 1,
|
||||
} CustomActorFlags;
|
||||
|
||||
static inline u32 actor_get_interpolation_skipped(Actor* actor) {
|
||||
return (actorIdByte2(actor) & ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED) != 0;
|
||||
}
|
||||
|
||||
static inline void actor_set_interpolation_skipped(Actor* actor) {
|
||||
actorIdByte2(actor) |= ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED;
|
||||
}
|
||||
|
||||
static inline void actor_clear_interpolation_skipped(Actor* actor) {
|
||||
actorIdByte2(actor) &= ~ACTOR_TRANSFORM_FLAG_INTERPOLATION_SKIPPED;
|
||||
}
|
||||
|
||||
static inline void actor_set_custom_flag_1(Actor* actor) {
|
||||
actorIdByte2(actor) |= ACTOR_CUSTOM_FLAG_1;
|
||||
}
|
||||
|
||||
static inline void actor_clear_custom_flag_1(Actor* actor) {
|
||||
actorIdByte2(actor) &= ~ACTOR_CUSTOM_FLAG_1;
|
||||
}
|
||||
|
||||
static inline bool actor_get_custom_flag_1(Actor* actor) {
|
||||
return (actorIdByte2(actor) & ACTOR_CUSTOM_FLAG_1) != 0;
|
||||
}
|
||||
|
||||
u32 actor_transform_id(Actor* actor);
|
||||
u32 actor_get_interpolation_skipped(Actor* actor);
|
||||
void actor_set_interpolation_skipped(Actor* actor);
|
||||
void actor_clear_interpolation_skipped(Actor* actor);
|
||||
void actor_set_custom_flag_1(Actor* actor);
|
||||
bool actor_get_custom_flag_1(Actor* actor);
|
||||
void force_camera_interpolation();
|
||||
void force_camera_skip_interpolation();
|
||||
|
||||
ActorExtensionId actor_get_slot(Actor* actor);
|
||||
void actor_set_slot(Actor* actor, ActorExtensionId);
|
||||
|
||||
void register_base_actor_extensions();
|
||||
|
||||
#endif
|
||||
|
9
patches/ui_funcs.h
Normal file
9
patches/ui_funcs.h
Normal file
@@ -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
|
@@ -487,10 +487,8 @@ RECOMP_PATCH void Interface_Draw(PlayState* play) {
|
||||
|
||||
Magic_DrawMeter(play);
|
||||
|
||||
// @recomp Draw the D-Pad and its item icons as well as the autosave icon if the game is unpaused.
|
||||
// @recomp Draw the autosave icon if the game is unpaused.
|
||||
if (pauseCtx->state != PAUSE_STATE_MAIN) {
|
||||
draw_dpad(play);
|
||||
draw_dpad_icons(play);
|
||||
draw_autosave_icon(play);
|
||||
}
|
||||
|
||||
|
@@ -42,6 +42,10 @@ static struct {
|
||||
bool rumble_active;
|
||||
} InputState;
|
||||
|
||||
static struct {
|
||||
std::list<std::filesystem::path> files_dropped;
|
||||
} DropState;
|
||||
|
||||
std::atomic<recomp::InputDevice> scanning_device = recomp::InputDevice::COUNT;
|
||||
std::atomic<recomp::InputField> scanned_input;
|
||||
|
||||
@@ -103,7 +107,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
SDL_KeyboardEvent* keyevent = &event->key;
|
||||
|
||||
// Skip repeated events when not in the menu
|
||||
if (recompui::get_current_menu() == recompui::Menu::None &&
|
||||
if (!recompui::is_context_capturing_input() &&
|
||||
event->key.repeat) {
|
||||
break;
|
||||
}
|
||||
@@ -156,10 +160,6 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (recompui::get_current_menu() != recompui::Menu::Config) {
|
||||
recompui::set_current_menu(recompui::Menu::Config);
|
||||
}
|
||||
|
||||
zelda64::open_quit_game_prompt();
|
||||
recompui::activate_mouse();
|
||||
break;
|
||||
@@ -275,6 +275,18 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
InputState.pending_mouse_delta[0] += motion_event->xrel;
|
||||
InputState.pending_mouse_delta[1] += motion_event->yrel;
|
||||
}
|
||||
queue_if_enabled(event);
|
||||
break;
|
||||
case SDL_EventType::SDL_DROPBEGIN:
|
||||
DropState.files_dropped.clear();
|
||||
break;
|
||||
case SDL_EventType::SDL_DROPFILE:
|
||||
DropState.files_dropped.emplace_back(std::filesystem::path(std::u8string_view((const char8_t *)(event->drop.file))));
|
||||
SDL_free(event->drop.file);
|
||||
break;
|
||||
case SDL_EventType::SDL_DROPCOMPLETE:
|
||||
recompui::drop_files(DropState.files_dropped);
|
||||
break;
|
||||
default:
|
||||
queue_if_enabled(event);
|
||||
break;
|
||||
@@ -284,6 +296,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
|
||||
|
||||
void recomp::handle_events() {
|
||||
SDL_Event cur_event;
|
||||
static bool started = false;
|
||||
static bool exited = false;
|
||||
while (SDL_PollEvent(&cur_event) && !exited) {
|
||||
exited = sdl_event_filter(nullptr, &cur_event);
|
||||
@@ -300,6 +313,11 @@ void recomp::handle_events() {
|
||||
SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE);
|
||||
SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE);
|
||||
}
|
||||
|
||||
if (!started && ultramodern::is_game_started()) {
|
||||
started = true;
|
||||
recompui::process_game_started();
|
||||
}
|
||||
}
|
||||
|
||||
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;
|
||||
@@ -711,8 +729,8 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
|
||||
}
|
||||
|
||||
bool recomp::game_input_disabled() {
|
||||
// Disable input if any menu is open.
|
||||
return recompui::get_current_menu() != recompui::Menu::None;
|
||||
// Disable input if any menu that blocks input is open.
|
||||
return recompui::is_context_capturing_input();
|
||||
}
|
||||
|
||||
bool recomp::all_input_disabled() {
|
||||
|
237
src/game/recomp_actor_api.cpp
Normal file
237
src/game/recomp_actor_api.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
#include "slot_map.h"
|
||||
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "librecomp/addresses.hpp"
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
#include "recomp_ui.h"
|
||||
#include "recomp_data.h"
|
||||
#include "../patches/actor_funcs.h"
|
||||
|
||||
struct ExtensionInfo {
|
||||
// Either the actor's type ID, or 0xFFFFFFFF if this is for generic data.
|
||||
uint32_t actor_type;
|
||||
// The offset from either the start of the actor's data or the start of the actor's specific extension data depending on the value of actor_type.
|
||||
uint32_t data_offset;
|
||||
};
|
||||
|
||||
struct ExtensionData {
|
||||
uint32_t actor_spawn_index;
|
||||
PTR(void) data_addr;
|
||||
};
|
||||
|
||||
std::mutex actor_data_mutex{};
|
||||
// The total size of actor-specific extension data for each actor type.
|
||||
std::vector<uint32_t> actor_data_sizes{};
|
||||
// The total size of all generic actor extension data.
|
||||
uint32_t generic_data_size;
|
||||
// The registered actor extensions.
|
||||
std::vector<ExtensionInfo> actor_extensions{};
|
||||
// The extension data for every actor.
|
||||
using actor_data_map_t = dod::slot_map32<ExtensionData>;
|
||||
actor_data_map_t actor_data{};
|
||||
// The number of actors spawned since the last reset.
|
||||
uint32_t actor_spawn_count = 0;
|
||||
// Whether or not extensions can be registered at this time.
|
||||
bool can_register = false;
|
||||
|
||||
// Debug counters.
|
||||
size_t alloc_count = 0;
|
||||
size_t free_count = 0;
|
||||
|
||||
void recomputil::init_extended_actor_data() {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
|
||||
actor_data_sizes.clear();
|
||||
generic_data_size = 0;
|
||||
actor_extensions.clear();
|
||||
actor_data.reset();
|
||||
actor_spawn_count = 0;
|
||||
can_register = true;
|
||||
// Create a dummy extension so the first extension handle is nonzero, should help catch bugs.
|
||||
actor_extensions.push_back({});
|
||||
}
|
||||
|
||||
void recomputil::reset_actor_data() {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
actor_data.reset();
|
||||
actor_spawn_count = 0;
|
||||
|
||||
assert(alloc_count == free_count);
|
||||
alloc_count = 0;
|
||||
free_count = 0;
|
||||
}
|
||||
|
||||
constexpr uint32_t round_up_16(uint32_t value) {
|
||||
return (value + 15) & (~15);
|
||||
}
|
||||
|
||||
extern "C" void recomp_register_actor_extension(uint8_t* rdram, recomp_context* ctx) {
|
||||
u32 actor_type = _arg<0, u32>(rdram, ctx);
|
||||
u32 size = _arg<1, u32>(rdram, ctx);
|
||||
|
||||
if (!can_register) {
|
||||
recompui::message_box("Fatal error in mod - attempted to register actor extension data after actors have been spawned.");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
if (actor_data_sizes.size() <= actor_type) {
|
||||
actor_data_sizes.resize(2 * actor_type);
|
||||
}
|
||||
|
||||
// Increase the actor type's extension data size by the provided size (rounded up to a multiple of 16).
|
||||
uint32_t data_offset = actor_data_sizes[actor_type];
|
||||
actor_data_sizes[actor_type] += round_up_16(size);
|
||||
|
||||
// Register the extension.
|
||||
uint32_t ret = static_cast<uint32_t>(actor_extensions.size());
|
||||
actor_extensions.emplace_back(ExtensionInfo{.actor_type = actor_type, .data_offset = data_offset});
|
||||
|
||||
// printf("Registered actor extension data for type %u (size 0x%08X, offset 0x%08X)\n", actor_type, size, data_offset);
|
||||
|
||||
_return<u32>(ctx, ret);
|
||||
}
|
||||
|
||||
extern "C" void recomp_register_actor_extension_generic(uint8_t* rdram, recomp_context* ctx) {
|
||||
u32 size = _arg<0, u32>(rdram, ctx);
|
||||
|
||||
// Increase the generic extension data size by the provided size (rounded up to a multiple of 16).
|
||||
uint32_t data_offset = generic_data_size;
|
||||
generic_data_size += round_up_16(size);
|
||||
|
||||
// Register the extension.
|
||||
uint32_t ret = static_cast<uint32_t>(actor_extensions.size());
|
||||
actor_extensions.emplace_back(ExtensionInfo{.actor_type = 0xFFFFFFFFU, .data_offset = data_offset});
|
||||
|
||||
// printf("Registered generic actor extension data (size 0x%08X, offset 0x%08X)\n", size, data_offset);
|
||||
_return<u32>(ctx, ret);
|
||||
}
|
||||
|
||||
extern "C" void recomp_clear_all_actor_data(uint8_t* rdram, recomp_context* ctx) {
|
||||
(void)rdram;
|
||||
(void)ctx;
|
||||
recomputil::reset_actor_data();
|
||||
}
|
||||
|
||||
extern "C" void recomp_create_actor_data(uint8_t* rdram, recomp_context* ctx) {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
|
||||
can_register = false;
|
||||
|
||||
// Determine the number of bytes to allocate based on the actor type's extensions and the generic extensions.
|
||||
u32 actor_type = _arg<0, u32>(rdram, ctx);
|
||||
u32 alloc_size = generic_data_size;
|
||||
[[maybe_unused]] u32 type_data_size = 0;
|
||||
|
||||
if (actor_type < actor_data_sizes.size()) {
|
||||
type_data_size = actor_data_sizes[actor_type];
|
||||
alloc_size += type_data_size;
|
||||
}
|
||||
|
||||
// Allocate the extension data if it's of nonzero size.
|
||||
PTR(void) data_ptr = NULLPTR;
|
||||
if (alloc_size != 0) {
|
||||
void* data = recomp::alloc(rdram, alloc_size);
|
||||
alloc_count++;
|
||||
data_ptr = reinterpret_cast<uint8_t*>(data) - rdram + 0xFFFFFFFF80000000U;
|
||||
// Zero the allocated memory.
|
||||
// A memset should be fine here since this data is aligned, but use a byteswapped loop just to be safe.
|
||||
for (size_t i = 0; i < alloc_size; i++) {
|
||||
MEM_B(i, data_ptr) = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the actor's fields to the actor data slotmap.
|
||||
u32 spawn_index = actor_spawn_count++;
|
||||
dod::slot_map_key32<ExtensionData> key = actor_data.emplace(ExtensionData{.actor_spawn_index = spawn_index, .data_addr = data_ptr});
|
||||
|
||||
// printf("Allocated actor data: address 0x%08X with 0x%08X bytes total (0x%08X bytes generic and 0x%08X bytes specific), handle 0x%08X, spawn index %d\n",
|
||||
// data_ptr, alloc_size, generic_data_size, type_data_size, key.raw, spawn_index);
|
||||
|
||||
_return<u32>(ctx, key.raw);
|
||||
}
|
||||
|
||||
extern "C" void recomp_destroy_actor_data(uint8_t* rdram, recomp_context* ctx) {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
|
||||
u32 actor_handle = _arg<0, u32>(rdram, ctx);
|
||||
actor_data_map_t::key actor_key{actor_handle};
|
||||
|
||||
ExtensionData* data = actor_data.get(actor_key);
|
||||
if (data != nullptr) {
|
||||
// printf("Freeing actor data: address 0x%08X handle 0x%08X\n", data->data_addr, actor_handle);
|
||||
if (data->data_addr != NULLPTR) {
|
||||
recomp::free(rdram, TO_PTR(void, data->data_addr));
|
||||
free_count++;
|
||||
}
|
||||
actor_data.erase(actor_data_map_t::key{actor_handle});
|
||||
}
|
||||
else {
|
||||
// Not an irrecoverable error, but catch it in debug mode with an assert to help find bugs.
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void recomp_get_actor_data(uint8_t* rdram, recomp_context* ctx) {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
|
||||
u32 actor_handle = _arg<0, u32>(rdram, ctx);
|
||||
u32 extension_handle = _arg<1, u32>(rdram, ctx);
|
||||
u32 actor_type = _arg<2, u32>(rdram, ctx);
|
||||
|
||||
// Check if the extension handle is valid.
|
||||
if (extension_handle == 0 || extension_handle >= actor_extensions.size()) {
|
||||
_return<PTR(void)>(ctx, NULLPTR);
|
||||
return;
|
||||
}
|
||||
|
||||
ExtensionInfo& extension = actor_extensions[extension_handle];
|
||||
bool generic_extension = extension.actor_type == 0xFFFFFFFFU;
|
||||
|
||||
// Check if the extension is generic or for the provided actor type.
|
||||
if (!generic_extension && extension.actor_type != actor_type) {
|
||||
_return<PTR(void)>(ctx, NULLPTR);
|
||||
return;
|
||||
}
|
||||
|
||||
actor_data_map_t::key actor_key{actor_handle};
|
||||
ExtensionData* data = actor_data.get(actor_key);
|
||||
|
||||
// Check if actor handle is valid.
|
||||
if (data == nullptr) {
|
||||
_return<PTR(void)>(ctx, NULLPTR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the address for this specific extension's data.
|
||||
PTR(void) base_address = data->data_addr;
|
||||
u32 offset = extension.data_offset;
|
||||
// Specific actor data is after generic actor data, so increase the offset by the total generic actor data if this isn't generic data.
|
||||
if (!generic_extension) {
|
||||
offset += generic_data_size;
|
||||
}
|
||||
|
||||
PTR(void) ret = base_address + offset;
|
||||
_return<PTR(void)>(ctx, ret);
|
||||
}
|
||||
|
||||
extern "C" void recomp_get_actor_spawn_index(uint8_t* rdram, recomp_context* ctx) {
|
||||
std::lock_guard lock{ actor_data_mutex };
|
||||
|
||||
u32 actor_handle = _arg<0, u32>(rdram, ctx);
|
||||
|
||||
actor_data_map_t::key actor_key{actor_handle};
|
||||
ExtensionData* data = actor_data.get(actor_key);
|
||||
|
||||
// Check if actor handle is valid.
|
||||
if (data == nullptr) {
|
||||
_return<u32>(ctx, 0xFFFFFFFFU);
|
||||
return;
|
||||
}
|
||||
|
||||
_return<u32>(ctx, data->actor_spawn_index);
|
||||
}
|
||||
|
727
src/game/recomp_data_api.cpp
Normal file
727
src/game/recomp_data_api.cpp
Normal file
@@ -0,0 +1,727 @@
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "slot_map.h"
|
||||
#include "recomp_data.h"
|
||||
#include "recomp_ui.h"
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "librecomp/overlays.hpp"
|
||||
#include "librecomp/addresses.hpp"
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
|
||||
template <typename KeyType, typename ValueType>
|
||||
class LockedMap {
|
||||
private:
|
||||
std::mutex mutex{};
|
||||
std::unordered_map<KeyType, ValueType> map{};
|
||||
public:
|
||||
bool get(const KeyType& key, ValueType& out) {
|
||||
std::lock_guard lock{mutex};
|
||||
auto find_it = map.find(key);
|
||||
if (find_it == map.end()) {
|
||||
return false;
|
||||
}
|
||||
out = find_it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool insert(const KeyType& key, ValueType val) {
|
||||
std::lock_guard lock{mutex};
|
||||
auto ret = map.insert_or_assign(key, val);
|
||||
return ret.second;
|
||||
}
|
||||
|
||||
bool erase(const KeyType& key) {
|
||||
std::lock_guard lock{mutex};
|
||||
size_t num_erased = map.erase(key);
|
||||
return num_erased != 0;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
std::lock_guard lock{mutex};
|
||||
map.clear();
|
||||
}
|
||||
|
||||
bool erase_first(ValueType& out) {
|
||||
std::lock_guard lock{mutex};
|
||||
auto it = map.begin();
|
||||
|
||||
if (it == map.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out = it->second;
|
||||
map.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool contains(const KeyType& key) {
|
||||
std::lock_guard lock{mutex};
|
||||
|
||||
return map.contains(key);
|
||||
}
|
||||
|
||||
size_t size() {
|
||||
std::lock_guard lock{mutex};
|
||||
|
||||
return map.size();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename KeyType>
|
||||
class LockedSet {
|
||||
private:
|
||||
std::mutex mutex{};
|
||||
std::unordered_set<KeyType> set{};
|
||||
public:
|
||||
bool contains(const KeyType& key) {
|
||||
std::lock_guard lock{mutex};
|
||||
return set.contains(key);
|
||||
}
|
||||
|
||||
bool insert(const KeyType& key) {
|
||||
std::lock_guard lock{mutex};
|
||||
auto it = set.insert(key);
|
||||
return it.second;
|
||||
}
|
||||
|
||||
bool erase(const KeyType& key) {
|
||||
std::lock_guard lock{mutex};
|
||||
size_t num_erased = set.erase(key);
|
||||
return num_erased != 0;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
std::lock_guard lock{mutex};
|
||||
set.clear();
|
||||
}
|
||||
|
||||
size_t size() {
|
||||
std::lock_guard lock{mutex};
|
||||
return set.size();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename ValueType>
|
||||
class LockedSlotmap {
|
||||
private:
|
||||
std::mutex mutex{};
|
||||
dod::slot_map32<ValueType> map{};
|
||||
using key_t = typename dod::slot_map32<ValueType>::key;
|
||||
public:
|
||||
bool get(uint32_t key, ValueType** out) {
|
||||
std::lock_guard lock{mutex};
|
||||
ValueType* ret = map.get(key_t{key});
|
||||
*out = ret;
|
||||
return ret != nullptr;
|
||||
}
|
||||
|
||||
uint32_t create() {
|
||||
std::lock_guard lock{mutex};
|
||||
return map.emplace().raw;
|
||||
}
|
||||
|
||||
bool erase(uint32_t key) {
|
||||
std::lock_guard lock{mutex};
|
||||
if (!map.has_key(key_t{ key })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
map.erase(key_t{ key });
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
std::lock_guard lock{mutex};
|
||||
map.clear();
|
||||
}
|
||||
|
||||
bool erase_first(ValueType& out) {
|
||||
std::lock_guard lock{mutex};
|
||||
auto it = map.items().begin();
|
||||
|
||||
if (it == map.items().end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out = it->second;
|
||||
map.erase(it->first);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t size() {
|
||||
std::lock_guard lock{mutex};
|
||||
|
||||
return map.size();
|
||||
}
|
||||
};
|
||||
|
||||
using U32ValueMap = LockedMap<uint32_t, uint32_t>;
|
||||
using U32MemoryMap = std::pair<LockedMap<uint32_t, PTR(void)>, u32>;
|
||||
using U32HashSet = LockedSet<uint32_t>;
|
||||
using U32Slotmap = LockedSlotmap<uint32_t>;
|
||||
using MemorySlotmap = std::pair<LockedSlotmap<PTR(void)>, u32>;
|
||||
|
||||
LockedSlotmap<U32ValueMap> u32_value_hashmaps{};
|
||||
LockedSlotmap<U32MemoryMap> u32_memory_hashmaps{};
|
||||
LockedSlotmap<U32HashSet> u32_hashsets{};
|
||||
LockedSlotmap<U32Slotmap> u32_slotmaps{};
|
||||
LockedSlotmap<MemorySlotmap> memory_slotmaps{};
|
||||
|
||||
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
|
||||
|
||||
static void show_fatal_error_message_box(const char* funcname, const char* errstr) {
|
||||
std::string message = std::string{"Fatal error in mod - "} + funcname + " : " + errstr;
|
||||
recompui::message_box(message.c_str());
|
||||
}
|
||||
|
||||
#define HANDLE_INVALID_ERROR() \
|
||||
show_fatal_error_message_box(__FUNCTION__, "handle is invalid"); \
|
||||
assert(false); \
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
|
||||
#define SLOTMAP_KEY_INVALID_ERROR() \
|
||||
show_fatal_error_message_box(__FUNCTION__, "slotmap key is invalid"); \
|
||||
assert(false); \
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
|
||||
// u32 -> 32-bit value hashmap.
|
||||
|
||||
void recomputil_create_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
(void)rdram;
|
||||
_return(ctx, u32_value_hashmaps.create());
|
||||
}
|
||||
|
||||
void recomputil_destroy_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
if (!u32_value_hashmaps.erase(mapkey)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
}
|
||||
|
||||
void recomputil_u32_value_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32ValueMap* map;
|
||||
if (!u32_value_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, map->contains(key));
|
||||
}
|
||||
|
||||
void recomputil_u32_value_hashmap_insert(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
|
||||
|
||||
U32ValueMap* map;
|
||||
if (!u32_value_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, map->insert(key, value));
|
||||
}
|
||||
|
||||
void recomputil_u32_value_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
|
||||
|
||||
U32ValueMap* map;
|
||||
if (!u32_value_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
uint32_t ret;
|
||||
if (map->get(key, ret)) {
|
||||
MEM_W(0, val_out) = ret;
|
||||
_return(ctx, 1);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
_return(ctx, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void recomputil_u32_value_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32ValueMap* map;
|
||||
if (!u32_value_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, map->erase(key));
|
||||
}
|
||||
|
||||
void recomputil_u32_value_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
U32ValueMap* map;
|
||||
if (!u32_value_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, static_cast<uint32_t>(map->size()));
|
||||
}
|
||||
|
||||
// u32 -> memory hashmap.
|
||||
|
||||
void recomputil_create_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t element_size = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
// Create the map.
|
||||
uint32_t map_key = u32_memory_hashmaps.create();
|
||||
|
||||
// Retrieve the map and set its element size to the provided value.
|
||||
U32MemoryMap* map;
|
||||
u32_memory_hashmaps.get(map_key, &map);
|
||||
map->second = element_size;
|
||||
|
||||
// Return the created map's key.
|
||||
_return(ctx, map_key);
|
||||
}
|
||||
|
||||
void recomputil_destroy_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
// Retrieve the map.
|
||||
U32MemoryMap* map;
|
||||
if (!u32_memory_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
// Free all of the entries in the map.
|
||||
PTR(void) cur_mem;
|
||||
while (map->first.erase_first(cur_mem)) {
|
||||
recomp::free(rdram, TO_PTR(void, cur_mem));
|
||||
}
|
||||
|
||||
// Destroy the map itself.
|
||||
u32_memory_hashmaps.erase(mapkey);
|
||||
}
|
||||
|
||||
void recomputil_u32_memory_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32MemoryMap* map;
|
||||
if (!u32_memory_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, map->first.contains(key));
|
||||
}
|
||||
|
||||
void recomputil_u32_memory_hashmap_create(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32MemoryMap* map;
|
||||
if (!u32_memory_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
// Check if the map contains the key already to prevent inserting it twice.
|
||||
PTR(void) dummy;
|
||||
if (map->first.get(key, dummy)) {
|
||||
_return(ctx, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate the map's size and return the pointer.
|
||||
void* mem = recomp::alloc(rdram, map->second);
|
||||
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
|
||||
|
||||
// Zero the memory.
|
||||
for (size_t i = 0; i < map->second; i++) {
|
||||
MEM_B(i, addr) = 0;
|
||||
}
|
||||
|
||||
PTR(void) ret = static_cast<PTR(void)>(addr);
|
||||
map->first.insert(key, ret);
|
||||
_return(ctx, 1);
|
||||
}
|
||||
|
||||
void recomputil_u32_memory_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32MemoryMap* map;
|
||||
if (!u32_memory_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
PTR(void) ret;
|
||||
if (map->first.get(key, ret)) {
|
||||
_return(ctx, ret);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
_return(ctx, NULLPTR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void recomputil_u32_memory_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32MemoryMap* map;
|
||||
if (!u32_memory_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
// Free the memory for this key if the key exists.
|
||||
PTR(void) addr;
|
||||
bool has_value = map->first.get(key, addr);
|
||||
if (has_value) {
|
||||
void* mem = TO_PTR(void, addr);
|
||||
recomp::free(rdram, mem);
|
||||
}
|
||||
|
||||
_return(ctx, map->first.erase(key));
|
||||
}
|
||||
|
||||
void recomputil_u32_memory_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
U32MemoryMap* map;
|
||||
if (!u32_memory_hashmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, static_cast<uint32_t>(map->first.size()));
|
||||
}
|
||||
|
||||
// u32 hashset.
|
||||
|
||||
void recomputil_create_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
|
||||
(void)rdram;
|
||||
_return(ctx, u32_hashsets.create());
|
||||
}
|
||||
|
||||
void recomputil_destroy_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
if (!u32_hashsets.erase(setkey)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
}
|
||||
|
||||
void recomputil_u32_hashset_contains(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32HashSet* set;
|
||||
if (!u32_hashsets.get(setkey, &set)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, set->contains(key));
|
||||
}
|
||||
|
||||
void recomputil_u32_hashset_insert(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32HashSet* set;
|
||||
if (!u32_hashsets.get(setkey, &set)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, set->insert(key));
|
||||
}
|
||||
|
||||
void recomputil_u32_hashset_erase(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32HashSet* set;
|
||||
if (!u32_hashsets.get(setkey, &set)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, set->erase(key));
|
||||
}
|
||||
|
||||
void recomputil_u32_hashset_size(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
U32HashSet* set;
|
||||
if (!u32_hashsets.get(setkey, &set)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, static_cast<uint32_t>(set->size()));
|
||||
}
|
||||
|
||||
// u32 value slotmap.
|
||||
|
||||
void recomputil_create_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
(void)rdram;
|
||||
_return(ctx, u32_slotmaps.create());
|
||||
}
|
||||
|
||||
void recomputil_destroy_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
if (!u32_slotmaps.erase(mapkey)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
}
|
||||
|
||||
void recomputil_u32_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32Slotmap* map;
|
||||
if (!u32_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
uint32_t* dummy_ptr;
|
||||
_return(ctx, map->get(key, &dummy_ptr));
|
||||
}
|
||||
|
||||
void recomputil_u32_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
U32Slotmap* map;
|
||||
if (!u32_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, map->create());
|
||||
}
|
||||
|
||||
void recomputil_u32_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
|
||||
|
||||
U32Slotmap* map;
|
||||
if (!u32_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
uint32_t* ret;
|
||||
if (!map->get(key, &ret)) {
|
||||
_return(ctx, 0);
|
||||
}
|
||||
MEM_W(0, val_out) = *ret;
|
||||
_return(ctx, 1);
|
||||
}
|
||||
|
||||
void recomputil_u32_slotmap_set(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
|
||||
|
||||
U32Slotmap* map;
|
||||
if (!u32_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
uint32_t* value_ptr;
|
||||
if (!map->get(key, &value_ptr)) {
|
||||
_return(ctx, 0);
|
||||
}
|
||||
|
||||
*value_ptr = value;
|
||||
_return(ctx, 1);
|
||||
}
|
||||
|
||||
void recomputil_u32_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
U32Slotmap* map;
|
||||
if (!u32_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
if (!map->erase(key)) {
|
||||
_return(ctx, 0);
|
||||
}
|
||||
|
||||
_return(ctx, 1);
|
||||
}
|
||||
|
||||
void recomputil_u32_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
U32Slotmap* map;
|
||||
if (!u32_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, static_cast<uint32_t>(map->size()));
|
||||
}
|
||||
|
||||
// memory slotmap.
|
||||
|
||||
void recomputil_create_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
(void)rdram;
|
||||
_return(ctx, memory_slotmaps.create());
|
||||
}
|
||||
|
||||
void recomputil_destroy_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
// Retrieve the map.
|
||||
MemorySlotmap* map;
|
||||
if (!memory_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
// Free all of the entries in the map.
|
||||
PTR(void) cur_mem;
|
||||
while (map->first.erase_first(cur_mem)) {
|
||||
recomp::free(rdram, TO_PTR(void, cur_mem));
|
||||
}
|
||||
|
||||
// Destroy the map itself.
|
||||
memory_slotmaps.erase(mapkey);
|
||||
}
|
||||
|
||||
void recomputil_memory_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
MemorySlotmap* map;
|
||||
if (!memory_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
PTR(void)* dummy_ptr;
|
||||
_return(ctx, map->first.get(key, &dummy_ptr));
|
||||
}
|
||||
|
||||
void recomputil_memory_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
MemorySlotmap* map;
|
||||
if (!memory_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
// Create the slotmap element.
|
||||
u32 key = map->first.create();
|
||||
|
||||
// Allocate the map's element size.
|
||||
void* mem = recomp::alloc(rdram, map->second);
|
||||
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
|
||||
|
||||
// Zero the memory.
|
||||
for (size_t i = 0; i < map->second; i++) {
|
||||
MEM_B(i, addr) = 0;
|
||||
}
|
||||
|
||||
// Store the allocated pointer.
|
||||
PTR(void)* value_ptr;
|
||||
map->first.get(key, &value_ptr);
|
||||
MEM_W(0, *value_ptr) = addr;
|
||||
|
||||
// Return the key.
|
||||
_return(ctx, key);
|
||||
}
|
||||
|
||||
void recomputil_memory_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
|
||||
|
||||
MemorySlotmap* map;
|
||||
if (!memory_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
PTR(void)* ret;
|
||||
if (!map->first.get(key, &ret)) {
|
||||
SLOTMAP_KEY_INVALID_ERROR();
|
||||
}
|
||||
MEM_W(0, val_out) = *ret;
|
||||
}
|
||||
|
||||
void recomputil_memory_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
MemorySlotmap* map;
|
||||
if (!memory_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
// Free the memory for this key if the key exists.
|
||||
PTR(void)* addr;
|
||||
bool has_value = map->first.get(key, &addr);
|
||||
if (has_value) {
|
||||
void* mem = TO_PTR(void, addr);
|
||||
recomp::free(rdram, mem);
|
||||
}
|
||||
|
||||
_return(ctx, map->first.erase(key));
|
||||
}
|
||||
|
||||
void recomputil_memory_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
MemorySlotmap* map;
|
||||
if (!memory_slotmaps.get(mapkey, &map)) {
|
||||
HANDLE_INVALID_ERROR();
|
||||
}
|
||||
|
||||
_return(ctx, static_cast<uint32_t>(map->first.size()));
|
||||
}
|
||||
|
||||
// Exports.
|
||||
|
||||
void recomputil::register_data_api_exports() {
|
||||
REGISTER_FUNC(recomputil_create_u32_value_hashmap);
|
||||
REGISTER_FUNC(recomputil_destroy_u32_value_hashmap);
|
||||
REGISTER_FUNC(recomputil_u32_value_hashmap_contains);
|
||||
REGISTER_FUNC(recomputil_u32_value_hashmap_insert);
|
||||
REGISTER_FUNC(recomputil_u32_value_hashmap_get);
|
||||
REGISTER_FUNC(recomputil_u32_value_hashmap_erase);
|
||||
REGISTER_FUNC(recomputil_u32_value_hashmap_size);
|
||||
|
||||
REGISTER_FUNC(recomputil_create_u32_memory_hashmap);
|
||||
REGISTER_FUNC(recomputil_destroy_u32_memory_hashmap);
|
||||
REGISTER_FUNC(recomputil_u32_memory_hashmap_contains);
|
||||
REGISTER_FUNC(recomputil_u32_memory_hashmap_create);
|
||||
REGISTER_FUNC(recomputil_u32_memory_hashmap_get);
|
||||
REGISTER_FUNC(recomputil_u32_memory_hashmap_erase);
|
||||
REGISTER_FUNC(recomputil_u32_memory_hashmap_size);
|
||||
|
||||
REGISTER_FUNC(recomputil_create_u32_hashset);
|
||||
REGISTER_FUNC(recomputil_destroy_u32_hashset);
|
||||
REGISTER_FUNC(recomputil_u32_hashset_contains);
|
||||
REGISTER_FUNC(recomputil_u32_hashset_insert);
|
||||
REGISTER_FUNC(recomputil_u32_hashset_erase);
|
||||
REGISTER_FUNC(recomputil_u32_hashset_size);
|
||||
|
||||
REGISTER_FUNC(recomputil_create_u32_slotmap);
|
||||
REGISTER_FUNC(recomputil_destroy_u32_slotmap);
|
||||
REGISTER_FUNC(recomputil_u32_slotmap_contains);
|
||||
REGISTER_FUNC(recomputil_u32_slotmap_create);
|
||||
REGISTER_FUNC(recomputil_u32_slotmap_get);
|
||||
REGISTER_FUNC(recomputil_u32_slotmap_set);
|
||||
REGISTER_FUNC(recomputil_u32_slotmap_erase);
|
||||
REGISTER_FUNC(recomputil_u32_slotmap_size);
|
||||
|
||||
REGISTER_FUNC(recomputil_create_memory_slotmap);
|
||||
REGISTER_FUNC(recomputil_destroy_memory_slotmap);
|
||||
REGISTER_FUNC(recomputil_memory_slotmap_contains);
|
||||
REGISTER_FUNC(recomputil_memory_slotmap_create);
|
||||
REGISTER_FUNC(recomputil_memory_slotmap_get);
|
||||
REGISTER_FUNC(recomputil_memory_slotmap_erase);
|
||||
REGISTER_FUNC(recomputil_memory_slotmap_size);
|
||||
}
|
@@ -27,6 +27,7 @@
|
||||
#include "zelda_render.h"
|
||||
#include "zelda_support.h"
|
||||
#include "zelda_game.h"
|
||||
#include "recomp_data.h"
|
||||
#include "ovl_patches.hpp"
|
||||
#include "librecomp/game.hpp"
|
||||
#include "librecomp/mods.hpp"
|
||||
@@ -37,6 +38,8 @@
|
||||
#include "../../patches/sound.h"
|
||||
#include "../../patches/misc_funcs.h"
|
||||
|
||||
#include "mods/mm_recomp_dpad_builtin.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
@@ -544,15 +547,17 @@ void release_preload(PreloadContext& context) {
|
||||
#endif
|
||||
|
||||
void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
|
||||
(void)context;
|
||||
zelda64::renderer::enable_texture_pack(mod);
|
||||
zelda64::renderer::enable_texture_pack(context, mod);
|
||||
}
|
||||
|
||||
void disable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
|
||||
(void)context;
|
||||
void disable_texture_pack(recomp::mods::ModContext&, const recomp::mods::ModHandle& mod) {
|
||||
zelda64::renderer::disable_texture_pack(mod);
|
||||
}
|
||||
|
||||
void reorder_texture_pack(recomp::mods::ModContext&) {
|
||||
zelda64::renderer::trigger_texture_pack_update();
|
||||
}
|
||||
|
||||
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -614,6 +619,8 @@ int main(int argc, char** argv) {
|
||||
recomp::register_game(game);
|
||||
}
|
||||
|
||||
recomp::mods::register_embedded_mod("mm_recomp_dpad_builtin", { (const uint8_t*)(mm_recomp_dpad_builtin), std::size(mm_recomp_dpad_builtin)});
|
||||
|
||||
REGISTER_FUNC(recomp_get_window_resolution);
|
||||
REGISTER_FUNC(recomp_get_target_aspect_ratio);
|
||||
REGISTER_FUNC(recomp_get_target_framerate);
|
||||
@@ -627,9 +634,12 @@ int main(int argc, char** argv) {
|
||||
REGISTER_FUNC(recomp_get_mouse_deltas);
|
||||
REGISTER_FUNC(recomp_get_inverted_axes);
|
||||
REGISTER_FUNC(recomp_get_analog_inverted_axes);
|
||||
recompui::register_ui_exports();
|
||||
recomputil::register_data_api_exports();
|
||||
|
||||
zelda64::register_overlays();
|
||||
zelda64::register_patches();
|
||||
recomputil::init_extended_actor_data();
|
||||
zelda64::load_config();
|
||||
|
||||
recomp::rsp::callbacks_t rsp_callbacks{
|
||||
@@ -678,39 +688,13 @@ int main(int argc, char** argv) {
|
||||
.allow_runtime_toggle = true,
|
||||
.on_enabled = enable_texture_pack,
|
||||
.on_disabled = disable_texture_pack,
|
||||
.on_reordered = reorder_texture_pack,
|
||||
};
|
||||
auto texture_pack_content_type_id = recomp::mods::register_mod_content_type(texture_pack_content_type);
|
||||
|
||||
// Register the .rtz texture pack file format with the previous content type as its only allowed content type.
|
||||
recomp::mods::register_mod_container_type("rtz", std::vector{ texture_pack_content_type_id }, false);
|
||||
|
||||
recomp::mods::scan_mods();
|
||||
|
||||
printf("Found mods:\n");
|
||||
for (const auto& mod : recomp::mods::get_mod_details("mm")) {
|
||||
printf(" %s(%s)\n", mod.mod_id.c_str(), mod.version.to_string().c_str());
|
||||
if (!mod.authors.empty()) {
|
||||
printf(" Authors: %s", mod.authors[0].c_str());
|
||||
for (size_t author_index = 1; author_index < mod.authors.size(); author_index++) {
|
||||
const std::string& author = mod.authors[author_index];
|
||||
printf(", %s", author.c_str());
|
||||
}
|
||||
printf("\n");
|
||||
printf(" Runtime toggleable: %d\n", mod.runtime_toggleable);
|
||||
}
|
||||
if (!mod.dependencies.empty()) {
|
||||
printf(" Dependencies: %s:%s", mod.dependencies[0].mod_id.c_str(), mod.dependencies[0].version.to_string().c_str());
|
||||
for (size_t dep_index = 1; dep_index < mod.dependencies.size(); dep_index++) {
|
||||
const recomp::mods::Dependency& dep = mod.dependencies[dep_index];
|
||||
printf(", %s:%s", dep.mod_id.c_str(), dep.version.to_string().c_str());
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
// TODO load all mods as a temporary solution to not having a UI yet.
|
||||
recomp::mods::enable_mod(mod.mod_id, true);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
recomp::start(
|
||||
project_version,
|
||||
{},
|
||||
|
@@ -1,10 +1,12 @@
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
#include <variant>
|
||||
#include <algorithm>
|
||||
|
||||
#define HLSL_CPU
|
||||
#include "hle/rt64_application.h"
|
||||
#include "rt64_render_hooks.h"
|
||||
#include "overloaded.h"
|
||||
|
||||
#include "ultramodern/ultramodern.hpp"
|
||||
#include "ultramodern/config.hpp"
|
||||
@@ -13,12 +15,6 @@
|
||||
#include "recomp_ui.h"
|
||||
#include "concurrentqueue.h"
|
||||
|
||||
// Helper class for variant visiting.
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts... { using Ts::operator()...; };
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
|
||||
static bool sample_positions_supported = false;
|
||||
static bool high_precision_fb_enabled = false;
|
||||
@@ -27,14 +23,25 @@ static uint8_t DMEM[0x1000];
|
||||
static uint8_t IMEM[0x1000];
|
||||
|
||||
struct TexturePackEnableAction {
|
||||
std::filesystem::path path;
|
||||
std::string mod_id;
|
||||
};
|
||||
|
||||
struct TexturePackDisableAction {
|
||||
std::filesystem::path path;
|
||||
std::string mod_id;
|
||||
};
|
||||
|
||||
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction>;
|
||||
struct TexturePackSecondaryEnableAction {
|
||||
std::string mod_id;
|
||||
};
|
||||
|
||||
struct TexturePackSecondaryDisableAction {
|
||||
std::string mod_id;
|
||||
};
|
||||
|
||||
struct TexturePackUpdateAction {
|
||||
};
|
||||
|
||||
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction, TexturePackSecondaryEnableAction, TexturePackSecondaryDisableAction, TexturePackUpdateAction>;
|
||||
|
||||
static moodycamel::ConcurrentQueue<TexturePackAction> texture_pack_action_queue;
|
||||
|
||||
@@ -171,6 +178,7 @@ void set_application_user_config(RT64::Application* application, const ultramode
|
||||
application->userConfig.refreshRate = to_rt64(config.rr_option);
|
||||
application->userConfig.refreshRateTarget = config.rr_manual_value;
|
||||
application->userConfig.internalColorFormat = to_rt64(config.hpfb_option);
|
||||
application->userConfig.displayBuffering = RT64::UserConfiguration::DisplayBuffering::Triple;
|
||||
}
|
||||
|
||||
ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
|
||||
@@ -192,6 +200,23 @@ ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResu
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
ultramodern::renderer::GraphicsApi map_graphics_api(RT64::UserConfiguration::GraphicsAPI api) {
|
||||
switch (api) {
|
||||
case RT64::UserConfiguration::GraphicsAPI::D3D12:
|
||||
return ultramodern::renderer::GraphicsApi::D3D12;
|
||||
case RT64::UserConfiguration::GraphicsAPI::Vulkan:
|
||||
return ultramodern::renderer::GraphicsApi::Vulkan;
|
||||
case RT64::UserConfiguration::GraphicsAPI::Metal:
|
||||
return ultramodern::renderer::GraphicsApi::Metal;
|
||||
case RT64::UserConfiguration::GraphicsAPI::Automatic:
|
||||
return ultramodern::renderer::GraphicsApi::Auto;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Unhandled `RT64::UserConfiguration::GraphicsAPI` ?\n");
|
||||
assert(false);
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool debug) {
|
||||
static unsigned char dummy_rom_header[0x40];
|
||||
recompui::set_render_hooks();
|
||||
@@ -266,9 +291,8 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
|
||||
case ultramodern::renderer::GraphicsApi::Metal:
|
||||
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Metal;
|
||||
break;
|
||||
default:
|
||||
case ultramodern::renderer::GraphicsApi::Auto:
|
||||
// Don't override if auto is selected.
|
||||
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Automatic;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -278,6 +302,8 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
|
||||
thread_id = window_handle.thread_id;
|
||||
#endif
|
||||
setup_result = map_setup_result(app->setup(thread_id));
|
||||
// Get the API that RT64 chose.
|
||||
chosen_api = map_graphics_api(app->chosenGraphicsAPI);
|
||||
if (setup_result != ultramodern::renderer::SetupResult::Success) {
|
||||
app = nullptr;
|
||||
return;
|
||||
@@ -306,32 +332,7 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
|
||||
zelda64::renderer::RT64Context::~RT64Context() = default;
|
||||
|
||||
void zelda64::renderer::RT64Context::send_dl(const OSTask* task) {
|
||||
bool packs_disabled = false;
|
||||
TexturePackAction cur_action;
|
||||
while (texture_pack_action_queue.try_dequeue(cur_action)) {
|
||||
std::visit(overloaded{
|
||||
[&](TexturePackDisableAction& to_disable) {
|
||||
enabled_texture_packs.erase(to_disable.path);
|
||||
packs_disabled = true;
|
||||
},
|
||||
[&](TexturePackEnableAction& to_enable) {
|
||||
enabled_texture_packs.insert(to_enable.path);
|
||||
// Load the pack now if no packs have been disabled.
|
||||
if (!packs_disabled) {
|
||||
app->textureCache->loadReplacementDirectory(to_enable.path);
|
||||
}
|
||||
}
|
||||
}, cur_action);
|
||||
}
|
||||
|
||||
// If any packs were disabled, unload all packs and load all the active ones.
|
||||
if (packs_disabled) {
|
||||
app->textureCache->clearReplacementDirectories();
|
||||
for (const std::filesystem::path& cur_pack_path : enabled_texture_packs) {
|
||||
app->textureCache->loadReplacementDirectory(cur_pack_path);
|
||||
}
|
||||
}
|
||||
|
||||
check_texture_pack_actions();
|
||||
app->state->rsp->reset();
|
||||
app->interpreter->loadUCodeGBI(task->t.ucode & 0x3FFFFFF, task->t.ucode_data & 0x3FFFFFF, true);
|
||||
app->processDisplayLists(app->core.RDRAM, task->t.data_ptr & 0x3FFFFFF, 0, true);
|
||||
@@ -397,6 +398,66 @@ float zelda64::renderer::RT64Context::get_resolution_scale() const {
|
||||
}
|
||||
}
|
||||
|
||||
void zelda64::renderer::RT64Context::check_texture_pack_actions() {
|
||||
bool packs_changed = false;
|
||||
TexturePackAction cur_action;
|
||||
while (texture_pack_action_queue.try_dequeue(cur_action)) {
|
||||
std::visit(overloaded{
|
||||
[&](TexturePackDisableAction &to_disable) {
|
||||
enabled_texture_packs.erase(to_disable.mod_id);
|
||||
packs_changed = true;
|
||||
},
|
||||
[&](TexturePackEnableAction &to_enable) {
|
||||
enabled_texture_packs.insert(to_enable.mod_id);
|
||||
packs_changed = true;
|
||||
},
|
||||
[&](TexturePackSecondaryDisableAction &to_override_disable) {
|
||||
secondary_disabled_texture_packs.insert(to_override_disable.mod_id);
|
||||
packs_changed = true;
|
||||
},
|
||||
[&](TexturePackSecondaryEnableAction &to_override_enable) {
|
||||
secondary_disabled_texture_packs.erase(to_override_enable.mod_id);
|
||||
packs_changed = true;
|
||||
},
|
||||
[&](TexturePackUpdateAction &) {
|
||||
packs_changed = true;
|
||||
}
|
||||
}, cur_action);
|
||||
}
|
||||
|
||||
// If any packs were disabled, unload all packs and load all the active ones.
|
||||
if (packs_changed) {
|
||||
// Sort the enabled texture packs in reverse order so that earlier ones override later ones.
|
||||
std::vector<std::string> sorted_texture_packs{};
|
||||
sorted_texture_packs.reserve(enabled_texture_packs.size());
|
||||
for (const std::string& mod : enabled_texture_packs) {
|
||||
if (!secondary_disabled_texture_packs.contains(mod)) {
|
||||
sorted_texture_packs.emplace_back(mod);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(sorted_texture_packs.begin(), sorted_texture_packs.end(),
|
||||
[](const std::string& lhs, const std::string& rhs) {
|
||||
return recomp::mods::get_mod_order_index(lhs) > recomp::mods::get_mod_order_index(rhs);
|
||||
}
|
||||
);
|
||||
|
||||
// Build the path list from the sorted mod list.
|
||||
std::vector<RT64::ReplacementDirectory> replacement_directories;
|
||||
replacement_directories.reserve(enabled_texture_packs.size());
|
||||
for (const std::string &mod_id : sorted_texture_packs) {
|
||||
replacement_directories.emplace_back(RT64::ReplacementDirectory(recomp::mods::get_mod_filename(mod_id)));
|
||||
}
|
||||
|
||||
if (!replacement_directories.empty()) {
|
||||
app->textureCache->loadReplacementDirectories(replacement_directories);
|
||||
}
|
||||
else {
|
||||
app->textureCache->clearReplacementDirectories();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RT64::UserConfiguration::Antialiasing zelda64::renderer::RT64MaxMSAA() {
|
||||
return device_max_msaa;
|
||||
}
|
||||
@@ -413,10 +474,72 @@ bool zelda64::renderer::RT64HighPrecisionFBEnabled() {
|
||||
return high_precision_fb_enabled;
|
||||
}
|
||||
|
||||
void zelda64::renderer::enable_texture_pack(const recomp::mods::ModHandle& mod) {
|
||||
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_root_path});
|
||||
void zelda64::renderer::trigger_texture_pack_update() {
|
||||
texture_pack_action_queue.enqueue(TexturePackUpdateAction{});
|
||||
}
|
||||
|
||||
void zelda64::renderer::enable_texture_pack(const recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
|
||||
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_id});
|
||||
|
||||
// Check for the texture pack enabled config option.
|
||||
const recomp::mods::ConfigSchema& config_schema = context.get_mod_config_schema(mod.manifest.mod_id);
|
||||
auto find_it = config_schema.options_by_id.find(zelda64::renderer::special_option_texture_pack_enabled);
|
||||
if (find_it != config_schema.options_by_id.end()) {
|
||||
const recomp::mods::ConfigOption& config_option = config_schema.options[find_it->second];
|
||||
|
||||
if (is_texture_pack_enable_config_option(config_option, false)) {
|
||||
recomp::mods::ConfigValueVariant value_variant = context.get_mod_config_value(mod.manifest.mod_id, config_option.id);
|
||||
uint32_t value;
|
||||
if (uint32_t* value_ptr = std::get_if<uint32_t>(&value_variant)) {
|
||||
value = *value_ptr;
|
||||
}
|
||||
else {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
zelda64::renderer::secondary_enable_texture_pack(mod.manifest.mod_id);
|
||||
}
|
||||
else {
|
||||
zelda64::renderer::secondary_disable_texture_pack(mod.manifest.mod_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void zelda64::renderer::disable_texture_pack(const recomp::mods::ModHandle& mod) {
|
||||
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_root_path});
|
||||
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_id});
|
||||
}
|
||||
|
||||
void zelda64::renderer::secondary_enable_texture_pack(const std::string& mod_id) {
|
||||
texture_pack_action_queue.enqueue(TexturePackSecondaryEnableAction{mod_id});
|
||||
}
|
||||
|
||||
void zelda64::renderer::secondary_disable_texture_pack(const std::string& mod_id) {
|
||||
texture_pack_action_queue.enqueue(TexturePackSecondaryDisableAction{mod_id});
|
||||
}
|
||||
|
||||
|
||||
// HD texture enable option. Must be an enum with two options.
|
||||
// The first option is treated as disabled and the second option is treated as enabled.
|
||||
bool zelda64::renderer::is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors) {
|
||||
if (option.id == zelda64::renderer::special_option_texture_pack_enabled) {
|
||||
if (option.type != recomp::mods::ConfigOptionType::Enum) {
|
||||
if (show_errors) {
|
||||
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + zelda64::renderer::special_option_texture_pack_enabled + "\"), but the config option is not an enum.").c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
|
||||
if (option_enum.options.size() != 2) {
|
||||
if (show_errors) {
|
||||
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + zelda64::renderer::special_option_texture_pack_enabled + "\"), but the config option doesn't have exactly 2 values.").c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@@ -20,6 +20,29 @@ namespace zelda64 {
|
||||
callback(success, path);
|
||||
}
|
||||
|
||||
void perform_file_dialog_operation_multiple(const std::function<void(bool, const std::list<std::filesystem::path>&)>& callback) {
|
||||
const nfdpathset_t* native_paths = nullptr;
|
||||
nfdresult_t result = NFD_OpenDialogMultipleN(&native_paths, nullptr, 0, nullptr);
|
||||
|
||||
bool success = (result == NFD_OKAY);
|
||||
std::list<std::filesystem::path> paths;
|
||||
nfdpathsetsize_t count = 0;
|
||||
|
||||
if (success) {
|
||||
NFD_PathSet_GetCount(native_paths, &count);
|
||||
for (nfdpathsetsize_t i = 0; i < count; i++) {
|
||||
nfdnchar_t* cur_path = nullptr;
|
||||
nfdresult_t cur_result = NFD_PathSet_GetPathN(native_paths, i, &cur_path);
|
||||
if (cur_result == NFD_OKAY) {
|
||||
paths.emplace_back(std::filesystem::path{cur_path});
|
||||
}
|
||||
}
|
||||
NFD_PathSet_Free(native_paths);
|
||||
}
|
||||
|
||||
callback(success, paths);
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
std::filesystem::path get_asset_path(const char* asset) {
|
||||
@@ -41,6 +64,16 @@ namespace zelda64 {
|
||||
#endif
|
||||
}
|
||||
|
||||
void open_file_dialog_multiple(std::function<void(bool success, const std::list<std::filesystem::path>& paths)> callback) {
|
||||
#ifdef __APPLE__
|
||||
dispatch_on_ui_thread([callback]() {
|
||||
perform_file_dialog_operation_multiple(callback);
|
||||
});
|
||||
#else
|
||||
perform_file_dialog_operation_multiple(callback);
|
||||
#endif
|
||||
}
|
||||
|
||||
void show_error_message_box(const char *title, const char *message) {
|
||||
#ifdef __APPLE__
|
||||
std::string title_copy(title);
|
||||
|
668
src/ui/core/ui_context.cpp
Normal file
668
src/ui/core/ui_context.cpp
Normal file
@@ -0,0 +1,668 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <fstream>
|
||||
|
||||
#include "slot_map.h"
|
||||
#include "RmlUi/Core/StreamMemory.h"
|
||||
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
#include "recomp_ui.h"
|
||||
#include "ui_context.h"
|
||||
#include "../elements/ui_element.h"
|
||||
|
||||
// Hash implementations for ContextId and ResourceId.
|
||||
template <>
|
||||
struct std::hash<recompui::ContextId> {
|
||||
std::size_t operator()(const recompui::ContextId& id) const {
|
||||
return std::hash<uint32_t>()(id.slot_id);
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct std::hash<recompui::ResourceId> {
|
||||
std::size_t operator()(const recompui::ResourceId& id) const {
|
||||
return std::hash<uint32_t>()(id.slot_id);
|
||||
}
|
||||
};
|
||||
|
||||
using resource_slotmap = dod::slot_map32<std::unique_ptr<recompui::Style>>;
|
||||
|
||||
namespace recompui {
|
||||
struct Context {
|
||||
std::mutex context_lock;
|
||||
resource_slotmap resources;
|
||||
Rml::ElementDocument* document;
|
||||
Element root_element;
|
||||
Element* autofocus_element = nullptr;
|
||||
std::vector<Element*> loose_elements;
|
||||
std::unordered_set<ResourceId> to_update;
|
||||
bool captures_input = true;
|
||||
bool captures_mouse = true;
|
||||
Context(Rml::ElementDocument* document) : document(document), root_element(document) {}
|
||||
};
|
||||
} // namespace recompui
|
||||
|
||||
using context_slotmap = dod::slot_map32<recompui::Context>;
|
||||
|
||||
static struct {
|
||||
std::recursive_mutex all_contexts_lock;
|
||||
context_slotmap all_contexts;
|
||||
std::unordered_set<recompui::ContextId> opened_contexts;
|
||||
std::unordered_map<Rml::ElementDocument*, recompui::ContextId> documents_to_contexts;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> style_sheet;
|
||||
} context_state;
|
||||
|
||||
thread_local recompui::Context* opened_context = nullptr;
|
||||
thread_local recompui::ContextId opened_context_id = recompui::ContextId::null();
|
||||
|
||||
enum class ContextErrorType {
|
||||
OpenWithoutClose,
|
||||
OpenInvalidContext,
|
||||
CloseWithoutOpen,
|
||||
CloseWrongContext,
|
||||
DestroyInvalidContext,
|
||||
GetContextWithoutOpen,
|
||||
AddResourceWithoutOpen,
|
||||
AddResourceToWrongContext,
|
||||
UpdateElementWithoutContext,
|
||||
UpdateElementInWrongContext,
|
||||
GetResourceWithoutOpen,
|
||||
GetResourceFailed,
|
||||
DestroyResourceWithoutOpen,
|
||||
DestroyResourceInWrongContext,
|
||||
DestroyResourceNotFound,
|
||||
GetDocumentInvalidContext,
|
||||
GetAutofocusInvalidContext,
|
||||
SetAutofocusInvalidContext,
|
||||
InternalError,
|
||||
};
|
||||
|
||||
enum class SlotTag : uint8_t {
|
||||
Style = 0,
|
||||
Element = 1,
|
||||
};
|
||||
|
||||
void context_error(recompui::ContextId id, ContextErrorType type) {
|
||||
(void)id;
|
||||
|
||||
const char* error_message = "";
|
||||
|
||||
switch (type) {
|
||||
case ContextErrorType::OpenWithoutClose:
|
||||
error_message = "Attempted to open a UI context without closing another UI context";
|
||||
break;
|
||||
case ContextErrorType::OpenInvalidContext:
|
||||
error_message = "Attempted to open an invalid UI context";
|
||||
break;
|
||||
case ContextErrorType::CloseWithoutOpen:
|
||||
error_message = "Attempted to close a UI context without one being open";
|
||||
break;
|
||||
case ContextErrorType::CloseWrongContext:
|
||||
error_message = "Attempted to close a different UI context than the one that's open";
|
||||
break;
|
||||
case ContextErrorType::DestroyInvalidContext:
|
||||
error_message = "Attempted to destroy an invalid UI element";
|
||||
break;
|
||||
case ContextErrorType::GetContextWithoutOpen:
|
||||
error_message = "Attempted to get the current UI context with no UI context open";
|
||||
break;
|
||||
case ContextErrorType::AddResourceWithoutOpen:
|
||||
error_message = "Attempted to create a UI resource with no open UI context";
|
||||
break;
|
||||
case ContextErrorType::AddResourceToWrongContext:
|
||||
error_message = "Attempted to create a UI resource in a different UI context than the one that's open";
|
||||
break;
|
||||
case ContextErrorType::UpdateElementWithoutContext:
|
||||
error_message = "Attempted to update a UI element with no open UI context";
|
||||
break;
|
||||
case ContextErrorType::UpdateElementInWrongContext:
|
||||
error_message = "Attempted to update a UI element in a different UI context than the one that's open";
|
||||
break;
|
||||
case ContextErrorType::GetResourceWithoutOpen:
|
||||
error_message = "Attempted to get a UI resource with no open UI context";
|
||||
break;
|
||||
case ContextErrorType::GetResourceFailed:
|
||||
error_message = "Failed to get a UI resource from the current open UI context";
|
||||
break;
|
||||
case ContextErrorType::DestroyResourceWithoutOpen:
|
||||
error_message = "Attempted to destroy a UI resource with no open UI context";
|
||||
break;
|
||||
case ContextErrorType::DestroyResourceInWrongContext:
|
||||
error_message = "Attempted to destroy a UI resource in a different UI context than the one that's open";
|
||||
break;
|
||||
case ContextErrorType::DestroyResourceNotFound:
|
||||
error_message = "Attempted to destroy a UI resource that doesn't exist in the current context";
|
||||
break;
|
||||
case ContextErrorType::GetDocumentInvalidContext:
|
||||
error_message = "Attempted to get the document of an invalid UI context";
|
||||
break;
|
||||
case ContextErrorType::GetAutofocusInvalidContext:
|
||||
error_message = "Attempted to get the autofocus element of an invalid UI context";
|
||||
break;
|
||||
case ContextErrorType::SetAutofocusInvalidContext:
|
||||
error_message = "Attempted to set the autofocus element of an invalid UI context";
|
||||
break;
|
||||
case ContextErrorType::InternalError:
|
||||
error_message = "Internal error in UI context";
|
||||
break;
|
||||
default:
|
||||
error_message = "Unknown UI context error";
|
||||
break;
|
||||
}
|
||||
|
||||
// This assumes the error is coming from a mod, as it's unlikely that an end user will see a UI context error
|
||||
// in the base recomp.
|
||||
recompui::message_box((std::string{"Fatal error in mod - "} + error_message + ".").c_str());
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
recompui::ContextId create_context_impl(Rml::ElementDocument* document) {
|
||||
static Rml::ElementDocument dummy_document{""};
|
||||
bool add_to_dict = true;
|
||||
|
||||
if (document == nullptr) {
|
||||
document = &dummy_document;
|
||||
add_to_dict = false;
|
||||
}
|
||||
|
||||
recompui::ContextId ret;
|
||||
{
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
ret = { context_state.all_contexts.emplace(document).raw };
|
||||
|
||||
if (add_to_dict) {
|
||||
context_state.documents_to_contexts.emplace(document, ret);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void recompui::init_styling(const std::filesystem::path& rcss_file) {
|
||||
std::string style{};
|
||||
{
|
||||
std::ifstream style_stream{rcss_file};
|
||||
style_stream.seekg(0, std::ios::end);
|
||||
style.resize(style_stream.tellg());
|
||||
style_stream.seekg(0, std::ios::beg);
|
||||
|
||||
style_stream.read(style.data(), style.size());
|
||||
}
|
||||
std::unique_ptr<Rml::StreamMemory> rml_stream = std::make_unique<Rml::StreamMemory>(reinterpret_cast<Rml::byte*>(style.data()), style.size());
|
||||
rml_stream->SetSourceURL(rcss_file.filename().string());
|
||||
context_state.style_sheet = Rml::Factory::InstanceStyleSheetStream(rml_stream.get());
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::create_context(const std::filesystem::path& path) {
|
||||
ContextId new_context = create_context_impl(nullptr);
|
||||
|
||||
auto workingdir = std::filesystem::current_path();
|
||||
|
||||
new_context.open();
|
||||
Rml::ElementDocument* doc = recompui::load_document(path.string());
|
||||
opened_context->document = doc;
|
||||
opened_context->root_element.base = doc;
|
||||
new_context.close();
|
||||
|
||||
{
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
context_state.documents_to_contexts.emplace(doc, new_context);
|
||||
}
|
||||
|
||||
return new_context;
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::create_context(Rml::ElementDocument* document) {
|
||||
assert(document != nullptr);
|
||||
|
||||
return create_context_impl(document);
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::create_context() {
|
||||
Rml::ElementDocument* doc = create_empty_document();
|
||||
doc->SetStyleSheetContainer(context_state.style_sheet);
|
||||
ContextId ret = create_context_impl(doc);
|
||||
Element* root = ret.get_root_element();
|
||||
// Mark the root element as not being a shim, as that's only needed for elements that were parented to Rml ones manually.
|
||||
root->shim = false;
|
||||
|
||||
ret.open();
|
||||
root->set_width(100.0f, Unit::Percent);
|
||||
root->set_height(100.0f, Unit::Percent);
|
||||
root->set_display(Display::Flex);
|
||||
ret.close();
|
||||
|
||||
doc->Hide();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void recompui::destroy_context(ContextId id) {
|
||||
bool existed = false;
|
||||
|
||||
// TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping
|
||||
// from the slotmap?
|
||||
|
||||
// Check if the provided id exists.
|
||||
{
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
// Check if the target context is currently open.
|
||||
existed = context_state.all_contexts.has_key(context_slotmap::key{ id.slot_id });
|
||||
}
|
||||
|
||||
|
||||
// Raise an error if the context didn't exist.
|
||||
if (!existed) {
|
||||
context_error(id, ContextErrorType::DestroyInvalidContext);
|
||||
}
|
||||
|
||||
id.open();
|
||||
id.clear_children();
|
||||
id.close();
|
||||
|
||||
// Delete the provided id.
|
||||
{
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
context_state.all_contexts.erase(context_slotmap::key{ id.slot_id });
|
||||
}
|
||||
}
|
||||
|
||||
void recompui::destroy_all_contexts() {
|
||||
recompui::hide_all_contexts();
|
||||
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
// TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping
|
||||
// from the slotmap
|
||||
|
||||
std::vector<context_slotmap::key> keys{};
|
||||
for (const auto& [key, item] : context_state.all_contexts.items()) {
|
||||
keys.push_back(key);
|
||||
}
|
||||
|
||||
for (auto key : keys) {
|
||||
Context* ctx = context_state.all_contexts.get(key);
|
||||
|
||||
std::lock_guard context_lock{ ctx->context_lock };
|
||||
opened_context = ctx;
|
||||
opened_context_id = ContextId{ key };
|
||||
|
||||
opened_context_id.clear_children();
|
||||
|
||||
opened_context = nullptr;
|
||||
opened_context_id = ContextId::null();
|
||||
}
|
||||
|
||||
context_state.all_contexts.reset();
|
||||
context_state.documents_to_contexts.clear();
|
||||
}
|
||||
|
||||
void recompui::ContextId::open() {
|
||||
// Ensure no other context is opened by this thread already.
|
||||
if (opened_context_id != ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::OpenWithoutClose);
|
||||
}
|
||||
|
||||
// Get the context with this id.
|
||||
Context* ctx;
|
||||
{
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
// If the context was found, add it to the opened contexts.
|
||||
if (ctx != nullptr) {
|
||||
context_state.opened_contexts.emplace(*this);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the context exists.
|
||||
if (ctx == nullptr) {
|
||||
context_error(*this, ContextErrorType::OpenInvalidContext);
|
||||
}
|
||||
|
||||
// Take ownership of the target context.
|
||||
ctx->context_lock.lock();
|
||||
opened_context = ctx;
|
||||
opened_context_id = *this;
|
||||
}
|
||||
|
||||
bool recompui::ContextId::open_if_not_already() {
|
||||
if (opened_context_id == *this) {
|
||||
return false;
|
||||
}
|
||||
|
||||
open();
|
||||
return true;
|
||||
}
|
||||
|
||||
void recompui::ContextId::close() {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::CloseWithoutOpen);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::CloseWrongContext);
|
||||
}
|
||||
|
||||
// Release ownership of the target context.
|
||||
opened_context->context_lock.unlock();
|
||||
opened_context = nullptr;
|
||||
opened_context_id = ContextId::null();
|
||||
|
||||
// Remove this context from the opened contexts.
|
||||
{
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
context_state.opened_contexts.erase(*this);
|
||||
}
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::try_close_current_context() {
|
||||
if (opened_context_id != ContextId::null()) {
|
||||
ContextId prev_context = opened_context_id;
|
||||
opened_context_id.close();
|
||||
return prev_context;
|
||||
}
|
||||
return ContextId::null();
|
||||
}
|
||||
|
||||
void recompui::ContextId::process_updates() {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::InternalError);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::InternalError);
|
||||
}
|
||||
|
||||
// Move the current update set into a local variable. This clears the update set
|
||||
// and allows it to be used to queue updates from any element callbacks.
|
||||
std::unordered_set<ResourceId> to_update = std::move(opened_context->to_update);
|
||||
|
||||
Event update_event = Event::update_event();
|
||||
|
||||
for (auto cur_resource_id : to_update) {
|
||||
resource_slotmap::key cur_key{ cur_resource_id.slot_id };
|
||||
|
||||
// Ignore any resources that aren't elements.
|
||||
if (cur_key.get_tag() != static_cast<uint8_t>(SlotTag::Element)) {
|
||||
// Assert to catch errors of queueing other resource types for update.
|
||||
// This isn't an actual error, so there's no issue with continuing in release builds.
|
||||
assert(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the resource being updaten from the context.
|
||||
std::unique_ptr<Style>* cur_resource = opened_context->resources.get(cur_key);
|
||||
|
||||
// Make sure the resource exists before dispatching the event. It may have been deleted
|
||||
// after being queued for a update, so just continue to the next element if it doesn't exist.
|
||||
if (cur_resource == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
static_cast<Element*>(cur_resource->get())->handle_event(update_event);
|
||||
}
|
||||
}
|
||||
|
||||
bool recompui::ContextId::captures_input() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return ctx->captures_input;
|
||||
|
||||
}
|
||||
|
||||
bool recompui::ContextId::captures_mouse() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return ctx->captures_mouse;
|
||||
}
|
||||
|
||||
void recompui::ContextId::set_captures_input(bool captures_input) {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return;
|
||||
}
|
||||
ctx->captures_input = captures_input;
|
||||
}
|
||||
|
||||
void recompui::ContextId::set_captures_mouse(bool captures_mouse) {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
return;
|
||||
}
|
||||
ctx->captures_mouse = captures_mouse;
|
||||
}
|
||||
|
||||
recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&& resource) {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::AddResourceWithoutOpen);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::AddResourceToWrongContext);
|
||||
}
|
||||
|
||||
bool is_element = resource->is_element();
|
||||
Style* resource_ptr = resource.get();
|
||||
auto key = opened_context->resources.emplace(std::move(resource));
|
||||
|
||||
if (is_element) {
|
||||
Element* element_ptr = static_cast<Element*>(resource_ptr);
|
||||
element_ptr->set_id(std::string{element_ptr->get_type_name()} + "-" + std::to_string(key.raw));
|
||||
key.set_tag(static_cast<uint8_t>(SlotTag::Element));
|
||||
// Send one update to the element.
|
||||
opened_context->to_update.emplace(ResourceId{ key.raw });
|
||||
}
|
||||
else {
|
||||
key.set_tag(static_cast<uint8_t>(SlotTag::Style));
|
||||
}
|
||||
|
||||
resource_ptr->resource_id = { key.raw };
|
||||
return resource_ptr;
|
||||
}
|
||||
|
||||
void recompui::ContextId::add_loose_element(Element* element) {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::AddResourceWithoutOpen);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::AddResourceToWrongContext);
|
||||
}
|
||||
|
||||
opened_context->loose_elements.emplace_back(element);
|
||||
}
|
||||
|
||||
void recompui::ContextId::queue_element_update(ResourceId element) {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::UpdateElementWithoutContext);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::UpdateElementInWrongContext);
|
||||
}
|
||||
|
||||
// Check that the element that was specified is in the open context.
|
||||
auto* elementPtr = opened_context->resources.get(resource_slotmap::key{ element.slot_id });
|
||||
if (elementPtr == nullptr) {
|
||||
context_error(*this, ContextErrorType::UpdateElementInWrongContext);
|
||||
}
|
||||
|
||||
opened_context->to_update.emplace(element);
|
||||
}
|
||||
|
||||
recompui::Style* recompui::ContextId::create_style() {
|
||||
return add_resource_impl(std::make_unique<Style>());
|
||||
}
|
||||
|
||||
void recompui::ContextId::destroy_resource(Style* resource) {
|
||||
destroy_resource(resource->resource_id);
|
||||
}
|
||||
|
||||
void recompui::ContextId::destroy_resource(ResourceId resource) {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::DestroyResourceWithoutOpen);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::DestroyResourceInWrongContext);
|
||||
}
|
||||
|
||||
// Try to remove the resource from the current context.
|
||||
auto pop_result = opened_context->resources.pop(resource_slotmap::key{ resource.slot_id });
|
||||
if (!pop_result.has_value()) {
|
||||
context_error(*this, ContextErrorType::DestroyResourceNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
void recompui::ContextId::clear_children() {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(*this, ContextErrorType::DestroyResourceWithoutOpen);
|
||||
}
|
||||
|
||||
// Check that the context that was specified is the same one that's currently open.
|
||||
if (*this != opened_context_id) {
|
||||
context_error(*this, ContextErrorType::DestroyResourceInWrongContext);
|
||||
}
|
||||
|
||||
// Remove the root element's children.
|
||||
opened_context->root_element.clear_children();
|
||||
|
||||
// Remove any loose resources.
|
||||
for (Element* e : opened_context->loose_elements) {
|
||||
destroy_resource(e->resource_id);
|
||||
}
|
||||
opened_context->loose_elements.clear();
|
||||
}
|
||||
|
||||
Rml::ElementDocument* recompui::ContextId::get_document() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
context_error(*this, ContextErrorType::GetDocumentInvalidContext);
|
||||
}
|
||||
|
||||
return ctx->document;
|
||||
}
|
||||
|
||||
recompui::Element* recompui::ContextId::get_root_element() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
context_error(*this, ContextErrorType::GetDocumentInvalidContext);
|
||||
}
|
||||
|
||||
return &ctx->root_element;
|
||||
}
|
||||
|
||||
recompui::Element* recompui::ContextId::get_autofocus_element() {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
context_error(*this, ContextErrorType::GetAutofocusInvalidContext);
|
||||
}
|
||||
|
||||
return ctx->autofocus_element;
|
||||
}
|
||||
|
||||
void recompui::ContextId::set_autofocus_element(Element* element) {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
|
||||
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
|
||||
if (ctx == nullptr) {
|
||||
context_error(*this, ContextErrorType::SetAutofocusInvalidContext);
|
||||
}
|
||||
|
||||
ctx->autofocus_element = element;
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::get_current_context() {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == ContextId::null()) {
|
||||
context_error(ContextId::null(), ContextErrorType::GetContextWithoutOpen);
|
||||
}
|
||||
|
||||
return opened_context_id;
|
||||
}
|
||||
|
||||
recompui::Style* get_resource_from_current_context(resource_slotmap::key key) {
|
||||
// Ensure a context is currently opened by this thread.
|
||||
if (opened_context_id == recompui::ContextId::null()) {
|
||||
context_error(recompui::ContextId::null(), ContextErrorType::GetResourceWithoutOpen);
|
||||
}
|
||||
|
||||
auto* value = opened_context->resources.get(key);
|
||||
if (value == nullptr) {
|
||||
context_error(opened_context_id, ContextErrorType::GetResourceFailed);
|
||||
}
|
||||
|
||||
return value->get();
|
||||
}
|
||||
|
||||
const recompui::Style* recompui::ResourceId::operator*() const {
|
||||
resource_slotmap::key key{ slot_id };
|
||||
|
||||
return get_resource_from_current_context(key);
|
||||
}
|
||||
|
||||
recompui::Style* recompui::ResourceId::operator*() {
|
||||
resource_slotmap::key key{ slot_id };
|
||||
|
||||
return get_resource_from_current_context(key);
|
||||
}
|
||||
|
||||
const recompui::Element* recompui::ResourceId::as_element() const {
|
||||
resource_slotmap::key key{ slot_id };
|
||||
uint8_t tag = key.get_tag();
|
||||
|
||||
assert(tag == static_cast<uint8_t>(SlotTag::Element));
|
||||
|
||||
return static_cast<Element*>(get_resource_from_current_context(key));
|
||||
}
|
||||
|
||||
recompui::Element* recompui::ResourceId::as_element() {
|
||||
resource_slotmap::key key{ slot_id };
|
||||
uint8_t tag = key.get_tag();
|
||||
|
||||
assert(tag == static_cast<uint8_t>(SlotTag::Element));
|
||||
|
||||
return static_cast<Element*>(get_resource_from_current_context(key));
|
||||
}
|
||||
|
||||
recompui::ContextId recompui::get_context_from_document(Rml::ElementDocument* document) {
|
||||
std::lock_guard lock{ context_state.all_contexts_lock };
|
||||
auto find_it = context_state.documents_to_contexts.find(document);
|
||||
if (find_it == context_state.documents_to_contexts.end()) {
|
||||
return ContextId::null();
|
||||
}
|
||||
return find_it->second;
|
||||
}
|
69
src/ui/core/ui_context.h
Normal file
69
src/ui/core/ui_context.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
|
||||
#include "RmlUi/Core.h"
|
||||
|
||||
#include "ui_resource.h"
|
||||
|
||||
namespace recompui {
|
||||
class Style;
|
||||
class Element;
|
||||
class ContextId {
|
||||
Style* add_resource_impl(std::unique_ptr<Style>&& resource);
|
||||
public:
|
||||
uint32_t slot_id;
|
||||
auto operator<=>(const ContextId& rhs) const = default;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
T* create_element(Args... args) {
|
||||
return static_cast<T*>(add_resource_impl(std::make_unique<T>(std::forward<Args>(args)...)));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* create_element(T&& element) {
|
||||
return static_cast<T*>(add_resource_impl(std::make_unique<T>(std::move(element))));
|
||||
}
|
||||
|
||||
void add_loose_element(Element* element);
|
||||
void queue_element_update(ResourceId element);
|
||||
|
||||
Style* create_style();
|
||||
|
||||
void destroy_resource(Style* resource);
|
||||
void destroy_resource(ResourceId resource);
|
||||
void clear_children();
|
||||
|
||||
Rml::ElementDocument* get_document();
|
||||
Element* get_root_element();
|
||||
Element* get_autofocus_element();
|
||||
void set_autofocus_element(Element* element);
|
||||
|
||||
void open();
|
||||
bool open_if_not_already();
|
||||
void close();
|
||||
void process_updates();
|
||||
|
||||
static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; }
|
||||
|
||||
bool captures_input();
|
||||
bool captures_mouse();
|
||||
|
||||
void set_captures_input(bool captures_input);
|
||||
void set_captures_mouse(bool captures_input);
|
||||
};
|
||||
|
||||
ContextId create_context(const std::filesystem::path& path);
|
||||
ContextId create_context(Rml::ElementDocument* document);
|
||||
ContextId create_context();
|
||||
void destroy_context(ContextId id);
|
||||
ContextId get_current_context();
|
||||
ContextId get_context_from_document(Rml::ElementDocument* document);
|
||||
void destroy_all_contexts();
|
||||
|
||||
void register_ui_exports();
|
||||
} // namespace recompui
|
24
src/ui/core/ui_resource.h
Normal file
24
src/ui/core/ui_resource.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace recompui {
|
||||
class Style;
|
||||
class Element;
|
||||
struct ResourceId {
|
||||
uint32_t slot_id;
|
||||
|
||||
bool operator==(const ResourceId& rhs) const = default;
|
||||
|
||||
const Style* operator*() const;
|
||||
Style* operator*();
|
||||
|
||||
const Style* operator->() const { return *(*this); }
|
||||
Style* operator->() { return *(*this); }
|
||||
|
||||
const Element* as_element() const;
|
||||
Element* as_element();
|
||||
|
||||
static constexpr ResourceId null() { return ResourceId{ uint32_t(-1) }; }
|
||||
};
|
||||
} // namespace recompui
|
114
src/ui/elements/ui_button.cpp
Normal file
114
src/ui/elements/ui_button.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "ui_button.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "button", true) {
|
||||
this->style = style;
|
||||
|
||||
enable_focus();
|
||||
|
||||
set_text(text);
|
||||
set_display(Display::Block);
|
||||
set_padding(23.0f);
|
||||
set_border_width(1.1f);
|
||||
set_border_radius(12.0f);
|
||||
set_font_size(28.0f);
|
||||
set_letter_spacing(3.08f);
|
||||
set_line_height(28.0f);
|
||||
set_font_style(FontStyle::Normal);
|
||||
set_font_weight(700);
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_color(Color{ 204, 204, 204, 255 });
|
||||
set_tab_index(TabIndex::Auto);
|
||||
hover_style.set_color(Color{ 242, 242, 242, 255 });
|
||||
focus_style.set_color(Color{ 242, 242, 242, 255 });
|
||||
disabled_style.set_color(Color{ 204, 204, 204, 128 });
|
||||
hover_disabled_style.set_color(Color{ 242, 242, 242, 128 });
|
||||
|
||||
const uint8_t border_opacity = 204;
|
||||
const uint8_t background_opacity = 13;
|
||||
const uint8_t border_hover_opacity = 255;
|
||||
const uint8_t background_hover_opacity = 76;
|
||||
switch (style) {
|
||||
case ButtonStyle::Primary: {
|
||||
set_border_color({ 185, 125, 242, border_opacity });
|
||||
set_background_color({ 185, 125, 242, background_opacity });
|
||||
hover_style.set_border_color({ 185, 125, 242, border_hover_opacity });
|
||||
hover_style.set_background_color({ 185, 125, 242, background_hover_opacity });
|
||||
focus_style.set_border_color({ 185, 125, 242, border_hover_opacity });
|
||||
focus_style.set_background_color({ 185, 125, 242, background_hover_opacity });
|
||||
disabled_style.set_border_color({ 185, 125, 242, border_opacity / 4 });
|
||||
disabled_style.set_background_color({ 185, 125, 242, background_opacity / 4 });
|
||||
hover_disabled_style.set_border_color({ 185, 125, 242, border_hover_opacity / 4 });
|
||||
hover_disabled_style.set_background_color({ 185, 125, 242, background_hover_opacity / 4 });
|
||||
break;
|
||||
}
|
||||
case ButtonStyle::Secondary: {
|
||||
set_border_color({ 23, 214, 232, border_opacity });
|
||||
set_background_color({ 23, 214, 232, background_opacity });
|
||||
hover_style.set_border_color({ 23, 214, 232, border_hover_opacity });
|
||||
hover_style.set_background_color({ 23, 214, 232, background_hover_opacity });
|
||||
focus_style.set_border_color({ 23, 214, 232, border_hover_opacity });
|
||||
focus_style.set_background_color({ 23, 214, 232, background_hover_opacity });
|
||||
disabled_style.set_border_color({ 23, 214, 232, border_opacity / 4 });
|
||||
disabled_style.set_background_color({ 23, 214, 232, background_opacity / 4 });
|
||||
hover_disabled_style.set_border_color({ 23, 214, 232, border_hover_opacity / 4 });
|
||||
hover_disabled_style.set_background_color({ 23, 214, 232, background_hover_opacity / 4 });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assert(false && "Unknown button style.");
|
||||
break;
|
||||
}
|
||||
|
||||
add_style(&hover_style, hover_state);
|
||||
add_style(&focus_style, focus_state);
|
||||
add_style(&disabled_style, disabled_state);
|
||||
add_style(&hover_disabled_style, { hover_state, disabled_state });
|
||||
|
||||
// transition: color 0.05s linear-in-out, background-color 0.05s linear-in-out;
|
||||
}
|
||||
|
||||
void Button::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Click:
|
||||
if (is_enabled()) {
|
||||
for (const auto &function : pressed_callbacks) {
|
||||
function();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Hover:
|
||||
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
|
||||
break;
|
||||
case EventType::Enable:
|
||||
{
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
set_style_enabled(disabled_state, !enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Focus:
|
||||
set_style_enabled(focus_state, std::get<EventFocus>(e.variant).active);
|
||||
break;
|
||||
case EventType::Update:
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown event type.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Button::add_pressed_callback(std::function<void()> callback) {
|
||||
pressed_callbacks.emplace_back(callback);
|
||||
}
|
||||
};
|
33
src/ui/elements/ui_button.h
Normal file
33
src/ui/elements/ui_button.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
enum class ButtonStyle {
|
||||
Primary,
|
||||
Secondary
|
||||
};
|
||||
|
||||
class Button : public Element {
|
||||
protected:
|
||||
ButtonStyle style = ButtonStyle::Primary;
|
||||
Style hover_style;
|
||||
Style focus_style;
|
||||
Style disabled_style;
|
||||
Style hover_disabled_style;
|
||||
std::list<std::function<void()>> pressed_callbacks;
|
||||
|
||||
// Element overrides.
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Button"; }
|
||||
public:
|
||||
Button(Element *parent, const std::string &text, ButtonStyle style);
|
||||
void add_pressed_callback(std::function<void()> callback);
|
||||
Style* get_hover_style() { return &hover_style; }
|
||||
Style* get_focus_style() { return &focus_style; }
|
||||
Style* get_disabled_style() { return &disabled_style; }
|
||||
Style* get_hover_disabled_style() { return &hover_disabled_style; }
|
||||
};
|
||||
|
||||
} // namespace recompui
|
62
src/ui/elements/ui_clickable.cpp
Normal file
62
src/ui/elements/ui_clickable.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "ui_clickable.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
if (draggable) {
|
||||
set_drag(Drag::Drag);
|
||||
}
|
||||
}
|
||||
|
||||
void Clickable::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Click: {
|
||||
if (is_enabled()) {
|
||||
const EventClick &click = std::get<EventClick>(e.variant);
|
||||
for (const auto &function : pressed_callbacks) {
|
||||
function(click.x, click.y);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
case EventType::Hover:
|
||||
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
|
||||
break;
|
||||
case EventType::Enable:
|
||||
{
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
set_style_enabled(disabled_state, !enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Drag: {
|
||||
if (is_enabled()) {
|
||||
const EventDrag &drag = std::get<EventDrag>(e.variant);
|
||||
for (const auto &function : dragged_callbacks) {
|
||||
function(drag.x, drag.y, drag.phase);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Clickable::add_pressed_callback(std::function<void(float, float)> callback) {
|
||||
pressed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
void Clickable::add_dragged_callback(std::function<void(float, float, DragPhase)> callback) {
|
||||
dragged_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
};
|
21
src/ui/elements/ui_clickable.h
Normal file
21
src/ui/elements/ui_clickable.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class Clickable : public Element {
|
||||
protected:
|
||||
std::vector<std::function<void(float, float)>> pressed_callbacks;
|
||||
std::vector<std::function<void(float, float, DragPhase)>> dragged_callbacks;
|
||||
|
||||
// Element overrides.
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Clickable"; }
|
||||
public:
|
||||
Clickable(Element *parent, bool draggable = false);
|
||||
void add_pressed_callback(std::function<void(float, float)> callback);
|
||||
void add_dragged_callback(std::function<void(float, float, DragPhase)> callback);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
13
src/ui/elements/ui_container.cpp
Normal file
13
src/ui/elements/ui_container.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "ui_container.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled) : Element(parent, events_enabled) {
|
||||
set_display(Display::Flex);
|
||||
set_flex_direction(direction);
|
||||
set_justify_content(justify_content);
|
||||
}
|
||||
|
||||
};
|
14
src/ui/elements/ui_container.h
Normal file
14
src/ui/elements/ui_container.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class Container : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "Container"; }
|
||||
public:
|
||||
Container(Element* parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled = 0);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
478
src/ui/elements/ui_element.cpp
Normal file
478
src/ui/elements/ui_element.cpp
Normal file
@@ -0,0 +1,478 @@
|
||||
#include "RmlUi/Core/StringUtilities.h"
|
||||
|
||||
#include "overloaded.h"
|
||||
#include "recomp_ui.h"
|
||||
#include "ui_element.h"
|
||||
#include "../core/ui_context.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Element::Element(Rml::Element *base) {
|
||||
assert(base != nullptr);
|
||||
|
||||
this->base = base;
|
||||
this->base_owning = {};
|
||||
this->shim = true;
|
||||
}
|
||||
|
||||
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class, bool can_set_text) : can_set_text(can_set_text) {
|
||||
ContextId context = get_current_context();
|
||||
base_owning = context.get_document()->CreateElement(base_class);
|
||||
|
||||
if (parent != nullptr) {
|
||||
base = parent->base->AppendChild(std::move(base_owning));
|
||||
parent->add_child(this);
|
||||
}
|
||||
else {
|
||||
base = base_owning.get();
|
||||
}
|
||||
|
||||
set_display(Display::Block);
|
||||
set_property(Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox);
|
||||
|
||||
register_event_listeners(events_enabled);
|
||||
}
|
||||
|
||||
Element::~Element() {
|
||||
if (!shim) {
|
||||
clear_children();
|
||||
if (!base_owning) {
|
||||
base->GetParentNode()->RemoveChild(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Element::add_child(Element *child) {
|
||||
assert(child != nullptr);
|
||||
if (can_set_text) {
|
||||
assert(false && "Elements with settable text cannot have children");
|
||||
return;
|
||||
}
|
||||
|
||||
children.emplace_back(child);
|
||||
|
||||
if (shim) {
|
||||
ContextId context = get_current_context();
|
||||
context.add_loose_element(child);
|
||||
}
|
||||
}
|
||||
|
||||
void Element::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
|
||||
assert(base != nullptr);
|
||||
|
||||
base->SetProperty(property_id, property);
|
||||
Style::set_property(property_id, property);
|
||||
}
|
||||
|
||||
void Element::register_event_listeners(uint32_t events_enabled) {
|
||||
assert(base != nullptr);
|
||||
|
||||
this->events_enabled = events_enabled;
|
||||
|
||||
if (events_enabled & Events(EventType::Click)) {
|
||||
base->AddEventListener(Rml::EventId::Click, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::Focus)) {
|
||||
base->AddEventListener(Rml::EventId::Focus, this);
|
||||
base->AddEventListener(Rml::EventId::Blur, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::Hover)) {
|
||||
base->AddEventListener(Rml::EventId::Mouseover, this);
|
||||
base->AddEventListener(Rml::EventId::Mouseout, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::Drag)) {
|
||||
base->AddEventListener(Rml::EventId::Drag, this);
|
||||
base->AddEventListener(Rml::EventId::Dragstart, this);
|
||||
base->AddEventListener(Rml::EventId::Dragend, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::Text)) {
|
||||
base->AddEventListener(Rml::EventId::Change, this);
|
||||
}
|
||||
|
||||
if (events_enabled & Events(EventType::Navigate)) {
|
||||
base->AddEventListener(Rml::EventId::Keydown, this);
|
||||
}
|
||||
}
|
||||
|
||||
void Element::apply_style(Style *style) {
|
||||
for (auto it : style->property_map) {
|
||||
// Skip redundant SetProperty calls to prevent dirtying unnecessary state.
|
||||
// This avoids expensive layout operations when a simple color-only style is applied.
|
||||
const Rml::Property* cur_value = base->GetLocalProperty(it.first);
|
||||
if (*cur_value != it.second) {
|
||||
base->SetProperty(it.first, it.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Element::apply_styles() {
|
||||
apply_style(this);
|
||||
|
||||
for (size_t i = 0; i < styles_counter.size(); i++) {
|
||||
if (styles_counter[i] == 0) {
|
||||
apply_style(styles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Element::propagate_disabled(bool disabled) {
|
||||
disabled_from_parent = disabled;
|
||||
|
||||
bool attribute_state = disabled_from_parent || !enabled;
|
||||
if (disabled_attribute != attribute_state) {
|
||||
disabled_attribute = attribute_state;
|
||||
base->SetAttribute("disabled", attribute_state);
|
||||
|
||||
if (events_enabled & Events(EventType::Enable)) {
|
||||
handle_event(Event::enable_event(!attribute_state));
|
||||
}
|
||||
|
||||
for (auto &child : children) {
|
||||
child->propagate_disabled(attribute_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Element::handle_event(const Event& event) {
|
||||
for (const auto& callback : callbacks) {
|
||||
recompui::queue_ui_callback(resource_id, event, callback);
|
||||
}
|
||||
|
||||
process_event(event);
|
||||
}
|
||||
|
||||
void Element::set_id(const std::string& new_id) {
|
||||
id = new_id;
|
||||
base->SetId(new_id);
|
||||
}
|
||||
|
||||
void Element::ProcessEvent(Rml::Event &event) {
|
||||
ContextId prev_context = recompui::try_close_current_context();
|
||||
ContextId context = ContextId::null();
|
||||
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
|
||||
if (doc != nullptr) {
|
||||
context = get_context_from_document(doc);
|
||||
}
|
||||
|
||||
bool did_open = false;
|
||||
|
||||
// TODO disallow null contexts once the entire UI system has been migrated.
|
||||
if (context != ContextId::null()) {
|
||||
did_open = context.open_if_not_already();
|
||||
}
|
||||
|
||||
// Events that are processed during any phase.
|
||||
switch (event.GetId()) {
|
||||
case Rml::EventId::Click:
|
||||
handle_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
|
||||
break;
|
||||
case Rml::EventId::Keydown:
|
||||
switch ((Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0)) {
|
||||
case Rml::Input::KeyIdentifier::KI_LEFT:
|
||||
handle_event(Event::navigate_event(NavDirection::Left));
|
||||
break;
|
||||
case Rml::Input::KeyIdentifier::KI_UP:
|
||||
handle_event(Event::navigate_event(NavDirection::Up));
|
||||
break;
|
||||
case Rml::Input::KeyIdentifier::KI_RIGHT:
|
||||
handle_event(Event::navigate_event(NavDirection::Right));
|
||||
break;
|
||||
case Rml::Input::KeyIdentifier::KI_DOWN:
|
||||
handle_event(Event::navigate_event(NavDirection::Down));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Rml::EventId::Drag:
|
||||
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Events that are only processed during the Target phase.
|
||||
if (event.GetPhase() == Rml::EventPhase::Target) {
|
||||
switch (event.GetId()) {
|
||||
case Rml::EventId::Mouseover:
|
||||
handle_event(Event::hover_event(true));
|
||||
break;
|
||||
case Rml::EventId::Mouseout:
|
||||
handle_event(Event::hover_event(false));
|
||||
break;
|
||||
case Rml::EventId::Focus:
|
||||
handle_event(Event::focus_event(true));
|
||||
break;
|
||||
case Rml::EventId::Blur:
|
||||
handle_event(Event::focus_event(false));
|
||||
break;
|
||||
case Rml::EventId::Dragstart:
|
||||
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
|
||||
break;
|
||||
case Rml::EventId::Dragend:
|
||||
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End));
|
||||
break;
|
||||
case Rml::EventId::Change: {
|
||||
if (events_enabled & Events(EventType::Text)) {
|
||||
Rml::Variant *value_variant = base->GetAttribute("value");
|
||||
if (value_variant != nullptr) {
|
||||
handle_event(Event::text_event(value_variant->Get<std::string>()));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (context != ContextId::null() && did_open) {
|
||||
context.close();
|
||||
}
|
||||
|
||||
if (prev_context != ContextId::null()) {
|
||||
prev_context.open();
|
||||
}
|
||||
}
|
||||
|
||||
void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) {
|
||||
base->SetAttribute(attribute_key, attribute_value);
|
||||
}
|
||||
|
||||
void Element::process_event(const Event &) {
|
||||
// Does nothing by default.
|
||||
}
|
||||
|
||||
void Element::enable_focus() {
|
||||
set_tab_index_auto();
|
||||
set_focusable(true);
|
||||
set_nav_auto(NavDirection::Up);
|
||||
set_nav_auto(NavDirection::Down);
|
||||
set_nav_auto(NavDirection::Left);
|
||||
set_nav_auto(NavDirection::Right);
|
||||
}
|
||||
|
||||
void Element::clear_children() {
|
||||
if (children.empty()) {
|
||||
return;
|
||||
}
|
||||
ContextId context = get_current_context();
|
||||
|
||||
// Remove the children from the context.
|
||||
for (Element* child : children) {
|
||||
context.destroy_resource(child);
|
||||
}
|
||||
|
||||
// Clear the child list.
|
||||
children.clear();
|
||||
}
|
||||
|
||||
bool Element::remove_child(ResourceId child) {
|
||||
bool found = false;
|
||||
|
||||
ContextId context = get_current_context();
|
||||
|
||||
for (auto it = children.begin(); it != children.end(); ++it) {
|
||||
Element* cur_child = *it;
|
||||
if (cur_child->get_resource_id() == child) {
|
||||
children.erase(it);
|
||||
context.destroy_resource(cur_child);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
void Element::add_style(Style *style, const std::string_view style_name) {
|
||||
add_style(style, { style_name });
|
||||
}
|
||||
|
||||
void Element::add_style(Style *style, const std::initializer_list<std::string_view> &style_names) {
|
||||
for (const std::string_view &style_name : style_names) {
|
||||
style_name_index_map.emplace(style_name, styles.size());
|
||||
}
|
||||
|
||||
styles.emplace_back(style);
|
||||
|
||||
uint32_t initial_style_counter = style_names.size();
|
||||
for (const std::string_view &style_name : style_names) {
|
||||
if (style_active_set.find(style_name) != style_active_set.end()) {
|
||||
initial_style_counter--;
|
||||
}
|
||||
}
|
||||
|
||||
styles_counter.push_back(initial_style_counter);
|
||||
}
|
||||
|
||||
void Element::set_enabled(bool enabled) {
|
||||
this->enabled = enabled;
|
||||
|
||||
propagate_disabled(disabled_from_parent);
|
||||
}
|
||||
|
||||
bool Element::is_enabled() const {
|
||||
return enabled && !disabled_from_parent;
|
||||
}
|
||||
|
||||
// Adapted from RmlUi's `EncodeRml`.
|
||||
std::string escape_rml(std::string_view string)
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(string.size());
|
||||
for (char c : string)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '<': result += "<"; 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) {
|
||||
if (can_set_text) {
|
||||
// Escape the string into Rml to prevent element injection.
|
||||
base->SetInnerRML(escape_rml(text));
|
||||
}
|
||||
else {
|
||||
assert(false && "Attempted to set text of an element that cannot have its text set.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string Element::get_input_text() {
|
||||
return base->GetAttribute("value", std::string{});
|
||||
}
|
||||
|
||||
void Element::set_input_text(std::string_view val) {
|
||||
base->SetAttribute("value", std::string{ val });
|
||||
}
|
||||
|
||||
void Element::set_src(std::string_view src) {
|
||||
base->SetAttribute("src", std::string(src));
|
||||
}
|
||||
|
||||
void Element::set_style_enabled(std::string_view style_name, bool enable) {
|
||||
if (enable && style_active_set.find(style_name) == style_active_set.end()) {
|
||||
// Style was disabled and will be enabled.
|
||||
style_active_set.emplace(style_name);
|
||||
|
||||
}
|
||||
else if (!enable && style_active_set.find(style_name) != style_active_set.end()) {
|
||||
// Style was enabled and will be disabled.
|
||||
style_active_set.erase(style_name);
|
||||
}
|
||||
else {
|
||||
// Do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
auto range = style_name_index_map.equal_range(style_name);
|
||||
for (auto it = range.first; it != range.second; it++) {
|
||||
if (enable) {
|
||||
styles_counter[it->second]--;
|
||||
}
|
||||
else {
|
||||
styles_counter[it->second]++;
|
||||
}
|
||||
}
|
||||
|
||||
apply_styles();
|
||||
}
|
||||
|
||||
bool Element::is_style_enabled(std::string_view style_name) {
|
||||
return style_active_set.contains(style_name);
|
||||
}
|
||||
|
||||
float Element::get_absolute_left() {
|
||||
return base->GetAbsoluteLeft();
|
||||
}
|
||||
|
||||
float Element::get_absolute_top() {
|
||||
return base->GetAbsoluteTop();
|
||||
}
|
||||
|
||||
float Element::get_client_left() {
|
||||
return base->GetClientLeft();
|
||||
}
|
||||
|
||||
float Element::get_client_top() {
|
||||
return base->GetClientTop();
|
||||
}
|
||||
|
||||
float Element::get_client_width() {
|
||||
return base->GetClientWidth();
|
||||
}
|
||||
|
||||
float Element::get_client_height() {
|
||||
return base->GetClientHeight();
|
||||
}
|
||||
|
||||
uint32_t Element::get_input_value_u32() {
|
||||
ElementValue value = get_element_value();
|
||||
|
||||
return std::visit(overloaded {
|
||||
[](double d) { return (uint32_t)d; },
|
||||
[](float f) { return (uint32_t)f; },
|
||||
[](uint32_t u) { return u; },
|
||||
[](std::monostate) { return 0U; }
|
||||
}, value);
|
||||
}
|
||||
|
||||
float Element::get_input_value_float() {
|
||||
ElementValue value = get_element_value();
|
||||
|
||||
return std::visit(overloaded {
|
||||
[](double d) { return (float)d; },
|
||||
[](float f) { return f; },
|
||||
[](uint32_t u) { return (float)u; },
|
||||
[](std::monostate) { return 0.0f; }
|
||||
}, value);
|
||||
}
|
||||
|
||||
double Element::get_input_value_double() {
|
||||
ElementValue value = get_element_value();
|
||||
|
||||
return std::visit(overloaded {
|
||||
[](double d) { return d; },
|
||||
[](float f) { return (double)f; },
|
||||
[](uint32_t u) { return (double)u; },
|
||||
[](std::monostate) { return 0.0; }
|
||||
}, value);
|
||||
}
|
||||
|
||||
void Element::focus() {
|
||||
base->Focus();
|
||||
}
|
||||
|
||||
void Element::blur() {
|
||||
base->Blur();
|
||||
}
|
||||
|
||||
void Element::queue_update() {
|
||||
ContextId cur_context = get_current_context();
|
||||
|
||||
// TODO disallow null contexts once the entire UI system has been migrated.
|
||||
if (cur_context == ContextId::null()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cur_context.queue_element_update(resource_id);
|
||||
}
|
||||
|
||||
void Element::register_callback(ContextId context, PTR(void) callback, PTR(void) userdata) {
|
||||
callbacks.emplace_back(UICallback{.context = context, .callback = callback, .userdata = userdata});
|
||||
}
|
||||
|
||||
}
|
106
src/ui/elements/ui_element.h
Normal file
106
src/ui/elements/ui_element.h
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_style.h"
|
||||
#include "../core/ui_context.h"
|
||||
|
||||
#include "recomp.h"
|
||||
#include <ultramodern/ultra64.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
|
||||
namespace recompui {
|
||||
struct UICallback {
|
||||
ContextId context;
|
||||
PTR(void) callback;
|
||||
PTR(void) userdata;
|
||||
};
|
||||
|
||||
using ElementValue = std::variant<uint32_t, float, double, std::monostate>;
|
||||
|
||||
class ContextId;
|
||||
class Element : public Style, public Rml::EventListener {
|
||||
friend ContextId create_context(const std::filesystem::path& path);
|
||||
friend ContextId create_context();
|
||||
friend class ContextId; // To allow ContextId to call the handle_event method directly.
|
||||
private:
|
||||
Rml::Element *base = nullptr;
|
||||
Rml::ElementPtr base_owning = {};
|
||||
uint32_t events_enabled = 0;
|
||||
std::vector<Style *> styles;
|
||||
std::vector<uint32_t> styles_counter;
|
||||
std::unordered_set<std::string_view> style_active_set;
|
||||
std::unordered_multimap<std::string_view, uint32_t> style_name_index_map;
|
||||
std::vector<UICallback> callbacks;
|
||||
std::vector<Element *> children;
|
||||
std::string id;
|
||||
bool shim = false;
|
||||
bool enabled = true;
|
||||
bool disabled_attribute = false;
|
||||
bool disabled_from_parent = false;
|
||||
bool can_set_text = false;
|
||||
|
||||
void add_child(Element *child);
|
||||
void register_event_listeners(uint32_t events_enabled);
|
||||
void apply_style(Style *style);
|
||||
void propagate_disabled(bool disabled);
|
||||
void handle_event(const Event &e);
|
||||
void set_id(const std::string& new_id);
|
||||
|
||||
// Style overrides.
|
||||
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override;
|
||||
|
||||
// Rml::EventListener overrides.
|
||||
void ProcessEvent(Rml::Event &event) override final;
|
||||
protected:
|
||||
// Use of this method in inherited classes is discouraged unless it's necessary.
|
||||
void set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value);
|
||||
virtual void process_event(const Event &e);
|
||||
virtual ElementValue get_element_value() { return std::monostate{}; }
|
||||
virtual void set_input_value(const ElementValue&) {}
|
||||
virtual std::string_view get_type_name() { return "Element"; }
|
||||
public:
|
||||
// Used for backwards compatibility with legacy UI elements.
|
||||
Element(Rml::Element *base);
|
||||
|
||||
// Used to actually construct elements.
|
||||
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div", bool can_set_text = false);
|
||||
virtual ~Element();
|
||||
void clear_children();
|
||||
bool remove_child(ResourceId child);
|
||||
bool remove_child(Element *child) { return remove_child(child->get_resource_id()); }
|
||||
void add_style(Style *style, std::string_view style_name);
|
||||
void add_style(Style *style, const std::initializer_list<std::string_view> &style_names);
|
||||
void set_enabled(bool enabled);
|
||||
bool is_enabled() const;
|
||||
void set_text(std::string_view text);
|
||||
std::string get_input_text();
|
||||
void set_input_text(std::string_view text);
|
||||
void set_src(std::string_view src);
|
||||
void set_style_enabled(std::string_view style_name, bool enabled);
|
||||
bool is_style_enabled(std::string_view style_name);
|
||||
void apply_styles();
|
||||
bool is_element() override { return true; }
|
||||
float get_absolute_left();
|
||||
float get_absolute_top();
|
||||
float get_client_left();
|
||||
float get_client_top();
|
||||
float get_client_width();
|
||||
float get_client_height();
|
||||
void enable_focus();
|
||||
void focus();
|
||||
void blur();
|
||||
void queue_update();
|
||||
void register_callback(ContextId context, PTR(void) callback, PTR(void) userdata);
|
||||
uint32_t get_input_value_u32();
|
||||
float get_input_value_float();
|
||||
double get_input_value_double();
|
||||
void set_input_value_u32(uint32_t val) { set_input_value(val); }
|
||||
void set_input_value_float(float val) { set_input_value(val); }
|
||||
void set_input_value_double(double val) { set_input_value(val); }
|
||||
const std::string& get_id() { return id; }
|
||||
};
|
||||
|
||||
void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback);
|
||||
|
||||
} // namespace recompui
|
11
src/ui/elements/ui_image.cpp
Normal file
11
src/ui/elements/ui_image.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "ui_image.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Image::Image(Element *parent, std::string_view src) : Element(parent, 0, "img") {
|
||||
set_src(src);
|
||||
}
|
||||
|
||||
};
|
14
src/ui/elements/ui_image.h
Normal file
14
src/ui/elements/ui_image.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class Image : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ImageView"; }
|
||||
public:
|
||||
Image(Element *parent, std::string_view src);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
43
src/ui/elements/ui_label.cpp
Normal file
43
src/ui/elements/ui_label.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "ui_label.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Label::Label(Element *parent, LabelStyle label_style) : Element(parent, 0U, "div", true) {
|
||||
switch (label_style) {
|
||||
case LabelStyle::Annotation:
|
||||
set_color(Color{ 185, 125, 242, 255 });
|
||||
set_font_size(18.0f);
|
||||
set_letter_spacing(2.52f);
|
||||
set_line_height(18.0f);
|
||||
set_font_weight(400);
|
||||
break;
|
||||
case LabelStyle::Small:
|
||||
set_font_size(20.0f);
|
||||
set_letter_spacing(0.0f);
|
||||
set_line_height(20.0f);
|
||||
set_font_weight(400);
|
||||
break;
|
||||
case LabelStyle::Normal:
|
||||
set_font_size(28.0f);
|
||||
set_letter_spacing(3.08f);
|
||||
set_line_height(28.0f);
|
||||
set_font_weight(700);
|
||||
break;
|
||||
case LabelStyle::Large:
|
||||
set_font_size(36.0f);
|
||||
set_letter_spacing(2.52f);
|
||||
set_line_height(36.0f);
|
||||
set_font_weight(700);
|
||||
break;
|
||||
}
|
||||
|
||||
set_font_style(FontStyle::Normal);
|
||||
}
|
||||
|
||||
Label::Label(Element *parent, const std::string &text, LabelStyle label_style) : Label(parent, label_style) {
|
||||
set_text(text);
|
||||
}
|
||||
|
||||
};
|
22
src/ui/elements/ui_label.h
Normal file
22
src/ui/elements/ui_label.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
enum class LabelStyle {
|
||||
Annotation,
|
||||
Small,
|
||||
Normal,
|
||||
Large
|
||||
};
|
||||
|
||||
class Label : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "Label"; }
|
||||
public:
|
||||
Label(Element *parent, LabelStyle label_style);
|
||||
Label(Element *parent, const std::string &text, LabelStyle label_style);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
240
src/ui/elements/ui_radio.cpp
Normal file
240
src/ui/elements/ui_radio.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
#include "overloaded.h"
|
||||
#include "ui_radio.h"
|
||||
#include "../ui_utils.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
// RadioOption
|
||||
|
||||
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable, EventType::Update), "label", true) {
|
||||
this->index = index;
|
||||
|
||||
enable_focus();
|
||||
set_text(name);
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_font_size(20.0f);
|
||||
set_letter_spacing(2.8f);
|
||||
set_line_height(20.0f);
|
||||
set_font_weight(400);
|
||||
set_font_style(FontStyle::Normal);
|
||||
set_border_color(Color{ 242, 242, 242, 0 });
|
||||
set_border_bottom_width(1.0f);
|
||||
set_color(Color{ 255, 255, 255, 153 });
|
||||
set_padding_bottom(8.0f);
|
||||
set_text_transform(TextTransform::Uppercase);
|
||||
set_height_auto();
|
||||
hover_style.set_color(Color{ 255, 255, 255, 204 });
|
||||
checked_style.set_color(Color{ 255, 255, 255, 255 });
|
||||
checked_style.set_border_color(Color{ 242, 242, 242, 255 });
|
||||
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
|
||||
|
||||
add_style(&hover_style, { hover_state });
|
||||
add_style(&checked_style, { checked_state });
|
||||
add_style(&pulsing_style, { focus_state });
|
||||
}
|
||||
|
||||
void RadioOption::set_pressed_callback(std::function<void(uint32_t)> callback) {
|
||||
pressed_callback = callback;
|
||||
}
|
||||
|
||||
void RadioOption::set_selected_state(bool enable) {
|
||||
set_style_enabled(checked_state, enable);
|
||||
}
|
||||
|
||||
void RadioOption::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Click:
|
||||
pressed_callback(index);
|
||||
break;
|
||||
case EventType::Hover:
|
||||
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
|
||||
break;
|
||||
case EventType::Enable:
|
||||
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
|
||||
break;
|
||||
case EventType::Focus:
|
||||
{
|
||||
bool active = std::get<EventFocus>(e.variant).active;
|
||||
set_style_enabled(focus_state, active);
|
||||
if (active) {
|
||||
queue_update();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Update:
|
||||
if (is_style_enabled(focus_state)) {
|
||||
pulsing_style.set_color(recompui::get_pulse_color(750));
|
||||
apply_styles();
|
||||
queue_update();
|
||||
}
|
||||
if (focus_queued) {
|
||||
focus_queued = false;
|
||||
focus();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Radio
|
||||
|
||||
void Radio::set_index_internal(uint32_t index, bool setup, bool trigger_callbacks) {
|
||||
if (this->index != index || setup) {
|
||||
options[this->index]->set_selected_state(false);
|
||||
this->index = index;
|
||||
options[index]->set_selected_state(true);
|
||||
|
||||
if (trigger_callbacks) {
|
||||
for (const auto &function : index_changed_callbacks) {
|
||||
function(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::option_selected(uint32_t index) {
|
||||
set_index_internal(index, false, true);
|
||||
}
|
||||
|
||||
void Radio::set_input_value(const ElementValue& val) {
|
||||
std::visit(overloaded {
|
||||
[this](uint32_t u) { set_index(u); },
|
||||
[this](float f) { set_index(f); },
|
||||
[this](double d) { set_index(d); },
|
||||
[](std::monostate) {}
|
||||
}, val);
|
||||
}
|
||||
|
||||
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus)) {
|
||||
set_gap(24.0f);
|
||||
set_align_items(AlignItems::FlexStart);
|
||||
enable_focus();
|
||||
}
|
||||
|
||||
void Radio::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Focus:
|
||||
if (!options.empty()) {
|
||||
if (std::get<EventFocus>(e.variant).active) {
|
||||
blur();
|
||||
options[index]->queue_focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Radio::~Radio() {
|
||||
|
||||
}
|
||||
|
||||
void Radio::add_option(std::string_view name) {
|
||||
RadioOption *option = get_current_context().create_element<RadioOption>(this, name, uint32_t(options.size()));
|
||||
option->set_pressed_callback([this](uint32_t index){ option_selected(index); });
|
||||
options.emplace_back(option);
|
||||
|
||||
// The first option was added, select it.
|
||||
if (options.size() == 1) {
|
||||
set_index_internal(0, true, false);
|
||||
}
|
||||
// At least one other option already existed, so set up navigation.
|
||||
else {
|
||||
options[options.size() - 2]->set_nav(NavDirection::Right, options[options.size() - 1]);
|
||||
options[options.size() - 1]->set_nav(NavDirection::Left, options[options.size() - 2]);
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_index(uint32_t index) {
|
||||
set_index_internal(index, false, false);
|
||||
}
|
||||
|
||||
uint32_t Radio::get_index() const {
|
||||
return index;
|
||||
}
|
||||
|
||||
void Radio::add_index_changed_callback(std::function<void(uint32_t)> callback) {
|
||||
index_changed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
void Radio::set_nav_auto(NavDirection dir) {
|
||||
Element::set_nav_auto(dir);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav_auto(dir);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav_auto(dir);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav_auto(dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_nav_none(NavDirection dir) {
|
||||
Element::set_nav_none(dir);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav_none(dir);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav_none(dir);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav_none(dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_nav(NavDirection dir, Element* element) {
|
||||
Element::set_nav(dir, element);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav(dir, element);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav(dir, element);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav(dir, element);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::set_nav_manual(NavDirection dir, const std::string& target) {
|
||||
Element::set_nav_manual(dir, target);
|
||||
if (!options.empty()) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
case NavDirection::Down:
|
||||
for (Element* e : options) {
|
||||
e->set_nav_manual(dir, target);
|
||||
}
|
||||
break;
|
||||
case NavDirection::Left:
|
||||
options.front()->set_nav_manual(dir, target);
|
||||
break;
|
||||
case NavDirection::Right:
|
||||
options.back()->set_nav_manual(dir, target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
54
src/ui/elements/ui_radio.h
Normal file
54
src/ui/elements/ui_radio.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_container.h"
|
||||
#include "ui_label.h"
|
||||
|
||||
namespace recompui {
|
||||
class RadioOption : public Element {
|
||||
private:
|
||||
Style hover_style;
|
||||
Style checked_style;
|
||||
Style pulsing_style;
|
||||
std::function<void(uint32_t)> pressed_callback = nullptr;
|
||||
uint32_t index = 0;
|
||||
bool focus_queued = false;
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "LabelRadioOption"; }
|
||||
public:
|
||||
RadioOption(Element *parent, std::string_view name, uint32_t index);
|
||||
void set_pressed_callback(std::function<void(uint32_t)> callback);
|
||||
void set_selected_state(bool enable);
|
||||
void queue_focus() { focus_queued = true; queue_update(); }
|
||||
};
|
||||
|
||||
class Radio : public Container {
|
||||
private:
|
||||
std::vector<RadioOption *> options;
|
||||
uint32_t index = 0;
|
||||
std::vector<std::function<void(uint32_t)>> index_changed_callbacks;
|
||||
|
||||
void set_index_internal(uint32_t index, bool setup, bool trigger_callbacks);
|
||||
void option_selected(uint32_t index);
|
||||
void set_input_value(const ElementValue& val) override;
|
||||
ElementValue get_element_value() override { return get_index(); }
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "LabelRadio"; }
|
||||
public:
|
||||
Radio(Element *parent);
|
||||
virtual ~Radio();
|
||||
void add_option(std::string_view name);
|
||||
void set_index(uint32_t index);
|
||||
uint32_t get_index() const;
|
||||
void add_index_changed_callback(std::function<void(uint32_t)> callback);
|
||||
size_t num_options() const { return options.size(); }
|
||||
RadioOption* get_option_element(size_t option_index) { return options[option_index]; }
|
||||
RadioOption* get_current_option_element() { return options.empty() ? nullptr : options[index]; }
|
||||
void set_nav_auto(NavDirection dir) override;
|
||||
void set_nav_none(NavDirection dir) override;
|
||||
void set_nav(NavDirection dir, Element* element) override;
|
||||
void set_nav_manual(NavDirection dir, const std::string& target) override;
|
||||
};
|
||||
|
||||
} // namespace recompui
|
27
src/ui/elements/ui_scroll_container.cpp
Normal file
27
src/ui/elements/ui_scroll_container.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "ui_scroll_container.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
ScrollContainer::ScrollContainer(Element *parent, ScrollDirection direction) : Element(parent) {
|
||||
set_flex(1.0f, 1.0f, 100.0f);
|
||||
set_width(100.0f, Unit::Percent);
|
||||
set_height(100.0f, Unit::Percent);
|
||||
|
||||
switch (direction) {
|
||||
case ScrollDirection::Horizontal:
|
||||
set_max_width(100.0f, Unit::Percent);
|
||||
set_overflow_x(Overflow::Auto);
|
||||
break;
|
||||
case ScrollDirection::Vertical:
|
||||
set_max_height(100.0f, Unit::Percent);
|
||||
set_overflow_y(Overflow::Auto);
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown scroll direction.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
19
src/ui/elements/ui_scroll_container.h
Normal file
19
src/ui/elements/ui_scroll_container.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
enum class ScrollDirection {
|
||||
Horizontal,
|
||||
Vertical
|
||||
};
|
||||
|
||||
class ScrollContainer : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ScrollContainer"; }
|
||||
public:
|
||||
ScrollContainer(Element *parent, ScrollDirection direction);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
236
src/ui/elements/ui_slider.cpp
Normal file
236
src/ui/elements/ui_slider.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "overloaded.h"
|
||||
#include "ui_slider.h"
|
||||
#include "../ui_utils.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <charconv>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
void Slider::set_value_internal(double v, bool setup, bool trigger_callbacks) {
|
||||
if (step_value != 0.0) {
|
||||
v = std::lround(v / step_value) * step_value;
|
||||
}
|
||||
|
||||
if (value != v || setup) {
|
||||
value = v;
|
||||
update_circle_position();
|
||||
update_label_text();
|
||||
|
||||
if (trigger_callbacks) {
|
||||
for (auto callback : value_changed_callbacks) {
|
||||
callback(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::bar_clicked(float x, float) {
|
||||
update_value_from_mouse(x);
|
||||
}
|
||||
|
||||
void Slider::bar_dragged(float x, float, DragPhase) {
|
||||
update_value_from_mouse(x);
|
||||
}
|
||||
|
||||
void Slider::circle_dragged(float x, float, DragPhase) {
|
||||
update_value_from_mouse(x);
|
||||
}
|
||||
|
||||
void Slider::update_value_from_mouse(float x) {
|
||||
double left = slider_element->get_absolute_left();
|
||||
double width = slider_element->get_client_width();
|
||||
double ratio = std::clamp((x - left) / width, 0.0, 1.0);
|
||||
set_value_internal(min_value + ratio * (max_value - min_value), false, true);
|
||||
}
|
||||
|
||||
void Slider::update_circle_position() {
|
||||
double ratio = std::clamp((value - min_value) / (max_value - min_value), 0.0, 1.0);
|
||||
circle_element->set_left(ratio * 100.0, Unit::Percent);
|
||||
}
|
||||
|
||||
void Slider::update_label_text() {
|
||||
char text_buffer[32];
|
||||
|
||||
if (type == SliderType::Double) {
|
||||
std::snprintf(text_buffer, sizeof(text_buffer), "%.1f", value);
|
||||
} else if (type == SliderType::Percent) {
|
||||
std::snprintf(text_buffer, sizeof(text_buffer), "%d%%", static_cast<int>(value));
|
||||
} else {
|
||||
std::snprintf(text_buffer, sizeof(text_buffer), "%d", static_cast<int>(value));
|
||||
}
|
||||
|
||||
value_label->set_text(text_buffer);
|
||||
}
|
||||
|
||||
void Slider::set_input_value(const ElementValue& val) {
|
||||
std::visit(overloaded {
|
||||
[this](uint32_t u) { set_value(u); },
|
||||
[this](float f) { set_value(f); },
|
||||
[this](double d) { set_value(d); },
|
||||
[](std::monostate) {}
|
||||
}, val);
|
||||
}
|
||||
|
||||
void Slider::process_event(const Event& e) {
|
||||
switch (e.type) {
|
||||
case EventType::Focus:
|
||||
{
|
||||
bool active = std::get<EventFocus>(e.variant).active;
|
||||
circle_element->set_style_enabled(focus_state, active);
|
||||
if (active) {
|
||||
queue_update();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Update:
|
||||
if (is_enabled()) {
|
||||
if (circle_element->is_style_enabled(focus_state)) {
|
||||
circle_element->set_background_color(recompui::get_pulse_color(750));
|
||||
queue_update();
|
||||
}
|
||||
else {
|
||||
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
|
||||
}
|
||||
}
|
||||
else {
|
||||
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
|
||||
}
|
||||
break;
|
||||
case EventType::Navigate:
|
||||
{
|
||||
NavDirection dir = std::get<EventNavigate>(e.variant).direction;
|
||||
if (dir == NavDirection::Left) {
|
||||
do_step(false);
|
||||
}
|
||||
else if (dir == NavDirection::Right) {
|
||||
do_step(true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType::Enable:
|
||||
{
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
circle_element->set_enabled(enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Slider::Slider(Element *parent, SliderType type) : Element(parent, Events(EventType::Focus, EventType::Update, EventType::Navigate, EventType::Enable)) {
|
||||
this->type = type;
|
||||
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_display(Display::Flex);
|
||||
set_flex_direction(FlexDirection::Row);
|
||||
set_text_align(TextAlign::Left);
|
||||
set_min_width(120.0f);
|
||||
|
||||
enable_focus();
|
||||
set_nav_none(NavDirection::Left);
|
||||
set_nav_none(NavDirection::Right);
|
||||
|
||||
ContextId context = get_current_context();
|
||||
|
||||
value_label = context.create_element<Label>(this, "0", LabelStyle::Small);
|
||||
value_label->set_margin_right(20.0f);
|
||||
value_label->set_min_width(60.0f);
|
||||
value_label->set_max_width(60.0f);
|
||||
|
||||
slider_element = context.create_element<Clickable>(this, true);
|
||||
slider_element->set_flex(1.0f, 0.0f);
|
||||
slider_element->add_pressed_callback([this](float x, float y){ bar_clicked(x, y); focus(); });
|
||||
slider_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
|
||||
|
||||
{
|
||||
bar_element = context.create_element<Clickable>(slider_element, true);
|
||||
bar_element->set_width(100.0f, Unit::Percent);
|
||||
bar_element->set_height(2.0f);
|
||||
bar_element->set_margin_top(8.0f);
|
||||
bar_element->set_background_color(Color{ 255, 255, 255, 50 });
|
||||
bar_element->add_pressed_callback([this](float x, float y){ bar_clicked(x, y); focus(); });
|
||||
bar_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
|
||||
|
||||
circle_element = context.create_element<Clickable>(bar_element, true);
|
||||
circle_element->set_position(Position::Relative);
|
||||
circle_element->set_width(16.0f);
|
||||
circle_element->set_height(16.0f);
|
||||
circle_element->set_margin_top(-7.0f);
|
||||
circle_element->set_margin_right(-8.0f);
|
||||
circle_element->set_margin_left(-8.0f);
|
||||
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
|
||||
circle_element->set_border_radius(8.0f);
|
||||
circle_element->add_pressed_callback([this](float, float){ focus(); });
|
||||
circle_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ circle_dragged(x, y, phase); focus(); });
|
||||
circle_element->set_cursor(Cursor::Pointer);
|
||||
}
|
||||
|
||||
set_value_internal(value, true, false);
|
||||
}
|
||||
|
||||
Slider::~Slider() {
|
||||
|
||||
}
|
||||
|
||||
void Slider::set_value(double v) {
|
||||
set_value_internal(v, false, false);
|
||||
}
|
||||
|
||||
double Slider::get_value() const {
|
||||
return value;
|
||||
}
|
||||
void Slider::set_min_value(double v) {
|
||||
min_value = v;
|
||||
}
|
||||
|
||||
double Slider::get_min_value() const {
|
||||
return min_value;
|
||||
}
|
||||
|
||||
void Slider::set_max_value(double v) {
|
||||
max_value = v;
|
||||
}
|
||||
|
||||
double Slider::get_max_value() const {
|
||||
return max_value;
|
||||
}
|
||||
|
||||
void Slider::set_step_value(double v) {
|
||||
step_value = v;
|
||||
}
|
||||
|
||||
double Slider::get_step_value() const {
|
||||
return step_value;
|
||||
}
|
||||
|
||||
void Slider::add_value_changed_callback(std::function<void(double)> callback) {
|
||||
value_changed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
void Slider::do_step(bool increment) {
|
||||
double new_value = value;
|
||||
if (increment) {
|
||||
new_value += step_value;
|
||||
}
|
||||
else {
|
||||
new_value -= step_value;
|
||||
}
|
||||
new_value = std::clamp(new_value, min_value, max_value);
|
||||
if (new_value != value) {
|
||||
set_value_internal(new_value, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace recompui
|
56
src/ui/elements/ui_slider.h
Normal file
56
src/ui/elements/ui_slider.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_clickable.h"
|
||||
#include "ui_label.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
enum SliderType {
|
||||
Double,
|
||||
Percent,
|
||||
Integer
|
||||
};
|
||||
|
||||
class Slider : public Element {
|
||||
private:
|
||||
SliderType type = SliderType::Percent;
|
||||
Label *value_label = nullptr;
|
||||
Clickable *slider_element = nullptr;
|
||||
Clickable *bar_element = nullptr;
|
||||
Clickable *circle_element = nullptr;
|
||||
double value = 50.0;
|
||||
double min_value = 0.0;
|
||||
double max_value = 100.0;
|
||||
double step_value = 0.0;
|
||||
std::vector<std::function<void(double)>> value_changed_callbacks;
|
||||
|
||||
void set_value_internal(double v, bool setup, bool trigger_callbacks);
|
||||
void bar_clicked(float x, float y);
|
||||
void bar_dragged(float x, float y, DragPhase phase);
|
||||
void circle_dragged(float x, float y, DragPhase phase);
|
||||
void update_value_from_mouse(float x);
|
||||
void update_circle_position();
|
||||
void update_label_text();
|
||||
void set_input_value(const ElementValue& val) override;
|
||||
ElementValue get_element_value() override { return get_value(); }
|
||||
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Slider"; }
|
||||
|
||||
public:
|
||||
Slider(Element *parent, SliderType type);
|
||||
virtual ~Slider();
|
||||
void set_value(double v);
|
||||
double get_value() const;
|
||||
void set_min_value(double v);
|
||||
double get_min_value() const;
|
||||
void set_max_value(double v);
|
||||
double get_max_value() const;
|
||||
void set_step_value(double v);
|
||||
double get_step_value() const;
|
||||
void add_value_changed_callback(std::function<void(double)> callback);
|
||||
void do_step(bool increment);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
15
src/ui/elements/ui_span.cpp
Normal file
15
src/ui/elements/ui_span.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "ui_span.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Span::Span(Element *parent) : Element(parent, 0, "span", true) {
|
||||
set_font_style(FontStyle::Normal);
|
||||
}
|
||||
|
||||
Span::Span(Element *parent, const std::string &text) : Span(parent) {
|
||||
set_text(text);
|
||||
}
|
||||
|
||||
};
|
16
src/ui/elements/ui_span.h
Normal file
16
src/ui/elements/ui_span.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
#include "ui_label.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class Span : public Element {
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "Span"; }
|
||||
public:
|
||||
Span(Element *parent);
|
||||
Span(Element *parent, const std::string &text);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
612
src/ui/elements/ui_style.cpp
Normal file
612
src/ui/elements/ui_style.cpp
Normal file
@@ -0,0 +1,612 @@
|
||||
#include "ui_style.h"
|
||||
#include "ui_element.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
static Rml::Unit to_rml(Unit unit) {
|
||||
switch (unit) {
|
||||
case Unit::Px:
|
||||
return Rml::Unit::PX;
|
||||
case Unit::Dp:
|
||||
return Rml::Unit::DP;
|
||||
case Unit::Percent:
|
||||
return Rml::Unit::PERCENT;
|
||||
default:
|
||||
return Rml::Unit::UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::AlignItems to_rml(AlignItems align_items) {
|
||||
switch (align_items) {
|
||||
case AlignItems::FlexStart:
|
||||
return Rml::Style::AlignItems::FlexStart;
|
||||
case AlignItems::FlexEnd:
|
||||
return Rml::Style::AlignItems::FlexEnd;
|
||||
case AlignItems::Center:
|
||||
return Rml::Style::AlignItems::Center;
|
||||
case AlignItems::Baseline:
|
||||
return Rml::Style::AlignItems::Baseline;
|
||||
case AlignItems::Stretch:
|
||||
return Rml::Style::AlignItems::Stretch;
|
||||
default:
|
||||
assert(false && "Unknown align items.");
|
||||
return Rml::Style::AlignItems::FlexStart;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::Overflow to_rml(Overflow overflow) {
|
||||
switch (overflow) {
|
||||
case Overflow::Visible:
|
||||
return Rml::Style::Overflow::Visible;
|
||||
case Overflow::Hidden:
|
||||
return Rml::Style::Overflow::Hidden;
|
||||
case Overflow::Auto:
|
||||
return Rml::Style::Overflow::Auto;
|
||||
case Overflow::Scroll:
|
||||
return Rml::Style::Overflow::Scroll;
|
||||
default:
|
||||
assert(false && "Unknown overflow.");
|
||||
return Rml::Style::Overflow::Visible;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::TextAlign to_rml(TextAlign text_align) {
|
||||
switch (text_align) {
|
||||
case TextAlign::Left:
|
||||
return Rml::Style::TextAlign::Left;
|
||||
case TextAlign::Right:
|
||||
return Rml::Style::TextAlign::Right;
|
||||
case TextAlign::Center:
|
||||
return Rml::Style::TextAlign::Center;
|
||||
case TextAlign::Justify:
|
||||
return Rml::Style::TextAlign::Justify;
|
||||
default:
|
||||
assert(false && "Unknown text align.");
|
||||
return Rml::Style::TextAlign::Left;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::TextTransform to_rml(TextTransform text_transform) {
|
||||
switch (text_transform) {
|
||||
case TextTransform::None:
|
||||
return Rml::Style::TextTransform::None;
|
||||
case TextTransform::Capitalize:
|
||||
return Rml::Style::TextTransform::Capitalize;
|
||||
case TextTransform::Uppercase:
|
||||
return Rml::Style::TextTransform::Uppercase;
|
||||
case TextTransform::Lowercase:
|
||||
return Rml::Style::TextTransform::Lowercase;
|
||||
default:
|
||||
assert(false && "Unknown text transform.");
|
||||
return Rml::Style::TextTransform::None;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::Drag to_rml(Drag drag) {
|
||||
switch (drag) {
|
||||
case Drag::None:
|
||||
return Rml::Style::Drag::None;
|
||||
case Drag::Drag:
|
||||
return Rml::Style::Drag::Drag;
|
||||
case Drag::DragDrop:
|
||||
return Rml::Style::Drag::DragDrop;
|
||||
case Drag::Block:
|
||||
return Rml::Style::Drag::Block;
|
||||
case Drag::Clone:
|
||||
return Rml::Style::Drag::Clone;
|
||||
default:
|
||||
assert(false && "Unknown drag.");
|
||||
return Rml::Style::Drag::None;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::TabIndex to_rml(TabIndex tab_index) {
|
||||
switch (tab_index) {
|
||||
case TabIndex::None:
|
||||
return Rml::Style::TabIndex::None;
|
||||
case TabIndex::Auto:
|
||||
return Rml::Style::TabIndex::Auto;
|
||||
default:
|
||||
assert(false && "Unknown tab index.");
|
||||
return Rml::Style::TabIndex::None;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::Display to_rml(Display display) {
|
||||
switch (display) {
|
||||
case Display::None:
|
||||
return Rml::Style::Display::None;
|
||||
case Display::Block:
|
||||
return Rml::Style::Display::Block;
|
||||
case Display::Inline:
|
||||
return Rml::Style::Display::Inline;
|
||||
case Display::InlineBlock:
|
||||
return Rml::Style::Display::InlineBlock;
|
||||
case Display::FlowRoot:
|
||||
return Rml::Style::Display::FlowRoot;
|
||||
case Display::Flex:
|
||||
return Rml::Style::Display::Flex;
|
||||
case Display::InlineFlex:
|
||||
return Rml::Style::Display::InlineFlex;
|
||||
case Display::Table:
|
||||
return Rml::Style::Display::Table;
|
||||
case Display::InlineTable:
|
||||
return Rml::Style::Display::InlineTable;
|
||||
case Display::TableRow:
|
||||
return Rml::Style::Display::TableRow;
|
||||
case Display::TableRowGroup:
|
||||
return Rml::Style::Display::TableRowGroup;
|
||||
case Display::TableColumn:
|
||||
return Rml::Style::Display::TableColumn;
|
||||
case Display::TableColumnGroup:
|
||||
return Rml::Style::Display::TableColumnGroup;
|
||||
case Display::TableCell:
|
||||
return Rml::Style::Display::TableCell;
|
||||
default:
|
||||
assert(false && "Unknown display.");
|
||||
return Rml::Style::Display::Block;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::Style::JustifyContent to_rml(JustifyContent justify_content) {
|
||||
switch (justify_content) {
|
||||
case JustifyContent::FlexStart:
|
||||
return Rml::Style::JustifyContent::FlexStart;
|
||||
case JustifyContent::FlexEnd:
|
||||
return Rml::Style::JustifyContent::FlexEnd;
|
||||
case JustifyContent::Center:
|
||||
return Rml::Style::JustifyContent::Center;
|
||||
case JustifyContent::SpaceBetween:
|
||||
return Rml::Style::JustifyContent::SpaceBetween;
|
||||
case JustifyContent::SpaceAround:
|
||||
return Rml::Style::JustifyContent::SpaceAround;
|
||||
case JustifyContent::SpaceEvenly:
|
||||
return Rml::Style::JustifyContent::SpaceEvenly;
|
||||
default:
|
||||
assert(false && "Unknown justify content.");
|
||||
return Rml::Style::JustifyContent::FlexStart;
|
||||
}
|
||||
}
|
||||
|
||||
static Rml::PropertyId nav_to_property(NavDirection dir) {
|
||||
switch (dir) {
|
||||
case NavDirection::Up:
|
||||
return Rml::PropertyId::NavUp;
|
||||
case NavDirection::Right:
|
||||
return Rml::PropertyId::NavRight;
|
||||
case NavDirection::Down:
|
||||
return Rml::PropertyId::NavDown;
|
||||
case NavDirection::Left:
|
||||
return Rml::PropertyId::NavLeft;
|
||||
default:
|
||||
assert(false && "Unknown nav direction.");
|
||||
return Rml::PropertyId::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
|
||||
property_map[property_id] = property;
|
||||
}
|
||||
|
||||
Style::Style() {
|
||||
|
||||
}
|
||||
|
||||
Style::~Style() {
|
||||
|
||||
}
|
||||
|
||||
void Style::set_visibility(Visibility visibility) {
|
||||
switch (visibility) {
|
||||
case Visibility::Visible:
|
||||
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
|
||||
break;
|
||||
case Visibility::Hidden:
|
||||
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_position(Position position) {
|
||||
switch (position) {
|
||||
case Position::Absolute:
|
||||
set_property(Rml::PropertyId::Position, Rml::Style::Position::Absolute);
|
||||
break;
|
||||
case Position::Relative:
|
||||
set_property(Rml::PropertyId::Position, Rml::Style::Position::Relative);
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown position.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_left(float left, Unit unit) {
|
||||
set_property(Rml::PropertyId::Left, Rml::Property(left, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_top(float top, Unit unit) {
|
||||
set_property(Rml::PropertyId::Top, Rml::Property(top, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_right(float right, Unit unit) {
|
||||
set_property(Rml::PropertyId::Right, Rml::Property(right, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_bottom(float bottom, Unit unit) {
|
||||
set_property(Rml::PropertyId::Bottom, Rml::Property(bottom, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_width(float width, Unit unit) {
|
||||
set_property(Rml::PropertyId::Width, Rml::Property(width, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_width_auto() {
|
||||
set_property(Rml::PropertyId::Width, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_height(float height, Unit unit) {
|
||||
set_property(Rml::PropertyId::Height, Rml::Property(height, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_height_auto() {
|
||||
set_property(Rml::PropertyId::Height, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_min_width(float width, Unit unit) {
|
||||
set_property(Rml::PropertyId::MinWidth, Rml::Property(width, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_min_height(float height, Unit unit) {
|
||||
set_property(Rml::PropertyId::MinHeight, Rml::Property(height, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_max_width(float width, Unit unit) {
|
||||
set_property(Rml::PropertyId::MaxWidth, Rml::Property(width, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_max_height(float height, Unit unit) {
|
||||
set_property(Rml::PropertyId::MaxHeight, Rml::Property(height, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_padding(float padding, Unit unit) {
|
||||
set_property(Rml::PropertyId::PaddingLeft, Rml::Property(padding, to_rml(unit)));
|
||||
set_property(Rml::PropertyId::PaddingTop, Rml::Property(padding, to_rml(unit)));
|
||||
set_property(Rml::PropertyId::PaddingRight, Rml::Property(padding, to_rml(unit)));
|
||||
set_property(Rml::PropertyId::PaddingBottom, Rml::Property(padding, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_padding_left(float padding, Unit unit) {
|
||||
set_property(Rml::PropertyId::PaddingLeft, Rml::Property(padding, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_padding_top(float padding, Unit unit) {
|
||||
set_property(Rml::PropertyId::PaddingTop, Rml::Property(padding, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_padding_right(float padding, Unit unit) {
|
||||
set_property(Rml::PropertyId::PaddingRight, Rml::Property(padding, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_padding_bottom(float padding, Unit unit) {
|
||||
set_property(Rml::PropertyId::PaddingBottom, Rml::Property(padding, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_margin(float margin, Unit unit) {
|
||||
set_property(Rml::PropertyId::MarginLeft, Rml::Property(margin, to_rml(unit)));
|
||||
set_property(Rml::PropertyId::MarginTop, Rml::Property(margin, to_rml(unit)));
|
||||
set_property(Rml::PropertyId::MarginRight, Rml::Property(margin, to_rml(unit)));
|
||||
set_property(Rml::PropertyId::MarginBottom, Rml::Property(margin, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_margin_left(float margin, Unit unit) {
|
||||
set_property(Rml::PropertyId::MarginLeft, Rml::Property(margin, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_margin_top(float margin, Unit unit) {
|
||||
set_property(Rml::PropertyId::MarginTop, Rml::Property(margin, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_margin_right(float margin, Unit unit) {
|
||||
set_property(Rml::PropertyId::MarginRight, Rml::Property(margin, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_margin_bottom(float margin, Unit unit) {
|
||||
set_property(Rml::PropertyId::MarginBottom, Rml::Property(margin, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_margin_auto() {
|
||||
set_property(Rml::PropertyId::MarginLeft, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
set_property(Rml::PropertyId::MarginTop, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
set_property(Rml::PropertyId::MarginRight, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
set_property(Rml::PropertyId::MarginBottom, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_margin_left_auto() {
|
||||
set_property(Rml::PropertyId::MarginLeft, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_margin_top_auto() {
|
||||
set_property(Rml::PropertyId::MarginTop, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_margin_right_auto() {
|
||||
set_property(Rml::PropertyId::MarginRight, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_margin_bottom_auto() {
|
||||
set_property(Rml::PropertyId::MarginBottom, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_border_width(float width, Unit unit) {
|
||||
Rml::Property property(width, to_rml(unit));
|
||||
set_property(Rml::PropertyId::BorderTopWidth, property);
|
||||
set_property(Rml::PropertyId::BorderBottomWidth, property);
|
||||
set_property(Rml::PropertyId::BorderLeftWidth, property);
|
||||
set_property(Rml::PropertyId::BorderRightWidth, property);
|
||||
}
|
||||
|
||||
void Style::set_border_left_width(float width, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderLeftWidth, Rml::Property(width, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_border_top_width(float width, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderTopWidth, Rml::Property(width, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_border_right_width(float width, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderRightWidth, Rml::Property(width, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_border_bottom_width(float width, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderBottomWidth, Rml::Property(width, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_border_radius(float radius, Unit unit) {
|
||||
Rml::Property property(radius, to_rml(unit));
|
||||
set_property(Rml::PropertyId::BorderTopLeftRadius, property);
|
||||
set_property(Rml::PropertyId::BorderTopRightRadius, property);
|
||||
set_property(Rml::PropertyId::BorderBottomLeftRadius, property);
|
||||
set_property(Rml::PropertyId::BorderBottomRightRadius, property);
|
||||
}
|
||||
|
||||
void Style::set_border_top_left_radius(float radius, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderTopLeftRadius, Rml::Property(radius, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_border_top_right_radius(float radius, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderTopRightRadius, Rml::Property(radius, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_border_bottom_left_radius(float radius, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderBottomLeftRadius, Rml::Property(radius, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_border_bottom_right_radius(float radius, Unit unit) {
|
||||
set_property(Rml::PropertyId::BorderBottomRightRadius, Rml::Property(radius, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_background_color(const Color &color) {
|
||||
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
|
||||
set_property(Rml::PropertyId::BackgroundColor, property);
|
||||
}
|
||||
|
||||
void Style::set_border_color(const Color &color) {
|
||||
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
|
||||
set_property(Rml::PropertyId::BorderTopColor, property);
|
||||
set_property(Rml::PropertyId::BorderBottomColor, property);
|
||||
set_property(Rml::PropertyId::BorderLeftColor, property);
|
||||
set_property(Rml::PropertyId::BorderRightColor, property);
|
||||
}
|
||||
|
||||
void Style::set_border_left_color(const Color &color) {
|
||||
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
|
||||
set_property(Rml::PropertyId::BorderLeftColor, property);
|
||||
}
|
||||
|
||||
void Style::set_border_top_color(const Color &color) {
|
||||
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
|
||||
set_property(Rml::PropertyId::BorderTopColor, property);
|
||||
}
|
||||
|
||||
void Style::set_border_right_color(const Color &color) {
|
||||
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
|
||||
set_property(Rml::PropertyId::BorderRightColor, property);
|
||||
}
|
||||
|
||||
void Style::set_border_bottom_color(const Color &color) {
|
||||
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
|
||||
set_property(Rml::PropertyId::BorderBottomColor, property);
|
||||
}
|
||||
|
||||
void Style::set_color(const Color &color) {
|
||||
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
|
||||
set_property(Rml::PropertyId::Color, property);
|
||||
}
|
||||
|
||||
void Style::set_cursor(Cursor cursor) {
|
||||
switch (cursor) {
|
||||
case Cursor::None:
|
||||
set_property(Rml::PropertyId::Cursor, Rml::Property("", Rml::Unit::STRING));
|
||||
break;
|
||||
case Cursor::Pointer:
|
||||
set_property(Rml::PropertyId::Cursor, Rml::Property("pointer", Rml::Unit::STRING));
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown cursor.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_opacity(float opacity) {
|
||||
set_property(Rml::PropertyId::Opacity, Rml::Property(opacity, Rml::Unit::NUMBER));
|
||||
}
|
||||
|
||||
void Style::set_display(Display display) {
|
||||
set_property(Rml::PropertyId::Display, to_rml(display));
|
||||
}
|
||||
|
||||
void Style::set_justify_content(JustifyContent justify_content) {
|
||||
set_property(Rml::PropertyId::JustifyContent, to_rml(justify_content));
|
||||
}
|
||||
|
||||
void Style::set_flex_grow(float grow) {
|
||||
set_property(Rml::PropertyId::FlexGrow, Rml::Property(grow, Rml::Unit::NUMBER));
|
||||
}
|
||||
|
||||
void Style::set_flex_shrink(float shrink) {
|
||||
set_property(Rml::PropertyId::FlexShrink, Rml::Property(shrink, Rml::Unit::NUMBER));
|
||||
}
|
||||
|
||||
void Style::set_flex_basis_auto() {
|
||||
set_property(Rml::PropertyId::FlexBasis, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
|
||||
}
|
||||
|
||||
void Style::set_flex_basis(float basis, Unit unit) {
|
||||
set_property(Rml::PropertyId::FlexBasis, Rml::Property(basis, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_flex(float grow, float shrink) {
|
||||
set_flex_grow(grow);
|
||||
set_flex_shrink(shrink);
|
||||
set_flex_basis_auto();
|
||||
}
|
||||
|
||||
void Style::set_flex(float grow, float shrink, float basis, Unit basis_unit) {
|
||||
set_flex_grow(grow);
|
||||
set_flex_shrink(shrink);
|
||||
set_flex_basis(basis, basis_unit);
|
||||
}
|
||||
|
||||
void Style::set_flex_direction(FlexDirection flex_direction) {
|
||||
switch (flex_direction) {
|
||||
case FlexDirection::Row:
|
||||
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row);
|
||||
break;
|
||||
case FlexDirection::Column:
|
||||
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column);
|
||||
break;
|
||||
case FlexDirection::RowReverse:
|
||||
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::RowReverse);
|
||||
break;
|
||||
case FlexDirection::ColumnReverse:
|
||||
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::ColumnReverse);
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown flex direction.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_align_items(AlignItems align_items) {
|
||||
set_property(Rml::PropertyId::AlignItems, to_rml(align_items));
|
||||
}
|
||||
|
||||
void Style::set_overflow(Overflow overflow) {
|
||||
set_property(Rml::PropertyId::OverflowX, to_rml(overflow));
|
||||
set_property(Rml::PropertyId::OverflowY, to_rml(overflow));
|
||||
}
|
||||
|
||||
void Style::set_overflow_x(Overflow overflow) {
|
||||
set_property(Rml::PropertyId::OverflowX, to_rml(overflow));
|
||||
}
|
||||
|
||||
void Style::set_overflow_y(Overflow overflow) {
|
||||
set_property(Rml::PropertyId::OverflowY, to_rml(overflow));
|
||||
}
|
||||
|
||||
void Style::set_font_size(float size, Unit unit) {
|
||||
set_property(Rml::PropertyId::FontSize, Rml::Property(size, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_letter_spacing(float spacing, Unit unit) {
|
||||
set_property(Rml::PropertyId::LetterSpacing, Rml::Property(spacing, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_line_height(float height, Unit unit) {
|
||||
set_property(Rml::PropertyId::LineHeight, Rml::Property(height, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_font_style(FontStyle style) {
|
||||
switch (style) {
|
||||
case FontStyle::Normal:
|
||||
set_property(Rml::PropertyId::FontStyle, Rml::Style::FontStyle::Normal);
|
||||
break;
|
||||
case FontStyle::Italic:
|
||||
set_property(Rml::PropertyId::FontStyle, Rml::Style::FontStyle::Italic);
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown font style.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Style::set_font_weight(uint32_t weight) {
|
||||
set_property(Rml::PropertyId::FontWeight, Rml::Style::FontWeight(weight));
|
||||
}
|
||||
|
||||
void Style::set_text_align(TextAlign text_align) {
|
||||
set_property(Rml::PropertyId::TextAlign, to_rml(text_align));
|
||||
}
|
||||
|
||||
void Style::set_text_transform(TextTransform text_transform) {
|
||||
set_property(Rml::PropertyId::TextTransform, to_rml(text_transform));
|
||||
}
|
||||
|
||||
void Style::set_gap(float size, Unit unit) {
|
||||
set_row_gap(size, unit);
|
||||
set_column_gap(size, unit);
|
||||
}
|
||||
|
||||
void Style::set_row_gap(float size, Unit unit) {
|
||||
set_property(Rml::PropertyId::RowGap, Rml::Property(size, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_column_gap(float size, Unit unit) {
|
||||
set_property(Rml::PropertyId::ColumnGap, Rml::Property(size, to_rml(unit)));
|
||||
}
|
||||
|
||||
void Style::set_drag(Drag drag) {
|
||||
set_property(Rml::PropertyId::Drag, to_rml(drag));
|
||||
}
|
||||
|
||||
void Style::set_tab_index(TabIndex tab_index) {
|
||||
set_property(Rml::PropertyId::TabIndex, to_rml(tab_index));
|
||||
}
|
||||
|
||||
void Style::set_font_family(std::string_view family) {
|
||||
set_property(Rml::PropertyId::FontFamily, Rml::Property(Rml::String{ family }, Rml::Unit::UNKNOWN));
|
||||
}
|
||||
|
||||
void Style::set_nav_auto(NavDirection dir) {
|
||||
set_property(nav_to_property(dir), Rml::Style::Nav::Auto);
|
||||
}
|
||||
|
||||
void Style::set_nav_none(NavDirection dir) {
|
||||
set_property(nav_to_property(dir), Rml::Style::Nav::None);
|
||||
}
|
||||
|
||||
void Style::set_nav(NavDirection dir, Element* element) {
|
||||
set_property(nav_to_property(dir), Rml::Property(Rml::String{ "#" + element->get_id() }, Rml::Unit::STRING));
|
||||
}
|
||||
|
||||
void Style::set_nav_manual(NavDirection dir, const std::string& target) {
|
||||
set_property(nav_to_property(dir), Rml::Property(target, Rml::Unit::STRING));
|
||||
}
|
||||
|
||||
void Style::set_tab_index_auto() {
|
||||
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::Auto);
|
||||
}
|
||||
|
||||
void Style::set_tab_index_none() {
|
||||
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::None);
|
||||
}
|
||||
|
||||
void Style::set_focusable(bool focusable) {
|
||||
set_property(Rml::PropertyId::Focus, focusable ? Rml::Style::Focus::Auto : Rml::Style::Focus::None);
|
||||
}
|
||||
|
||||
|
||||
} // namespace recompui
|
108
src/ui/elements/ui_style.h
Normal file
108
src/ui/elements/ui_style.h
Normal file
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "RmlUi/Core.h"
|
||||
|
||||
#include "../core/ui_resource.h"
|
||||
#include "ui_types.h"
|
||||
|
||||
namespace recompui {
|
||||
class ContextId;
|
||||
class Style {
|
||||
friend class Element; // For access to property_map without making it visible to element subclasses.
|
||||
friend class ContextId;
|
||||
private:
|
||||
std::map<Rml::PropertyId, Rml::Property> property_map;
|
||||
protected:
|
||||
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property);
|
||||
ResourceId resource_id = ResourceId::null();
|
||||
public:
|
||||
Style();
|
||||
virtual ~Style();
|
||||
void set_visibility(Visibility visibility);
|
||||
void set_position(Position position);
|
||||
void set_left(float left, Unit unit = Unit::Dp);
|
||||
void set_top(float top, Unit unit = Unit::Dp);
|
||||
void set_right(float right, Unit unit = Unit::Dp);
|
||||
void set_bottom(float bottom, Unit unit = Unit::Dp);
|
||||
void set_width(float width, Unit unit = Unit::Dp);
|
||||
void set_width_auto();
|
||||
void set_height(float height, Unit unit = Unit::Dp);
|
||||
void set_height_auto();
|
||||
void set_min_width(float width, Unit unit = Unit::Dp);
|
||||
void set_min_height(float height, Unit unit = Unit::Dp);
|
||||
void set_max_width(float width, Unit unit = Unit::Dp);
|
||||
void set_max_height(float height, Unit unit = Unit::Dp);
|
||||
void set_padding(float padding, Unit unit = Unit::Dp);
|
||||
void set_padding_left(float padding, Unit unit = Unit::Dp);
|
||||
void set_padding_top(float padding, Unit unit = Unit::Dp);
|
||||
void set_padding_right(float padding, Unit unit = Unit::Dp);
|
||||
void set_padding_bottom(float padding, Unit unit = Unit::Dp);
|
||||
void set_margin(float margin, Unit unit = Unit::Dp);
|
||||
void set_margin_left(float margin, Unit unit = Unit::Dp);
|
||||
void set_margin_top(float margin, Unit unit = Unit::Dp);
|
||||
void set_margin_right(float margin, Unit unit = Unit::Dp);
|
||||
void set_margin_bottom(float margin, Unit unit = Unit::Dp);
|
||||
void set_margin_auto();
|
||||
void set_margin_left_auto();
|
||||
void set_margin_top_auto();
|
||||
void set_margin_right_auto();
|
||||
void set_margin_bottom_auto();
|
||||
void set_border_width(float width, Unit unit = Unit::Dp);
|
||||
void set_border_left_width(float width, Unit unit = Unit::Dp);
|
||||
void set_border_top_width(float width, Unit unit = Unit::Dp);
|
||||
void set_border_right_width(float width, Unit unit = Unit::Dp);
|
||||
void set_border_bottom_width(float width, Unit unit = Unit::Dp);
|
||||
void set_border_radius(float radius, Unit unit = Unit::Dp);
|
||||
void set_border_top_left_radius(float radius, Unit unit = Unit::Dp);
|
||||
void set_border_top_right_radius(float radius, Unit unit = Unit::Dp);
|
||||
void set_border_bottom_left_radius(float radius, Unit unit = Unit::Dp);
|
||||
void set_border_bottom_right_radius(float radius, Unit unit = Unit::Dp);
|
||||
void set_background_color(const Color &color);
|
||||
void set_border_color(const Color &color);
|
||||
void set_border_left_color(const Color &color);
|
||||
void set_border_top_color(const Color &color);
|
||||
void set_border_right_color(const Color &color);
|
||||
void set_border_bottom_color(const Color &color);
|
||||
void set_color(const Color &color);
|
||||
void set_cursor(Cursor cursor);
|
||||
void set_opacity(float opacity);
|
||||
void set_display(Display display);
|
||||
void set_justify_content(JustifyContent justify_content);
|
||||
void set_flex_grow(float grow);
|
||||
void set_flex_shrink(float shrink);
|
||||
void set_flex_basis_auto();
|
||||
void set_flex_basis(float basis, Unit unit = Unit::Percent);
|
||||
void set_flex(float grow, float shrink);
|
||||
void set_flex(float grow, float shrink, float basis, Unit basis_unit = Unit::Percent);
|
||||
void set_flex_direction(FlexDirection flex_direction);
|
||||
void set_align_items(AlignItems align_items);
|
||||
void set_overflow(Overflow overflow);
|
||||
void set_overflow_x(Overflow overflow);
|
||||
void set_overflow_y(Overflow overflow);
|
||||
void set_font_size(float size, Unit unit = Unit::Dp);
|
||||
void set_letter_spacing(float spacing, Unit unit = Unit::Dp);
|
||||
void set_line_height(float height, Unit unit = Unit::Dp);
|
||||
void set_font_style(FontStyle style);
|
||||
void set_font_weight(uint32_t weight);
|
||||
void set_text_align(TextAlign text_align);
|
||||
void set_text_transform(TextTransform text_transform);
|
||||
void set_gap(float size, Unit unit = Unit::Dp);
|
||||
void set_row_gap(float size, Unit unit = Unit::Dp);
|
||||
void set_column_gap(float size, Unit unit = Unit::Dp);
|
||||
void set_drag(Drag drag);
|
||||
void set_tab_index(TabIndex focus);
|
||||
void set_font_family(std::string_view family);
|
||||
virtual void set_nav_auto(NavDirection dir);
|
||||
virtual void set_nav_none(NavDirection dir);
|
||||
virtual void set_nav(NavDirection dir, Element* element);
|
||||
virtual void set_nav_manual(NavDirection dir, const std::string& target);
|
||||
void set_tab_index_auto();
|
||||
void set_tab_index_none();
|
||||
void set_focusable(bool focusable);
|
||||
virtual bool is_element() { return false; }
|
||||
ResourceId get_resource_id() { return resource_id; }
|
||||
};
|
||||
|
||||
} // namespace recompui
|
51
src/ui/elements/ui_text_input.cpp
Normal file
51
src/ui/elements/ui_text_input.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "ui_text_input.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
void TextInput::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Text: {
|
||||
const EventText &event = std::get<EventText>(e.variant);
|
||||
text = event.text;
|
||||
|
||||
for (const auto &function : text_changed_callbacks) {
|
||||
function(text);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TextInput::TextInput(Element *parent, bool text_visible) : Element(parent, Events(EventType::Text), "input") {
|
||||
if (!text_visible) {
|
||||
set_attribute("type", "password");
|
||||
}
|
||||
set_min_width(60.0f);
|
||||
set_border_color(Color{ 242, 242, 242, 255 });
|
||||
set_border_bottom_width(1.0f);
|
||||
set_padding_bottom(6.0f);
|
||||
set_focusable(true);
|
||||
set_nav_auto(NavDirection::Up);
|
||||
set_nav_auto(NavDirection::Down);
|
||||
set_tab_index_auto();
|
||||
}
|
||||
|
||||
void TextInput::set_text(std::string_view text) {
|
||||
this->text = std::string(text);
|
||||
set_attribute("value", this->text);
|
||||
}
|
||||
|
||||
const std::string &TextInput::get_text() {
|
||||
return text;
|
||||
}
|
||||
|
||||
void TextInput::add_text_changed_callback(std::function<void(const std::string &)> callback) {
|
||||
text_changed_callbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
};
|
21
src/ui/elements/ui_text_input.h
Normal file
21
src/ui/elements/ui_text_input.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class TextInput : public Element {
|
||||
private:
|
||||
std::string text;
|
||||
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
|
||||
protected:
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "TextInput"; }
|
||||
public:
|
||||
TextInput(Element *parent, bool text_visible = true);
|
||||
void set_text(std::string_view text);
|
||||
const std::string &get_text();
|
||||
void add_text_changed_callback(std::function<void(const std::string &)> callback);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
161
src/ui/elements/ui_toggle.cpp
Normal file
161
src/ui/elements/ui_toggle.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
#include "ui_toggle.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <ultramodern/ultramodern.hpp>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
|
||||
enable_focus();
|
||||
|
||||
set_width(162.0f);
|
||||
set_height(72.0f);
|
||||
set_border_radius(36.0f);
|
||||
set_opacity(0.9f);
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_border_width(2.0f);
|
||||
set_border_color(Color{ 177, 76, 34, 255 });
|
||||
set_background_color(Color{ 0, 0, 0, 0 });
|
||||
checked_style.set_border_color(Color{ 34, 177, 76, 255 });
|
||||
hover_style.set_border_color(Color{ 177, 76, 34, 255 });
|
||||
hover_style.set_background_color(Color{ 206, 120, 68, 76 });
|
||||
focus_style.set_border_color(Color{ 177, 76, 34, 255 });
|
||||
focus_style.set_background_color(Color{ 206, 120, 68, 76 });
|
||||
checked_hover_style.set_border_color(Color{ 34, 177, 76, 255 });
|
||||
checked_hover_style.set_background_color(Color{ 68, 206, 120, 76 });
|
||||
checked_focus_style.set_border_color(Color{ 34, 177, 76, 255 });
|
||||
checked_focus_style.set_background_color(Color{ 68, 206, 120, 76 });
|
||||
disabled_style.set_border_color(Color{ 177, 76, 34, 128 });
|
||||
checked_disabled_style.set_border_color(Color{ 34, 177, 76, 128 });
|
||||
add_style(&checked_style, checked_state);
|
||||
add_style(&hover_style, hover_state);
|
||||
add_style(&focus_style, focus_state);
|
||||
add_style(&checked_hover_style, { checked_state, hover_state });
|
||||
add_style(&checked_focus_style, { checked_state, focus_state });
|
||||
add_style(&disabled_style, disabled_state);
|
||||
add_style(&checked_disabled_style, { checked_state, disabled_state });
|
||||
|
||||
ContextId context = get_current_context();
|
||||
|
||||
floater = context.create_element<Element>(this);
|
||||
floater->set_position(Position::Relative);
|
||||
floater->set_top(2.0f);
|
||||
floater->set_width(80.0f);
|
||||
floater->set_height(64.0f);
|
||||
floater->set_border_radius(32.0f);
|
||||
floater->set_background_color(Color{ 177, 76, 34, 255 });
|
||||
floater_checked_style.set_background_color(Color{ 34, 177, 76, 255 });
|
||||
floater_disabled_style.set_background_color(Color{ 177, 76, 34, 128 });
|
||||
floater_disabled_checked_style.set_background_color(Color{ 34, 177, 76, 128 });
|
||||
floater->add_style(&floater_checked_style, checked_state);
|
||||
floater->add_style(&floater_disabled_style, disabled_state);
|
||||
floater->add_style(&floater_disabled_checked_style, { checked_state, disabled_state });
|
||||
|
||||
set_checked_internal(false, false, true, false);
|
||||
}
|
||||
|
||||
void Toggle::set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks) {
|
||||
if (this->checked != checked || setup) {
|
||||
this->checked = checked;
|
||||
|
||||
if (animate) {
|
||||
last_time = ultramodern::time_since_start();
|
||||
queue_update();
|
||||
}
|
||||
else {
|
||||
floater_left = floater_left_target();
|
||||
}
|
||||
|
||||
floater->set_left(floater_left, Unit::Dp);
|
||||
|
||||
if (trigger_callbacks) {
|
||||
for (const auto &function : checked_callbacks) {
|
||||
function(checked);
|
||||
}
|
||||
}
|
||||
|
||||
set_style_enabled(checked_state, checked);
|
||||
floater->set_style_enabled(checked_state, checked);
|
||||
}
|
||||
}
|
||||
|
||||
float Toggle::floater_left_target() const {
|
||||
return checked ? 78.0f : 4.0f;
|
||||
}
|
||||
|
||||
void Toggle::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Click:
|
||||
if (is_enabled()) {
|
||||
set_checked_internal(!checked, true, false, true);
|
||||
}
|
||||
|
||||
break;
|
||||
case EventType::Hover: {
|
||||
bool hover_active = std::get<EventHover>(e.variant).active && is_enabled();
|
||||
set_style_enabled(hover_state, hover_active);
|
||||
floater->set_style_enabled(hover_state, hover_active);
|
||||
break;
|
||||
}
|
||||
case EventType::Focus: {
|
||||
bool focus_active = std::get<EventFocus>(e.variant).active;
|
||||
set_style_enabled(focus_state, focus_active);
|
||||
break;
|
||||
}
|
||||
case EventType::Enable: {
|
||||
bool enable_active = std::get<EventEnable>(e.variant).active;
|
||||
set_style_enabled(disabled_state, !enable_active);
|
||||
floater->set_style_enabled(disabled_state, !enable_active);
|
||||
if (enable_active) {
|
||||
set_cursor(Cursor::Pointer);
|
||||
set_focusable(true);
|
||||
}
|
||||
else {
|
||||
set_cursor(Cursor::None);
|
||||
set_focusable(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EventType::Update: {
|
||||
std::chrono::high_resolution_clock::duration now = ultramodern::time_since_start();
|
||||
float delta_time = std::max(std::chrono::duration<float>(now - last_time).count(), 0.0f);
|
||||
last_time = now;
|
||||
|
||||
constexpr float dp_speed = 740.0f;
|
||||
const float target = floater_left_target();
|
||||
if (target < floater_left) {
|
||||
floater_left += std::max(-dp_speed * delta_time, target - floater_left);
|
||||
}
|
||||
else {
|
||||
floater_left += std::min(dp_speed * delta_time, target - floater_left);
|
||||
}
|
||||
|
||||
if (abs(target - floater_left) < 1e-4f) {
|
||||
floater_left = target;
|
||||
}
|
||||
else {
|
||||
queue_update();
|
||||
}
|
||||
|
||||
floater->set_left(floater_left, Unit::Dp);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Toggle::set_checked(bool checked) {
|
||||
set_checked_internal(checked, false, false, false);
|
||||
}
|
||||
|
||||
bool Toggle::is_checked() const {
|
||||
return checked;
|
||||
}
|
||||
|
||||
void Toggle::add_checked_callback(std::function<void(bool)> callback) {
|
||||
checked_callbacks.emplace_back(callback);
|
||||
}
|
||||
};
|
38
src/ui/elements/ui_toggle.h
Normal file
38
src/ui/elements/ui_toggle.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_element.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class Toggle : public Element {
|
||||
protected:
|
||||
Element *floater;
|
||||
float floater_left = 0.0f;
|
||||
std::chrono::high_resolution_clock::duration last_time;
|
||||
std::list<std::function<void(bool)>> checked_callbacks;
|
||||
Style checked_style;
|
||||
Style hover_style;
|
||||
Style focus_style;
|
||||
Style checked_hover_style;
|
||||
Style checked_focus_style;
|
||||
Style disabled_style;
|
||||
Style checked_disabled_style;
|
||||
Style floater_checked_style;
|
||||
Style floater_disabled_style;
|
||||
Style floater_disabled_checked_style;
|
||||
bool checked = false;
|
||||
|
||||
void set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks);
|
||||
float floater_left_target() const;
|
||||
|
||||
// Element overrides.
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "Toggle"; }
|
||||
public:
|
||||
Toggle(Element *parent);
|
||||
void set_checked(bool checked);
|
||||
bool is_checked() const;
|
||||
void add_checked_callback(std::function<void(bool)> callback);
|
||||
};
|
||||
|
||||
} // namespace recompui
|
260
src/ui/elements/ui_types.h
Normal file
260
src/ui/elements/ui_types.h
Normal file
@@ -0,0 +1,260 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <variant>
|
||||
|
||||
namespace recompui {
|
||||
|
||||
constexpr std::string_view checked_state = "checked";
|
||||
constexpr std::string_view hover_state = "hover";
|
||||
constexpr std::string_view focus_state = "focus";
|
||||
constexpr std::string_view disabled_state = "disabled";
|
||||
|
||||
struct Color {
|
||||
uint8_t r = 255;
|
||||
uint8_t g = 255;
|
||||
uint8_t b = 255;
|
||||
uint8_t a = 255;
|
||||
};
|
||||
|
||||
enum class Cursor {
|
||||
None,
|
||||
Pointer
|
||||
};
|
||||
|
||||
// These two enums must be kept in sync with patches/recompui_event_structs.h!
|
||||
enum class EventType {
|
||||
None,
|
||||
Click,
|
||||
Focus,
|
||||
Hover,
|
||||
Enable,
|
||||
Drag,
|
||||
Text,
|
||||
Update,
|
||||
Navigate,
|
||||
Count
|
||||
};
|
||||
|
||||
enum class DragPhase {
|
||||
None,
|
||||
Start,
|
||||
Move,
|
||||
End
|
||||
};
|
||||
|
||||
enum class NavDirection {
|
||||
Up,
|
||||
Right,
|
||||
Down,
|
||||
Left
|
||||
};
|
||||
|
||||
template <typename Enum, typename = std::enable_if_t<std::is_enum_v<Enum>>>
|
||||
constexpr uint32_t Events(Enum first) {
|
||||
return 1u << static_cast<uint32_t>(first);
|
||||
}
|
||||
|
||||
template <typename Enum, typename... Enums, typename = std::enable_if_t<std::is_enum_v<Enum>>>
|
||||
constexpr uint32_t Events(Enum first, Enums... rest) {
|
||||
return Events(first) | Events(rest...);
|
||||
}
|
||||
|
||||
struct EventClick {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
struct EventFocus {
|
||||
bool active;
|
||||
};
|
||||
|
||||
struct EventHover {
|
||||
bool active;
|
||||
};
|
||||
|
||||
struct EventEnable {
|
||||
bool active;
|
||||
};
|
||||
|
||||
struct EventDrag {
|
||||
float x;
|
||||
float y;
|
||||
DragPhase phase;
|
||||
};
|
||||
|
||||
struct EventText {
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct EventNavigate {
|
||||
NavDirection direction;
|
||||
};
|
||||
|
||||
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, std::monostate>;
|
||||
|
||||
struct Event {
|
||||
EventType type;
|
||||
EventVariant variant;
|
||||
|
||||
// Factory methods for creating specific events
|
||||
static Event click_event(float x, float y) {
|
||||
Event e;
|
||||
e.type = EventType::Click;
|
||||
e.variant = EventClick{ x, y };
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event focus_event(bool active) {
|
||||
Event e;
|
||||
e.type = EventType::Focus;
|
||||
e.variant = EventFocus{ active };
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event hover_event(bool active) {
|
||||
Event e;
|
||||
e.type = EventType::Hover;
|
||||
e.variant = EventHover{ active };
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event enable_event(bool enable) {
|
||||
Event e;
|
||||
e.type = EventType::Enable;
|
||||
e.variant = EventEnable{ enable };
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event drag_event(float x, float y, DragPhase phase) {
|
||||
Event e;
|
||||
e.type = EventType::Drag;
|
||||
e.variant = EventDrag{ x, y, phase };
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event text_event(const std::string &text) {
|
||||
Event e;
|
||||
e.type = EventType::Text;
|
||||
e.variant = EventText{ text };
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event update_event() {
|
||||
Event e;
|
||||
e.type = EventType::Update;
|
||||
e.variant = std::monostate{};
|
||||
return e;
|
||||
}
|
||||
|
||||
static Event navigate_event(NavDirection direction) {
|
||||
Event e;
|
||||
e.type = EventType::Navigate;
|
||||
e.variant = EventNavigate{ direction };
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
enum class Display {
|
||||
None,
|
||||
Block,
|
||||
Inline,
|
||||
InlineBlock,
|
||||
FlowRoot,
|
||||
Flex,
|
||||
InlineFlex,
|
||||
Table,
|
||||
InlineTable,
|
||||
TableRow,
|
||||
TableRowGroup,
|
||||
TableColumn,
|
||||
TableColumnGroup,
|
||||
TableCell
|
||||
};
|
||||
|
||||
enum class Visibility {
|
||||
Visible,
|
||||
Hidden
|
||||
};
|
||||
|
||||
enum class Position {
|
||||
Absolute,
|
||||
Relative
|
||||
};
|
||||
|
||||
enum class JustifyContent {
|
||||
FlexStart,
|
||||
FlexEnd,
|
||||
Center,
|
||||
SpaceBetween,
|
||||
SpaceAround,
|
||||
SpaceEvenly
|
||||
};
|
||||
|
||||
enum class FlexDirection {
|
||||
Row,
|
||||
Column,
|
||||
RowReverse,
|
||||
ColumnReverse
|
||||
};
|
||||
|
||||
enum class AlignItems {
|
||||
FlexStart,
|
||||
FlexEnd,
|
||||
Center,
|
||||
Baseline,
|
||||
Stretch
|
||||
};
|
||||
|
||||
enum class Overflow {
|
||||
Visible,
|
||||
Hidden,
|
||||
Auto,
|
||||
Scroll
|
||||
};
|
||||
|
||||
enum class Unit {
|
||||
Px,
|
||||
Dp,
|
||||
Percent
|
||||
};
|
||||
|
||||
enum class AnimationType : uint32_t {
|
||||
None,
|
||||
Set,
|
||||
Tween
|
||||
};
|
||||
|
||||
enum class FontStyle {
|
||||
Normal,
|
||||
Italic
|
||||
};
|
||||
|
||||
enum class TextAlign {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
Justify
|
||||
};
|
||||
|
||||
enum class TextTransform {
|
||||
None,
|
||||
Capitalize,
|
||||
Uppercase,
|
||||
Lowercase
|
||||
};
|
||||
|
||||
enum class Drag {
|
||||
None,
|
||||
Drag,
|
||||
DragDrop,
|
||||
Block,
|
||||
Clone
|
||||
};
|
||||
|
||||
enum class TabIndex {
|
||||
None,
|
||||
Auto
|
||||
};
|
||||
|
||||
} // namespace recompui
|
1031
src/ui/ui_api.cpp
Normal file
1031
src/ui/ui_api.cpp
Normal file
File diff suppressed because it is too large
Load Diff
118
src/ui/ui_api_events.cpp
Normal file
118
src/ui/ui_api_events.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "concurrentqueue.h"
|
||||
|
||||
#include "overloaded.h"
|
||||
#include "recomp_ui.h"
|
||||
|
||||
#include "core/ui_context.h"
|
||||
#include "core/ui_resource.h"
|
||||
|
||||
#include "elements/ui_element.h"
|
||||
#include "elements/ui_button.h"
|
||||
#include "elements/ui_clickable.h"
|
||||
#include "elements/ui_container.h"
|
||||
#include "elements/ui_image.h"
|
||||
#include "elements/ui_label.h"
|
||||
#include "elements/ui_radio.h"
|
||||
#include "elements/ui_scroll_container.h"
|
||||
#include "elements/ui_slider.h"
|
||||
#include "elements/ui_style.h"
|
||||
#include "elements/ui_text_input.h"
|
||||
#include "elements/ui_toggle.h"
|
||||
#include "elements/ui_types.h"
|
||||
|
||||
#include "librecomp/overlays.hpp"
|
||||
#include "librecomp/helpers.hpp"
|
||||
|
||||
#include "../patches/ui_funcs.h"
|
||||
|
||||
struct QueuedCallback {
|
||||
recompui::ResourceId resource;
|
||||
recompui::Event event;
|
||||
recompui::UICallback callback;
|
||||
};
|
||||
|
||||
moodycamel::ConcurrentQueue<QueuedCallback> queued_callbacks{};
|
||||
|
||||
void recompui::queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback) {
|
||||
queued_callbacks.enqueue(QueuedCallback{ .resource = resource, .event = e, .callback = callback });
|
||||
}
|
||||
|
||||
bool convert_event(const recompui::Event& in, RecompuiEventData& out) {
|
||||
bool skip = false;
|
||||
out = {};
|
||||
out.type = static_cast<RecompuiEventType>(in.type);
|
||||
|
||||
switch (in.type) {
|
||||
default:
|
||||
case recompui::EventType::None:
|
||||
case recompui::EventType::Count:
|
||||
skip = true;
|
||||
break;
|
||||
case recompui::EventType::Click:
|
||||
{
|
||||
const recompui::EventClick &click = std::get<recompui::EventClick>(in.variant);
|
||||
out.data.click.x = click.x;
|
||||
out.data.click.y = click.y;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Focus:
|
||||
{
|
||||
const recompui::EventFocus &focus = std::get<recompui::EventFocus>(in.variant);
|
||||
out.data.focus.active = focus.active;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Hover:
|
||||
{
|
||||
const recompui::EventHover &hover = std::get<recompui::EventHover>(in.variant);
|
||||
out.data.hover.active = hover.active;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Enable:
|
||||
{
|
||||
const recompui::EventEnable &enable = std::get<recompui::EventEnable>(in.variant);
|
||||
out.data.enable.active = enable.active;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Drag:
|
||||
{
|
||||
const recompui::EventDrag &drag = std::get<recompui::EventDrag>(in.variant);
|
||||
out.data.drag.phase = static_cast<RecompuiDragPhase>(drag.phase);
|
||||
out.data.drag.x = drag.x;
|
||||
out.data.drag.y = drag.y;
|
||||
}
|
||||
break;
|
||||
case recompui::EventType::Text:
|
||||
skip = true; // Text events aren't supported in the UI mod API.
|
||||
break;
|
||||
case recompui::EventType::Update:
|
||||
// No data for an update event.
|
||||
break;
|
||||
}
|
||||
|
||||
return !skip;
|
||||
}
|
||||
|
||||
extern "C" void recomp_run_ui_callbacks(uint8_t* rdram, recomp_context* ctx) {
|
||||
// Allocate the event on the stack.
|
||||
gpr stack_frame = ctx->r29;
|
||||
ctx->r29 -= sizeof(RecompuiEventData);
|
||||
RecompuiEventData* event_data = TO_PTR(RecompuiEventData, stack_frame);
|
||||
|
||||
QueuedCallback cur_callback;
|
||||
|
||||
while (queued_callbacks.try_dequeue(cur_callback)) {
|
||||
if (convert_event(cur_callback.event, *event_data)) {
|
||||
recompui::ContextId cur_context = cur_callback.callback.context;
|
||||
cur_context.open();
|
||||
|
||||
ctx->r4 = static_cast<int32_t>(cur_callback.resource.slot_id);
|
||||
ctx->r5 = stack_frame;
|
||||
ctx->r6 = cur_callback.callback.userdata;
|
||||
|
||||
LOOKUP_FUNC(cur_callback.callback.callback)(rdram, ctx);
|
||||
cur_context.close();
|
||||
}
|
||||
}
|
||||
|
||||
ctx->r29 += sizeof(RecompuiEventData);
|
||||
}
|
131
src/ui/ui_api_images.cpp
Normal file
131
src/ui/ui_api_images.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "recomp_ui.h"
|
||||
#include "librecomp/overlays.hpp"
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "ultramodern/error_handling.hpp"
|
||||
|
||||
#include "ui_helpers.h"
|
||||
#include "ui_api_images.h"
|
||||
#include "elements/ui_image.h"
|
||||
|
||||
using namespace recompui;
|
||||
|
||||
struct {
|
||||
std::mutex mutex;
|
||||
std::unordered_set<uint32_t> textures{};
|
||||
uint32_t textures_created = 0;
|
||||
} TextureState;
|
||||
|
||||
const std::string mod_texture_prefix = "?/mod_api/";
|
||||
|
||||
static std::string get_texture_name(uint32_t texture_id) {
|
||||
return mod_texture_prefix + std::to_string(texture_id);
|
||||
}
|
||||
|
||||
static uint32_t get_new_texture_id() {
|
||||
std::lock_guard lock{TextureState.mutex};
|
||||
uint32_t cur_id = TextureState.textures_created++;
|
||||
TextureState.textures.emplace(cur_id);
|
||||
|
||||
return cur_id;
|
||||
}
|
||||
|
||||
static void release_texture(uint32_t texture_id) {
|
||||
std::string texture_name = get_texture_name(texture_id);
|
||||
std::lock_guard lock{TextureState.mutex};
|
||||
|
||||
if (TextureState.textures.erase(texture_id) == 0) {
|
||||
recompui::message_box("Fatal error in mod - attempted to destroy texture that doesn't exist!");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
recompui::release_image(texture_name);
|
||||
}
|
||||
|
||||
thread_local std::vector<char> swapped_image_bytes;
|
||||
|
||||
void recompui_create_texture_rgba32(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
|
||||
uint32_t width = _arg<1, uint32_t>(rdram, ctx);
|
||||
uint32_t height = _arg<2, uint32_t>(rdram, ctx);
|
||||
uint32_t cur_id = get_new_texture_id();
|
||||
|
||||
// The size in bytes of the image's pixel data.
|
||||
size_t size_bytes = width * height * 4 * sizeof(uint8_t);
|
||||
swapped_image_bytes.resize(size_bytes);
|
||||
|
||||
// Byteswap copy the pixel data.
|
||||
for (size_t i = 0; i < size_bytes; i++) {
|
||||
swapped_image_bytes[i] = MEM_B(i, data_in);
|
||||
}
|
||||
|
||||
// Create a texture name from the ID and queue its bytes.
|
||||
std::string texture_name = get_texture_name(cur_id);
|
||||
recompui::queue_image_from_bytes_rgba32(texture_name, swapped_image_bytes, width, height);
|
||||
|
||||
// Return the new texture ID.
|
||||
_return(ctx, cur_id);
|
||||
}
|
||||
|
||||
void recompui_create_texture_image_bytes(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
|
||||
uint32_t size_bytes = _arg<1, u32>(rdram, ctx);
|
||||
uint32_t cur_id = get_new_texture_id();
|
||||
|
||||
// The size in bytes of the image's data.
|
||||
swapped_image_bytes.resize(size_bytes);
|
||||
|
||||
// Byteswap copy the image's data.
|
||||
for (size_t i = 0; i < size_bytes; i++) {
|
||||
swapped_image_bytes[i] = MEM_B(i, data_in);
|
||||
}
|
||||
|
||||
// Create a texture name from the ID and queue its bytes.
|
||||
std::string texture_name = get_texture_name(cur_id);
|
||||
recompui::queue_image_from_bytes_file(texture_name, swapped_image_bytes);
|
||||
|
||||
// Return the new texture ID.
|
||||
_return(ctx, cur_id);
|
||||
}
|
||||
|
||||
void recompui_destroy_texture(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t texture_id = _arg<0, uint32_t>(rdram, ctx);
|
||||
|
||||
release_texture(texture_id);
|
||||
}
|
||||
|
||||
void recompui_create_imageview(uint8_t* rdram, recomp_context* ctx) {
|
||||
ContextId ui_context = get_context(rdram, ctx);
|
||||
Element* parent = arg_element<1>(rdram, ctx, ui_context);
|
||||
uint32_t texture_id = _arg<2, uint32_t>(rdram, ctx);
|
||||
|
||||
Element* ret = ui_context.create_element<Image>(parent, get_texture_name(texture_id));
|
||||
return_resource(ctx, ret->get_resource_id());
|
||||
}
|
||||
|
||||
void recompui_set_imageview_texture(uint8_t* rdram, recomp_context* ctx) {
|
||||
Style* resource = arg_style<0>(rdram, ctx);
|
||||
uint32_t texture_id = _arg<1, uint32_t>(rdram, ctx);
|
||||
|
||||
if (!resource->is_element()) {
|
||||
recompui::message_box("Fatal error in mod - attempted to set texture of non-element");
|
||||
assert(false);
|
||||
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
|
||||
}
|
||||
|
||||
Element* element = static_cast<Element*>(resource);
|
||||
element->set_src(get_texture_name(texture_id));
|
||||
}
|
||||
|
||||
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
|
||||
|
||||
void recompui::register_ui_image_exports() {
|
||||
REGISTER_FUNC(recompui_create_texture_rgba32);
|
||||
REGISTER_FUNC(recompui_create_texture_image_bytes);
|
||||
REGISTER_FUNC(recompui_destroy_texture);
|
||||
REGISTER_FUNC(recompui_create_imageview);
|
||||
REGISTER_FUNC(recompui_set_imageview_texture);
|
||||
}
|
10
src/ui/ui_api_images.h
Normal file
10
src/ui/ui_api_images.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef __UI_API_IMAGES_H__
|
||||
#define __UI_API_IMAGES_H__
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace recompui {
|
||||
void register_ui_image_exports();
|
||||
}
|
||||
|
||||
#endif
|
@@ -77,7 +77,7 @@ namespace recompui {
|
||||
}
|
||||
|
||||
// Parse each of the colour elements.
|
||||
for (int i = 0; i < 4; i++)
|
||||
for (size_t i = 0; i < 4; i++)
|
||||
{
|
||||
int tens = Rml::Math::HexToDecimal(hex_values[i][0]);
|
||||
int ones = Rml::Math::HexToDecimal(hex_values[i][1]);
|
||||
@@ -115,7 +115,7 @@ namespace recompui {
|
||||
}
|
||||
|
||||
// Parse the three RGB values.
|
||||
for (int i = 0; i < 3; ++i)
|
||||
for (size_t i = 0; i < 3; ++i)
|
||||
{
|
||||
int component;
|
||||
|
||||
|
@@ -10,6 +10,8 @@
|
||||
#include "ultramodern/ultramodern.hpp"
|
||||
#include "RmlUi/Core.h"
|
||||
|
||||
#include "core/ui_context.h"
|
||||
|
||||
ultramodern::renderer::GraphicsConfig new_options;
|
||||
Rml::DataModelHandle nav_help_model_handle;
|
||||
Rml::DataModelHandle general_model_handle;
|
||||
@@ -17,90 +19,29 @@ Rml::DataModelHandle controls_model_handle;
|
||||
Rml::DataModelHandle graphics_model_handle;
|
||||
Rml::DataModelHandle sound_options_model_handle;
|
||||
|
||||
recompui::PromptContext prompt_context;
|
||||
|
||||
namespace recompui {
|
||||
const std::unordered_map<ButtonVariant, std::string> button_variants {
|
||||
{ButtonVariant::Primary, "primary"},
|
||||
{ButtonVariant::Secondary, "secondary"},
|
||||
{ButtonVariant::Tertiary, "tertiary"},
|
||||
{ButtonVariant::Success, "success"},
|
||||
{ButtonVariant::Error, "error"},
|
||||
{ButtonVariant::Warning, "warning"}
|
||||
};
|
||||
|
||||
void PromptContext::close_prompt() {
|
||||
open = false;
|
||||
model_handle.DirtyVariable("prompt__open");
|
||||
}
|
||||
|
||||
void PromptContext::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
|
||||
) {
|
||||
open = true;
|
||||
header = headerText;
|
||||
content = contentText;
|
||||
confirmLabel = confirmLabelText;
|
||||
cancelLabel = cancelLabelText;
|
||||
onConfirm = confirmCb;
|
||||
onCancel = cancelCb;
|
||||
confirmVariant = _confirmVariant;
|
||||
cancelVariant = _cancelVariant;
|
||||
focusOnCancel = _focusOnCancel;
|
||||
returnElementId = _returnElementId;
|
||||
|
||||
model_handle.DirtyVariable("prompt__open");
|
||||
model_handle.DirtyVariable("prompt__header");
|
||||
model_handle.DirtyVariable("prompt__content");
|
||||
model_handle.DirtyVariable("prompt__confirmLabel");
|
||||
model_handle.DirtyVariable("prompt__cancelLabel");
|
||||
shouldFocus = true;
|
||||
}
|
||||
|
||||
void PromptContext::on_confirm(void) {
|
||||
onConfirm();
|
||||
open = false;
|
||||
model_handle.DirtyVariable("prompt__open");
|
||||
}
|
||||
|
||||
void PromptContext::on_cancel(void) {
|
||||
onCancel();
|
||||
open = false;
|
||||
model_handle.DirtyVariable("prompt__open");
|
||||
}
|
||||
|
||||
void PromptContext::on_click(Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
|
||||
Rml::Element *target = event.GetTargetElement();
|
||||
auto id = target->GetId();
|
||||
if (id == "prompt__confirm-button" || id == "prompt__confirm-button-label") {
|
||||
on_confirm();
|
||||
event.StopPropagation();
|
||||
} else if (id == "prompt__cancel-button" || id == "prompt__cancel-button-label") {
|
||||
on_cancel();
|
||||
event.StopPropagation();
|
||||
}
|
||||
if (event.GetCurrentElement()->GetId() == "prompt-root") {
|
||||
event.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
PromptContext *get_prompt_context() {
|
||||
return &prompt_context;
|
||||
}
|
||||
};
|
||||
|
||||
// True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
|
||||
bool configuring_controller = false;
|
||||
|
||||
static int config_tab_to_index(recompui::ConfigTab tab) {
|
||||
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 = "";
|
||||
@@ -211,11 +152,18 @@ void recomp::config_menu_set_cont_or_kb(bool cont_interacted) {
|
||||
void close_config_menu_impl() {
|
||||
zelda64::save_config();
|
||||
|
||||
if (ultramodern::is_game_started()) {
|
||||
recompui::set_current_menu(recompui::Menu::None);
|
||||
recompui::ContextId config_context = recompui::get_config_context_id();
|
||||
recompui::ContextId sub_menu_context = recompui::get_config_sub_menu_context_id();
|
||||
|
||||
if (recompui::is_context_shown(sub_menu_context)) {
|
||||
recompui::hide_context(sub_menu_context);
|
||||
}
|
||||
else {
|
||||
recompui::set_current_menu(recompui::Menu::Launcher);
|
||||
recompui::hide_context(config_context);
|
||||
}
|
||||
|
||||
if (!ultramodern::is_game_started()) {
|
||||
recompui::show_context(recompui::get_launcher_context_id(), "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +185,7 @@ void apply_graphics_config(void) {
|
||||
|
||||
void close_config_menu() {
|
||||
if (ultramodern::renderer::get_graphics_config() != new_options) {
|
||||
prompt_context.open_prompt(
|
||||
recompui::open_choice_prompt(
|
||||
"Graphics options have changed",
|
||||
"Would you like to apply or discard the changes?",
|
||||
"Apply",
|
||||
@@ -264,7 +212,7 @@ void close_config_menu() {
|
||||
}
|
||||
|
||||
void zelda64::open_quit_game_prompt() {
|
||||
prompt_context.open_prompt(
|
||||
recompui::open_choice_prompt(
|
||||
"Are you sure you want to quit?",
|
||||
"Any progress since your last save will be lost.",
|
||||
"Quit",
|
||||
@@ -498,20 +446,53 @@ struct DebugContext {
|
||||
}
|
||||
};
|
||||
|
||||
void recompui::update_rml_display_refresh_rate() {
|
||||
static uint32_t lastRate = 0;
|
||||
if (!graphics_model_handle) return;
|
||||
|
||||
uint32_t curRate = ultramodern::get_display_refresh_rate();
|
||||
if (curRate != lastRate) {
|
||||
graphics_model_handle.DirtyVariable("display_refresh_rate");
|
||||
}
|
||||
lastRate = curRate;
|
||||
}
|
||||
|
||||
DebugContext debug_context;
|
||||
|
||||
recompui::ContextId config_context;
|
||||
|
||||
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 == 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() {
|
||||
|
||||
@@ -519,9 +500,10 @@ public:
|
||||
~ConfigMenu() override {
|
||||
|
||||
}
|
||||
Rml::ElementDocument* load_document(Rml::Context* context) override {
|
||||
const std::filesystem::path asset = zelda64::get_asset_path("config_menu.rml");
|
||||
return context->LoadDocument(asset.string());
|
||||
void load_document() override {
|
||||
config_context = recompui::create_context(zelda64::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",
|
||||
@@ -531,7 +513,7 @@ public:
|
||||
});
|
||||
recompui::register_event(listener, "config_keydown",
|
||||
[](const std::string& param, Rml::Event& event) {
|
||||
if (!prompt_context.open && event.GetId() == Rml::EventId::Keydown) {
|
||||
if (!recompui::is_prompt_open() && event.GetId() == Rml::EventId::Keydown) {
|
||||
auto key = event.GetParameter<Rml::Input::KeyIdentifier>("key_identifier", Rml::Input::KeyIdentifier::KI_UNKNOWN);
|
||||
switch (key) {
|
||||
case Rml::Input::KeyIdentifier::KI_ESCAPE:
|
||||
@@ -990,34 +972,15 @@ public:
|
||||
debug_context.model_handle = constructor.GetModelHandle();
|
||||
}
|
||||
|
||||
void make_prompt_bindings(Rml::Context* context) {
|
||||
Rml::DataModelConstructor constructor = context->CreateDataModel("prompt_model");
|
||||
if (!constructor) {
|
||||
throw std::runtime_error("Failed to make RmlUi data model for the prompt");
|
||||
}
|
||||
|
||||
// Bind the debug mode enabled flag.
|
||||
constructor.Bind("prompt__open", &prompt_context.open);
|
||||
constructor.Bind("prompt__header", &prompt_context.header);
|
||||
constructor.Bind("prompt__content", &prompt_context.content);
|
||||
constructor.Bind("prompt__confirmLabel", &prompt_context.confirmLabel);
|
||||
constructor.Bind("prompt__cancelLabel", &prompt_context.cancelLabel);
|
||||
|
||||
constructor.BindEventCallback("prompt__on_click", &recompui::PromptContext::on_click, &prompt_context);
|
||||
|
||||
prompt_context.model_handle = constructor.GetModelHandle();
|
||||
}
|
||||
|
||||
void make_bindings(Rml::Context* context) override {
|
||||
// initially set cont state for ui help
|
||||
recomp::config_menu_set_cont_or_kb(recompui::get_cont_active());
|
||||
//recomp::config_menu_set_cont_or_kb(recompui::get_cont_active());
|
||||
make_nav_help_bindings(context);
|
||||
make_general_bindings(context);
|
||||
make_controls_bindings(context);
|
||||
make_graphics_bindings(context);
|
||||
make_sound_options_bindings(context);
|
||||
make_debug_bindings(context);
|
||||
make_prompt_bindings(context);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1052,3 +1015,46 @@ void recompui::toggle_fullscreen() {
|
||||
apply_graphics_config();
|
||||
graphics_model_handle.DirtyVariable("wm_option");
|
||||
}
|
||||
|
||||
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* tab_el = doc->GetElementById("tab_mods");
|
||||
assert(tab_el != nullptr);
|
||||
|
||||
if (old_context != ContextId::null()) {
|
||||
old_context.open();
|
||||
}
|
||||
|
||||
return tab_el;
|
||||
}
|
||||
|
250
src/ui/ui_config_sub_menu.cpp
Normal file
250
src/ui/ui_config_sub_menu.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
#include "ui_config_sub_menu.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <string_view>
|
||||
|
||||
#include "recomp_ui.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
// ConfigOptionElement
|
||||
|
||||
|
||||
void ConfigOptionElement::process_event(const Event &e) {
|
||||
switch (e.type) {
|
||||
case EventType::Hover:
|
||||
hover_callback(this, std::get<EventHover>(e.variant).active);
|
||||
break;
|
||||
case EventType::Update:
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown event type.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigOptionElement::ConfigOptionElement(Element *parent) : Element(parent, Events(EventType::Hover)) {
|
||||
set_display(Display::Flex);
|
||||
set_flex_direction(FlexDirection::Column);
|
||||
set_gap(16.0f);
|
||||
set_height(100.0f);
|
||||
|
||||
name_label = get_current_context().create_element<Label>(this, LabelStyle::Normal);
|
||||
}
|
||||
|
||||
ConfigOptionElement::~ConfigOptionElement() {
|
||||
|
||||
}
|
||||
|
||||
void ConfigOptionElement::set_option_id(std::string_view id) {
|
||||
this->option_id = id;
|
||||
}
|
||||
|
||||
void ConfigOptionElement::set_name(std::string_view name) {
|
||||
this->name = name;
|
||||
name_label->set_text(name);
|
||||
}
|
||||
|
||||
void ConfigOptionElement::set_description(std::string_view description) {
|
||||
this->description = description;
|
||||
}
|
||||
|
||||
void ConfigOptionElement::set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback) {
|
||||
hover_callback = callback;
|
||||
}
|
||||
|
||||
const std::string &ConfigOptionElement::get_description() const {
|
||||
return description;
|
||||
}
|
||||
|
||||
// ConfigOptionSlider
|
||||
|
||||
void ConfigOptionSlider::slider_value_changed(double v) {
|
||||
callback(option_id, v);
|
||||
}
|
||||
|
||||
ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback) : ConfigOptionElement(parent) {
|
||||
this->callback = callback;
|
||||
|
||||
slider = get_current_context().create_element<Slider>(this, percent ? SliderType::Percent : SliderType::Double);
|
||||
slider->set_max_width(380.0f);
|
||||
slider->set_min_value(min_value);
|
||||
slider->set_max_value(max_value);
|
||||
slider->set_step_value(step_value);
|
||||
slider->set_value(value);
|
||||
slider->add_value_changed_callback([this](double v){ slider_value_changed(v); });
|
||||
}
|
||||
|
||||
// ConfigOptionTextInput
|
||||
|
||||
void ConfigOptionTextInput::text_changed(const std::string &text) {
|
||||
callback(option_id, text);
|
||||
}
|
||||
|
||||
ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) : ConfigOptionElement(parent) {
|
||||
this->callback = callback;
|
||||
|
||||
text_input = get_current_context().create_element<TextInput>(this);
|
||||
text_input->set_max_width(400.0f);
|
||||
text_input->set_text(value);
|
||||
text_input->add_text_changed_callback([this](const std::string &text){ text_changed(text); });
|
||||
}
|
||||
|
||||
// ConfigOptionRadio
|
||||
|
||||
void ConfigOptionRadio::index_changed(uint32_t index) {
|
||||
callback(option_id, index);
|
||||
}
|
||||
|
||||
ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) : ConfigOptionElement(parent) {
|
||||
this->callback = callback;
|
||||
|
||||
radio = get_current_context().create_element<Radio>(this);
|
||||
radio->add_index_changed_callback([this](uint32_t index){ index_changed(index); });
|
||||
for (std::string_view option : options) {
|
||||
radio->add_option(option);
|
||||
}
|
||||
|
||||
if (value < options.size()) {
|
||||
radio->set_index(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigSubMenu
|
||||
|
||||
void ConfigSubMenu::back_button_pressed() {
|
||||
// Hide the config sub menu and show the config menu.
|
||||
ContextId config_context = recompui::get_config_context_id();
|
||||
ContextId sub_menu_context = recompui::get_config_sub_menu_context_id();
|
||||
|
||||
recompui::hide_context(sub_menu_context);
|
||||
recompui::show_context(config_context, "");
|
||||
recompui::focus_mod_configure_button();
|
||||
}
|
||||
|
||||
void ConfigSubMenu::option_hovered(ConfigOptionElement *option, bool active) {
|
||||
if (active) {
|
||||
hover_option_elements.emplace(option);
|
||||
}
|
||||
else {
|
||||
hover_option_elements.erase(option);
|
||||
}
|
||||
|
||||
if (hover_option_elements.empty()) {
|
||||
description_label->set_text("");
|
||||
}
|
||||
else {
|
||||
description_label->set_text((*hover_option_elements.begin())->get_description());
|
||||
}
|
||||
}
|
||||
|
||||
ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
set_display(Display::Flex);
|
||||
set_flex(1, 1, 100.0f, Unit::Percent);
|
||||
set_flex_direction(FlexDirection::Column);
|
||||
set_height(100.0f, Unit::Percent);
|
||||
|
||||
recompui::ContextId context = get_current_context();
|
||||
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
|
||||
header_container->set_flex_grow(0.0f);
|
||||
header_container->set_align_items(AlignItems::Center);
|
||||
header_container->set_padding(12.0f);
|
||||
header_container->set_gap(24.0f);
|
||||
|
||||
{
|
||||
back_button = context.create_element<Button>(header_container, "Back", ButtonStyle::Secondary);
|
||||
back_button->add_pressed_callback([this](){ back_button_pressed(); });
|
||||
title_label = context.create_element<Label>(header_container, "Title", LabelStyle::Large);
|
||||
}
|
||||
|
||||
body_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceEvenly);
|
||||
body_container->set_padding(32.0f);
|
||||
{
|
||||
config_container = context.create_element<Container>(body_container, FlexDirection::Column, JustifyContent::Center);
|
||||
config_container->set_display(Display::Block);
|
||||
config_container->set_flex_basis(100.0f);
|
||||
config_container->set_align_items(AlignItems::Center);
|
||||
{
|
||||
config_scroll_container = context.create_element<ScrollContainer>(config_container, ScrollDirection::Vertical);
|
||||
}
|
||||
|
||||
description_label = context.create_element<Label>(body_container, "Description", LabelStyle::Small);
|
||||
description_label->set_min_width(800.0f);
|
||||
}
|
||||
|
||||
recompui::get_current_context().set_autofocus_element(back_button);
|
||||
}
|
||||
|
||||
ConfigSubMenu::~ConfigSubMenu() {
|
||||
|
||||
}
|
||||
|
||||
void ConfigSubMenu::enter(std::string_view title) {
|
||||
title_label->set_text(title);
|
||||
}
|
||||
|
||||
void ConfigSubMenu::clear_options() {
|
||||
config_scroll_container->clear_children();
|
||||
config_option_elements.clear();
|
||||
hover_option_elements.clear();
|
||||
}
|
||||
|
||||
void ConfigSubMenu::add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description) {
|
||||
option->set_option_id(id);
|
||||
option->set_name(name);
|
||||
option->set_description(description);
|
||||
option->set_hover_callback([this](ConfigOptionElement *option, bool active){ option_hovered(option, active); });
|
||||
if (config_option_elements.empty()) {
|
||||
back_button->set_nav(NavDirection::Down, option->get_focus_element());
|
||||
option->set_nav(NavDirection::Up, back_button);
|
||||
}
|
||||
else {
|
||||
config_option_elements.back()->set_nav(NavDirection::Down, option->get_focus_element());
|
||||
option->set_nav(NavDirection::Up, config_option_elements.back()->get_focus_element());
|
||||
}
|
||||
|
||||
config_option_elements.emplace_back(option);
|
||||
}
|
||||
|
||||
void ConfigSubMenu::add_slider_option(std::string_view id, std::string_view name, std::string_view description, double value, double min, double max, double step, bool percent, std::function<void(const std::string &, double)> callback) {
|
||||
ConfigOptionSlider *option_slider = get_current_context().create_element<ConfigOptionSlider>(config_scroll_container, value, min, max, step, percent, callback);
|
||||
add_option(option_slider, id, name, description);
|
||||
}
|
||||
|
||||
void ConfigSubMenu::add_text_option(std::string_view id, std::string_view name, std::string_view description, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) {
|
||||
ConfigOptionTextInput *option_text_input = get_current_context().create_element<ConfigOptionTextInput>(config_scroll_container, value, callback);
|
||||
add_option(option_text_input, id, name, description);
|
||||
}
|
||||
|
||||
void ConfigSubMenu::add_radio_option(std::string_view id, std::string_view name, std::string_view description, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) {
|
||||
ConfigOptionRadio *option_radio = get_current_context().create_element<ConfigOptionRadio>(config_scroll_container, value, options, callback);
|
||||
add_option(option_radio, id, name, description);
|
||||
}
|
||||
|
||||
// ElementConfigSubMenu
|
||||
|
||||
ElementConfigSubMenu::ElementConfigSubMenu(const Rml::String &tag) : Rml::Element(tag) {
|
||||
SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Flex);
|
||||
SetProperty("width", "100%");
|
||||
SetProperty("height", "100%");
|
||||
|
||||
recompui::Element this_compat(this);
|
||||
recompui::ContextId context = get_current_context();
|
||||
config_sub_menu = context.create_element<ConfigSubMenu>(&this_compat);
|
||||
}
|
||||
|
||||
ElementConfigSubMenu::~ElementConfigSubMenu() {
|
||||
|
||||
}
|
||||
|
||||
void ElementConfigSubMenu::set_display(bool display) {
|
||||
SetProperty(Rml::PropertyId::Display, display ? Rml::Style::Display::Block : Rml::Style::Display::None);
|
||||
}
|
||||
|
||||
ConfigSubMenu *ElementConfigSubMenu::get_config_sub_menu_element() const {
|
||||
return config_sub_menu;
|
||||
}
|
||||
|
||||
}
|
115
src/ui/ui_config_sub_menu.h
Normal file
115
src/ui/ui_config_sub_menu.h
Normal file
@@ -0,0 +1,115 @@
|
||||
#ifndef RECOMPUI_CONFIG_SUB_MENU_H
|
||||
#define RECOMPUI_CONFIG_SUB_MENU_H
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "elements/ui_button.h"
|
||||
#include "elements/ui_container.h"
|
||||
#include "elements/ui_label.h"
|
||||
#include "elements/ui_radio.h"
|
||||
#include "elements/ui_scroll_container.h"
|
||||
#include "elements/ui_slider.h"
|
||||
#include "elements/ui_text_input.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class ConfigOptionElement : public Element {
|
||||
protected:
|
||||
Label *name_label = nullptr;
|
||||
std::string option_id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::function<void(ConfigOptionElement *, bool)> hover_callback = nullptr;
|
||||
|
||||
virtual void process_event(const Event &e) override;
|
||||
std::string_view get_type_name() override { return "ConfigOptionElement"; }
|
||||
public:
|
||||
ConfigOptionElement(Element *parent);
|
||||
virtual ~ConfigOptionElement();
|
||||
void set_option_id(std::string_view id);
|
||||
void set_name(std::string_view name);
|
||||
void set_description(std::string_view description);
|
||||
void set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback);
|
||||
const std::string &get_description() const;
|
||||
void set_nav_auto(NavDirection dir) override { get_focus_element()->set_nav_auto(dir); }
|
||||
void set_nav_none(NavDirection dir) override { get_focus_element()->set_nav_none(dir); }
|
||||
void set_nav(NavDirection dir, Element* element) override { get_focus_element()->set_nav(dir, element); }
|
||||
void set_nav_manual(NavDirection dir, const std::string& target) override { get_focus_element()->set_nav_manual(dir, target); }
|
||||
virtual Element* get_focus_element() { return this; }
|
||||
};
|
||||
|
||||
class ConfigOptionSlider : public ConfigOptionElement {
|
||||
protected:
|
||||
Slider *slider = nullptr;
|
||||
std::function<void(const std::string &, double)> callback;
|
||||
|
||||
void slider_value_changed(double v);
|
||||
std::string_view get_type_name() override { return "ConfigOptionSlider"; }
|
||||
public:
|
||||
ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback);
|
||||
Element* get_focus_element() override { return slider; }
|
||||
};
|
||||
|
||||
class ConfigOptionTextInput : public ConfigOptionElement {
|
||||
protected:
|
||||
TextInput *text_input = nullptr;
|
||||
std::function<void(const std::string &, const std::string &)> callback;
|
||||
|
||||
void text_changed(const std::string &text);
|
||||
std::string_view get_type_name() override { return "ConfigOptionTextInput"; }
|
||||
public:
|
||||
ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
|
||||
Element* get_focus_element() override { return text_input; }
|
||||
};
|
||||
|
||||
class ConfigOptionRadio : public ConfigOptionElement {
|
||||
protected:
|
||||
Radio *radio = nullptr;
|
||||
std::function<void(const std::string &, uint32_t)> callback;
|
||||
|
||||
void index_changed(uint32_t index);
|
||||
std::string_view get_type_name() override { return "ConfigOptionRadio"; }
|
||||
public:
|
||||
ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
|
||||
Element* get_focus_element() override { return radio; }
|
||||
};
|
||||
|
||||
class ConfigSubMenu : public Element {
|
||||
private:
|
||||
Container *header_container = nullptr;
|
||||
Button *back_button = nullptr;
|
||||
Label *title_label = nullptr;
|
||||
Container *body_container = nullptr;
|
||||
Label *description_label = nullptr;
|
||||
Container *config_container = nullptr;
|
||||
ScrollContainer *config_scroll_container = nullptr;
|
||||
std::vector<ConfigOptionElement *> config_option_elements;
|
||||
std::unordered_set<ConfigOptionElement *> hover_option_elements;
|
||||
|
||||
void back_button_pressed();
|
||||
void option_hovered(ConfigOptionElement *option, bool active);
|
||||
void add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description);
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ConfigSubMenu"; }
|
||||
public:
|
||||
ConfigSubMenu(Element *parent);
|
||||
virtual ~ConfigSubMenu();
|
||||
void enter(std::string_view title);
|
||||
void clear_options();
|
||||
void add_slider_option(std::string_view id, std::string_view name, std::string_view description, double value, double min, double max, double step, bool percent, std::function<void(const std::string &, double)> callback);
|
||||
void add_text_option(std::string_view id, std::string_view name, std::string_view description, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
|
||||
void add_radio_option(std::string_view id, std::string_view name, std::string_view description, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
|
||||
};
|
||||
|
||||
class ElementConfigSubMenu : public Rml::Element {
|
||||
public:
|
||||
ElementConfigSubMenu(const Rml::String &tag);
|
||||
virtual ~ElementConfigSubMenu();
|
||||
void set_display(bool display);
|
||||
ConfigSubMenu *get_config_sub_menu_element() const;
|
||||
private:
|
||||
ConfigSubMenu *config_sub_menu;
|
||||
};
|
||||
|
||||
}
|
||||
#endif
|
42
src/ui/ui_elements.cpp
Normal file
42
src/ui/ui_elements.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "ui_elements.h"
|
||||
|
||||
struct RecompCustomElement {
|
||||
Rml::String tag;
|
||||
std::unique_ptr<Rml::ElementInstancer> instancer;
|
||||
};
|
||||
|
||||
#define CUSTOM_ELEMENT(s, e) { s, std::make_unique< Rml::ElementInstancerGeneric< e > >() }
|
||||
|
||||
static RecompCustomElement custom_elements[] = {
|
||||
CUSTOM_ELEMENT("recomp-mod-menu", recompui::ElementModMenu),
|
||||
CUSTOM_ELEMENT("recomp-config-sub-menu", recompui::ElementConfigSubMenu),
|
||||
};
|
||||
|
||||
void recompui::register_custom_elements() {
|
||||
for (auto& element_config : custom_elements) {
|
||||
Rml::Factory::RegisterElementInstancer(element_config.tag, element_config.instancer.get());
|
||||
}
|
||||
}
|
||||
|
||||
Rml::ElementInstancer* recompui::get_custom_element_instancer(std::string tag) {
|
||||
for (auto& element_config : custom_elements) {
|
||||
if (tag == element_config.tag) {
|
||||
return element_config.instancer.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Rml::ElementPtr recompui::create_custom_element(Rml::Element* parent, std::string tag) {
|
||||
auto instancer = recompui::get_custom_element_instancer(tag);
|
||||
const Rml::XMLAttributes attributes = {};
|
||||
if (Rml::ElementPtr element = instancer->InstanceElement(parent, tag, attributes))
|
||||
{
|
||||
element->SetInstancer(instancer);
|
||||
element->SetAttributes(attributes);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
16
src/ui/ui_elements.h
Normal file
16
src/ui/ui_elements.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef RECOMPUI_ELEMENTS_H
|
||||
#define RECOMPUI_ELEMENTS_H
|
||||
|
||||
#include "recomp_ui.h"
|
||||
#include "RmlUi/Core/Element.h"
|
||||
|
||||
#include "ui_mod_menu.h"
|
||||
#include "ui_config_sub_menu.h"
|
||||
|
||||
namespace recompui {
|
||||
void register_custom_elements();
|
||||
|
||||
Rml::ElementInstancer* get_custom_element_instancer(std::string tag);
|
||||
}
|
||||
|
||||
#endif
|
154
src/ui/ui_helpers.h
Normal file
154
src/ui/ui_helpers.h
Normal file
@@ -0,0 +1,154 @@
|
||||
#ifndef __UI_HELPERS_H__
|
||||
#define __UI_HELPERS_H__
|
||||
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "librecomp/addresses.hpp"
|
||||
|
||||
#include "elements/ui_element.h"
|
||||
#include "elements/ui_types.h"
|
||||
#include "core/ui_context.h"
|
||||
#include "core/ui_resource.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
|
||||
|
||||
inline ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
|
||||
return ContextId{ .slot_id = context_id };
|
||||
}
|
||||
|
||||
inline float arg_float2(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = _arg<2, uint32_t>(rdram, ctx);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float3(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = _arg<3, uint32_t>(rdram, ctx);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float4(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = MEM_W(0x10, ctx->r29);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float5(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = MEM_W(0x14, ctx->r29);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
inline float arg_float6(uint8_t* rdram, recomp_context* ctx) {
|
||||
union {
|
||||
float f32;
|
||||
uint32_t u32;
|
||||
} val;
|
||||
|
||||
val.u32 = MEM_W(0x18, ctx->r29);
|
||||
return val.f32;
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
|
||||
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
|
||||
|
||||
return ResourceId{ .slot_id = slot_id };
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
|
||||
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
|
||||
|
||||
if (resource == ResourceId::null()) {
|
||||
return nullptr;
|
||||
}
|
||||
else if (resource == root_element_id) {
|
||||
return ui_context.get_root_element();
|
||||
}
|
||||
|
||||
return resource.as_element();
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
|
||||
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
|
||||
|
||||
if (resource == ResourceId::null()) {
|
||||
return nullptr;
|
||||
}
|
||||
else if (resource == root_element_id) {
|
||||
ContextId ui_context = recompui::get_current_context();
|
||||
return ui_context.get_root_element();
|
||||
}
|
||||
|
||||
return *resource;
|
||||
}
|
||||
|
||||
template <int arg_index>
|
||||
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
|
||||
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
|
||||
|
||||
Color ret{};
|
||||
|
||||
ret.r = MEM_B(0, color_arg);
|
||||
ret.g = MEM_B(1, color_arg);
|
||||
ret.b = MEM_B(2, color_arg);
|
||||
ret.a = MEM_B(3, color_arg);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline void return_resource(recomp_context* ctx, ResourceId resource) {
|
||||
_return<uint32_t>(ctx, resource.slot_id);
|
||||
}
|
||||
|
||||
inline void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) {
|
||||
gpr addr = (reinterpret_cast<uint8_t*>(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL;
|
||||
|
||||
for (size_t i = 0; i < ret.size(); i++) {
|
||||
MEM_B(i, addr) = ret[i];
|
||||
}
|
||||
MEM_B(ret.size(), addr) = '\x00';
|
||||
|
||||
_return<PTR(char)>(ctx, addr);
|
||||
}
|
||||
|
||||
inline std::string decode_string(uint8_t* rdram, PTR(char) str) {
|
||||
// Get the length of the byteswapped string.
|
||||
size_t len = 0;
|
||||
while (MEM_B(str, len) != 0x00) {
|
||||
len++;
|
||||
}
|
||||
|
||||
std::string ret{};
|
||||
ret.reserve(len + 1);
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
ret += (char)MEM_B(str, i);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -48,6 +48,12 @@ void select_rom() {
|
||||
});
|
||||
}
|
||||
|
||||
recompui::ContextId launcher_context;
|
||||
|
||||
recompui::ContextId recompui::get_launcher_context_id() {
|
||||
return launcher_context;
|
||||
}
|
||||
|
||||
class LauncherMenu : public recompui::MenuController {
|
||||
public:
|
||||
LauncherMenu() {
|
||||
@@ -56,9 +62,8 @@ public:
|
||||
~LauncherMenu() override {
|
||||
|
||||
}
|
||||
Rml::ElementDocument* load_document(Rml::Context* context) override {
|
||||
const std::filesystem::path asset = zelda64::get_asset_path("launcher.rml");
|
||||
return context->LoadDocument(asset.string());
|
||||
void load_document() override {
|
||||
launcher_context = recompui::create_context(zelda64::get_asset_path("launcher.rml"));
|
||||
}
|
||||
void register_events(recompui::UiEventListenerInstancer& listener) override {
|
||||
recompui::register_event(listener, "select_rom",
|
||||
@@ -75,19 +80,28 @@ public:
|
||||
recompui::register_event(listener, "start_game",
|
||||
[](const std::string& param, Rml::Event& event) {
|
||||
recomp::start_game(supported_games[0].game_id);
|
||||
recompui::set_current_menu(recompui::Menu::None);
|
||||
recompui::hide_all_contexts();
|
||||
}
|
||||
);
|
||||
recompui::register_event(listener, "open_controls",
|
||||
[](const std::string& param, Rml::Event& event) {
|
||||
recompui::set_current_menu(recompui::Menu::Config);
|
||||
recompui::set_config_submenu(recompui::ConfigSubmenu::Controls);
|
||||
recompui::set_config_tab(recompui::ConfigTab::Controls);
|
||||
recompui::hide_all_contexts();
|
||||
recompui::show_context(recompui::get_config_context_id(), "");
|
||||
}
|
||||
);
|
||||
recompui::register_event(listener, "open_settings",
|
||||
[](const std::string& param, Rml::Event& event) {
|
||||
recompui::set_current_menu(recompui::Menu::Config);
|
||||
recompui::set_config_submenu(recompui::ConfigSubmenu::General);
|
||||
recompui::set_config_tab(recompui::ConfigTab::General);
|
||||
recompui::hide_all_contexts();
|
||||
recompui::show_context(recompui::get_config_context_id(), "");
|
||||
}
|
||||
);
|
||||
recompui::register_event(listener, "open_mods",
|
||||
[](const std::string ¶m, Rml::Event &event) {
|
||||
recompui::set_config_tab(recompui::ConfigTab::Mods);
|
||||
recompui::hide_all_contexts();
|
||||
recompui::show_context(recompui::get_config_context_id(), "");
|
||||
}
|
||||
);
|
||||
recompui::register_event(listener, "exit_game",
|
||||
|
153
src/ui/ui_mod_details_panel.cpp
Normal file
153
src/ui/ui_mod_details_panel.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "ui_mod_details_panel.h"
|
||||
|
||||
#include "librecomp/mods.hpp"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
extern const std::string mod_tab_id;
|
||||
|
||||
ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
|
||||
set_flex(1.0f, 1.0f, 200.0f);
|
||||
set_height(100.0f, Unit::Percent);
|
||||
set_display(Display::Flex);
|
||||
set_flex_direction(FlexDirection::Column);
|
||||
set_background_color(Color{ 190, 184, 219, 25 });
|
||||
|
||||
ContextId context = get_current_context();
|
||||
|
||||
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
|
||||
header_container->set_flex(0.0f, 0.0f);
|
||||
header_container->set_padding(16.0f);
|
||||
header_container->set_gap(16.0f);
|
||||
header_container->set_background_color(Color{ 0, 0, 0, 89 });
|
||||
header_container->set_border_bottom_width(1.1f);
|
||||
header_container->set_border_bottom_color(Color{ 255, 255, 255, 25 });
|
||||
{
|
||||
thumbnail_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
|
||||
thumbnail_container->set_flex(0.0f, 0.0f);
|
||||
{
|
||||
thumbnail_image = context.create_element<Image>(thumbnail_container, "");
|
||||
thumbnail_image->set_width(100.0f);
|
||||
thumbnail_image->set_height(100.0f);
|
||||
thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 });
|
||||
}
|
||||
|
||||
header_details_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
|
||||
header_details_container->set_flex(1.0f, 1.0f);
|
||||
header_details_container->set_flex_basis(100.0f, Unit::Percent);
|
||||
header_details_container->set_text_align(TextAlign::Left);
|
||||
{
|
||||
title_label = context.create_element<Label>(header_details_container, LabelStyle::Large);
|
||||
version_label = context.create_element<Label>(header_details_container, LabelStyle::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
body_container = context.create_element<ScrollContainer>(this, ScrollDirection::Vertical);
|
||||
body_container->set_text_align(TextAlign::Left);
|
||||
body_container->set_padding(16.0f);
|
||||
{
|
||||
authors_label = context.create_element<Label>(body_container, LabelStyle::Normal);
|
||||
authors_label->set_margin_bottom(16.0f);
|
||||
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
|
||||
}
|
||||
|
||||
buttons_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceAround);
|
||||
buttons_container->set_flex(0.0f, 0.0f);
|
||||
buttons_container->set_padding(16.0f);
|
||||
buttons_container->set_justify_content(JustifyContent::SpaceBetween);
|
||||
buttons_container->set_border_top_width(1.1f);
|
||||
buttons_container->set_border_top_color(Color{ 255, 255, 255, 25 });
|
||||
buttons_container->set_background_color(Color{ 0, 0, 0, 89 });
|
||||
{
|
||||
enable_container = context.create_element<Container>(buttons_container, FlexDirection::Row, JustifyContent::FlexStart);
|
||||
enable_container->set_align_items(AlignItems::Center);
|
||||
enable_container->set_gap(16.0f);
|
||||
{
|
||||
enable_toggle = context.create_element<Toggle>(enable_container);
|
||||
enable_toggle->add_checked_callback([this](bool checked){ enable_toggle_checked(checked); });
|
||||
enable_toggle->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
|
||||
enable_label = context.create_element<Label>(enable_container, "A currently enabled mod requires this mod", LabelStyle::Annotation);
|
||||
}
|
||||
|
||||
configure_button = context.create_element<Button>(buttons_container, "Configure", recompui::ButtonStyle::Secondary);
|
||||
configure_button->add_pressed_callback([this](){ configure_button_pressed(); });
|
||||
configure_button->set_nav_manual(NavDirection::Up, mod_tab_id);
|
||||
}
|
||||
clear_mod_navigation();
|
||||
}
|
||||
|
||||
ModDetailsPanel::~ModDetailsPanel() {
|
||||
}
|
||||
|
||||
void ModDetailsPanel::disable_toggle() {
|
||||
enable_toggle->set_enabled(false);
|
||||
}
|
||||
|
||||
void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled) {
|
||||
cur_details = details;
|
||||
|
||||
thumbnail_image->set_src(thumbnail);
|
||||
|
||||
title_label->set_text(cur_details.display_name);
|
||||
version_label->set_text(cur_details.version.to_string());
|
||||
|
||||
std::string authors_str = "Authors:";
|
||||
bool first = true;
|
||||
for (const std::string& author : details.authors) {
|
||||
authors_str += (first ? " " : ", ") + author;
|
||||
first = false;
|
||||
}
|
||||
|
||||
authors_label->set_text(authors_str);
|
||||
description_label->set_text(cur_details.description);
|
||||
enable_toggle->set_checked(toggle_checked);
|
||||
enable_toggle->set_enabled(toggle_enabled);
|
||||
configure_button->set_enabled(configure_enabled);
|
||||
enable_label->set_display(toggle_label_visible ? Display::Block : Display::None);
|
||||
|
||||
if (configure_enabled) {
|
||||
enable_toggle->set_nav(NavDirection::Right, configure_button);
|
||||
}
|
||||
else {
|
||||
enable_toggle->set_nav_none(NavDirection::Right);
|
||||
}
|
||||
}
|
||||
|
||||
void ModDetailsPanel::set_mod_toggled_callback(std::function<void(bool)> callback) {
|
||||
mod_toggled_callback = callback;
|
||||
}
|
||||
|
||||
void ModDetailsPanel::set_mod_configure_pressed_callback(std::function<void()> callback) {
|
||||
mod_configure_pressed_callback = callback;
|
||||
}
|
||||
|
||||
void ModDetailsPanel::setup_mod_navigation(Element* nav_target) {
|
||||
enable_toggle->set_nav(NavDirection::Left, nav_target);
|
||||
|
||||
if (enable_toggle->is_enabled()) {
|
||||
configure_button->set_nav(NavDirection::Left, enable_toggle);
|
||||
}
|
||||
else {
|
||||
configure_button->set_nav(NavDirection::Left, nav_target);
|
||||
}
|
||||
}
|
||||
|
||||
void ModDetailsPanel::clear_mod_navigation() {
|
||||
enable_toggle->set_nav_none(NavDirection::Left);
|
||||
configure_button->set_nav_none(NavDirection::Left);
|
||||
}
|
||||
|
||||
void ModDetailsPanel::enable_toggle_checked(bool checked) {
|
||||
if (mod_toggled_callback != nullptr) {
|
||||
mod_toggled_callback(checked);
|
||||
}
|
||||
}
|
||||
|
||||
void ModDetailsPanel::configure_button_pressed() {
|
||||
if (mod_configure_pressed_callback != nullptr) {
|
||||
mod_configure_pressed_callback();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace recompui
|
52
src/ui/ui_mod_details_panel.h
Normal file
52
src/ui/ui_mod_details_panel.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef RECOMPUI_ELEMENT_MOD_DETAILS_PANEL_H
|
||||
#define RECOMPUI_ELEMENT_MOD_DETAILS_PANEL_H
|
||||
|
||||
#include "librecomp/mods.hpp"
|
||||
#include "elements/ui_button.h"
|
||||
#include "elements/ui_container.h"
|
||||
#include "elements/ui_image.h"
|
||||
#include "elements/ui_label.h"
|
||||
#include "elements/ui_toggle.h"
|
||||
#include "elements/ui_scroll_container.h"
|
||||
|
||||
namespace recompui {
|
||||
|
||||
class ModDetailsPanel : public Element {
|
||||
public:
|
||||
ModDetailsPanel(Element *parent);
|
||||
virtual ~ModDetailsPanel();
|
||||
void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled);
|
||||
void set_mod_toggled_callback(std::function<void(bool)> callback);
|
||||
void set_mod_configure_pressed_callback(std::function<void()> callback);
|
||||
void setup_mod_navigation(Element* nav_target);
|
||||
void clear_mod_navigation();
|
||||
Toggle* get_enable_toggle() { return enable_toggle; }
|
||||
Button* get_configure_button() { return configure_button; }
|
||||
void disable_toggle();
|
||||
protected:
|
||||
std::string_view get_type_name() override { return "ModDetailsPanel"; }
|
||||
private:
|
||||
recomp::mods::ModDetails cur_details;
|
||||
Container *thumbnail_container = nullptr;
|
||||
Image *thumbnail_image = nullptr;
|
||||
Container *header_container = nullptr;
|
||||
Container *header_details_container = nullptr;
|
||||
Label *title_label = nullptr;
|
||||
Label *version_label = nullptr;
|
||||
ScrollContainer *body_container = nullptr;
|
||||
Label *description_label = nullptr;
|
||||
Label *authors_label = nullptr;
|
||||
Container *buttons_container = nullptr;
|
||||
Container *enable_container = nullptr;
|
||||
Toggle *enable_toggle = nullptr;
|
||||
Label *enable_label = nullptr;
|
||||
Button *configure_button = nullptr;
|
||||
std::function<void(bool)> mod_toggled_callback = nullptr;
|
||||
std::function<void()> mod_configure_pressed_callback = nullptr;
|
||||
|
||||
void enable_toggle_checked(bool checked);
|
||||
void configure_button_pressed();
|
||||
};
|
||||
|
||||
} // namespace recompui
|
||||
#endif
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user