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

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

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

* Updated color option type styling

* Added dropdown option type

* Added TextField option type

* Button config type + callback wip

* init mod menu + bem class + button presets

* WIP mod menu, fix some warnings

* Rewrite mod details under new UI system.

* Refactored mods menu entirely.

* Remove ModMenu.scss.

* Take ownership of created pointers on Element class.

* Add styles.

* Multi-style state and disabled state propagation.

* Switch to string views.

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

* Mod menu progress.

* Layout for mod details panel, add gap property setters

* Update RmlUi for gap property in flexbox

* Add slot_map and begin ui context

* Implement context and resource storage slotmaps

* Config submenu.

* Refactored to account for context changes.

* Turn off tab searching when submenu is open.

* Revert accidental RmlUi downgrade

* Upgrade RmlUi to 6.0 release

* Text input.

* Radio option.

* Cleanup.

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

* Add support for config schema.

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

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

* Begin implementing mod UI API

* Link storage to mod menu.

* Proper enum parsing.

* Enable mod reordering.

* Draggable improvements to mod menu and runtime update.

* Adjust styling of submenu.

* Mods folder button.

* Linux build fixes.

* Hook up new manifest fields to mod UI

* Add basic thumbnail parsing functionality.

* More style changes.

* Implement update event for elements

* Use RT64's texture laoding instead.

* Restore spacer animations.

* Animation API begone.

* Auto-enabled mods.

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

* Sub menu display name, assert on text input.

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

* Add a state for when no mods are installed.

* Unify API function naming scheme and export relevant API functions

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

* Expose remaining property setters to mod UI API

* Implemented mod UI callbacks

* Implement actor extension data and use it for transform tagging

* Zero the memory allocated to hold extended actor data

* Implement label and textinput in mod UI API

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

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

* Enable triple buffering in RT64 (#546)

* Implement controlling input capturing for mod UI contexts

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

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

* Fix compilation after rebase

* Fixes for macOS

* Set the blender description manually for the UI renderer

* Created mod UI API functions for imageview elements

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

* Update RT64 for driver bug workarounds and misc fixes

* Update RT64 to fix native sampler issues with tile copies

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

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

* Update runtime to allow renderers to choose the graphics API

* Update RT64 to enable early Z test shader optimization

* Implement data structure mod APIs

* Update lunasvg to increase its minimum cmake version

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

* Add missing typename to fix compilation on some compilers

* Update RT64 to fix failed assert with MSAA off

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

* DnD prototype.

* Fix to dynamic lib path and runtime commit.

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

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

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

* Replace use of std::bind with lambdas

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

* Update runtime to fix renderer shutdown race condition

* Implement texture pack reordering

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

* Update runtime for mod version export

* Update runtime for save swapping mod API

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

* Updated mod list styling (#561)

* Updated mod list styling

* mod entry max height

* Update RT64 for v5 texture hash

* Update runtime for mod API to get save file path

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

* Update runtime for mod default enabled state

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

* Update runtime to fix default value of enabled_by_default

* Update runtime to allow NULL recomp_free

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

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

* Use a multiple file select dialog for mod install button

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

* Manual navigation in UI framework and WIP mod menu navigation

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

* Patch AnimationContext_SetLoadFrame to allow custom animations (#564)

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

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

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

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

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

* Update runtime after merge

* Experiment with removing the D-Pad.

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

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

* Add built-in mods readme

---------

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

* Fixing navigation of mods menu.

* Focused state for mod entry.

* Prevent hover styling and focus on input elements when disabled

* Fix up/down navigation on text input elements

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

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

* Update runtime for mod folder export

* Improve radio navigation and setup mod config submenu navigation setup

* Restore fd anywhere export functionality (#570)

* fix fd

* add comment back in

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

* Add navigation exports to mod UI API

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

---------

Co-authored-by: Dario <dariosamo@gmail.com>
Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com>
Co-authored-by: Garrett Cox <garrettjcox@gmail.com>
Co-authored-by: David Chavez <david@dcvz.io>
Co-authored-by: danielryb <59661841+danielryb@users.noreply.github.com>
Co-authored-by: Reonu <danileon95@gmail.com>
Co-authored-by: LittleCube <littlecubehax@gmail.com>
This commit is contained in:
Wiseguy
2025-04-28 02:49:43 -04:00
committed by Mr-Wiseguy
parent 8ec7b282e3
commit d766cf328f
113 changed files with 15316 additions and 3987 deletions

3
.gitmodules vendored
View File

@@ -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

View File

@@ -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}")

View File

@@ -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>

View File

@@ -20,19 +20,19 @@
}
.col {
flex: 1;
text-align: center;
text-align: center;
}
</style>
<link type="text/template" href="config_menu/general.rml" />
<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;">&#x21A7;</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> -->

View File

@@ -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" />

View 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>

View 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>

View File

@@ -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>

File diff suppressed because one or more lines are too long

View File

@@ -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": {

View File

@@ -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;
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}
}
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -2,6 +2,9 @@
@import "./ControlOption";
@import "./Tabs";
@import "./Config";
@import "./ConfigGroup";
@import "./ConfigOption";
@import "./ConfigDescription";
@import "./InputConfig";
@import "./Button";
@import "./IconButton";

View File

@@ -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;
}

View 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
View 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
View 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
View 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

View File

@@ -3,125 +3,143 @@
#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;
class Context;
class Event;
class ElementDocument;
class EventListenerInstancer;
class Context;
class Event;
}
namespace recompui {
class UiEventListenerInstancer;
class UiEventListenerInstancer;
class MenuController {
public:
virtual ~MenuController() {}
virtual Rml::ElementDocument* load_document(Rml::Context* context) = 0;
virtual void register_events(UiEventListenerInstancer& listener) = 0;
virtual void make_bindings(Rml::Context* context) = 0;
};
// TODO remove this once the UI has been ported over to the new system.
class MenuController {
public:
virtual ~MenuController() {}
virtual void load_document() = 0;
virtual void register_events(UiEventListenerInstancer& listener) = 0;
virtual void make_bindings(Rml::Context* context) = 0;
};
std::unique_ptr<MenuController> create_launcher_menu();
std::unique_ptr<MenuController> create_config_menu();
std::unique_ptr<MenuController> create_launcher_menu();
std::unique_ptr<MenuController> create_config_menu();
using event_handler_t = void(const std::string& param, Rml::Event&);
using event_handler_t = void(const std::string& param, Rml::Event&);
void queue_event(const SDL_Event& event);
bool try_deque_event(SDL_Event& out);
void queue_event(const SDL_Event& event);
bool try_deque_event(SDL_Event& out);
std::unique_ptr<UiEventListenerInstancer> make_event_listener_instancer();
void register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler);
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 {
General,
Controls,
Graphics,
Audio,
Debug,
Count
};
enum class ConfigTab {
General,
Controls,
Graphics,
Sound,
Mods,
Debug,
};
enum class ButtonVariant {
Primary,
Secondary,
Tertiary,
Success,
Error,
Warning,
NumVariants,
};
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();
void set_config_submenu(ConfigSubmenu submenu);
enum class ButtonVariant {
Primary,
Secondary,
Tertiary,
Success,
Error,
Warning,
NumVariants,
};
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();
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();
extern const std::unordered_map<ButtonVariant, std::string> button_variants;
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();
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;
bool get_cont_active(void);
void set_cont_active(bool active);
void activate_mouse();
std::string returnElementId = "";
void message_box(const char* msg);
bool open = false;
bool shouldFocus = false;
bool focusOnCancel = true;
void set_render_hooks();
PromptContext() = default;
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 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);
};
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);
PromptContext *get_prompt_context(void);
bool get_cont_active(void);
void set_cont_active(bool active);
void activate_mouse();
void message_box(const char* msg);
void set_render_hooks();
void drop_files(const std::list<std::filesystem::path> &file_list);
}
#endif

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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}
)

1
lib/slot_map Submodule

Submodule lib/slot_map added at b8ac8ebd89

6
mods/BUILTIN_MODS.md Normal file
View 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

Binary file not shown.

91
patches/actor_data.c Normal file
View 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
View 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

View File

@@ -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);
actor->init(actor, play);
actor->init = NULL;
// @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);
}
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));
// @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;;
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 != &params->player->actor))) {
CollisionCheck_ResetDamage(&actor->colChkInfo);
} else {
Math_Vec3f_Copy(&actor->prevPos, &actor->world.pos);
actor->xzDistToPlayer = Actor_WorldDistXZToActor(actor, &params->player->actor);
actor->playerHeightRel = Actor_HeightDiff(actor, &params->player->actor);
actor->xyzDistToPlayerSq = SQ(actor->xzDistToPlayer) + SQ(actor->playerHeightRel);
actor->yawTowardsPlayer = Actor_WorldYawTowardActor(actor, &params->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
View 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

View 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();
}
}

File diff suppressed because it is too large Load Diff

42
patches/memory_patches.c Normal file
View 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);
}
}
}

View File

@@ -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);

View File

@@ -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;
}

View 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

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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
View 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

View File

@@ -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);
}

View File

@@ -42,6 +42,10 @@ static struct {
bool rumble_active;
} InputState;
static struct {
std::list<std::filesystem::path> files_dropped;
} DropState;
std::atomic<recomp::InputDevice> scanning_device = recomp::InputDevice::COUNT;
std::atomic<recomp::InputField> scanned_input;
@@ -103,7 +107,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
SDL_KeyboardEvent* keyevent = &event->key;
// Skip repeated events when not in the menu
if (recompui::get_current_menu() == recompui::Menu::None &&
if (!recompui::is_context_capturing_input() &&
event->key.repeat) {
break;
}
@@ -156,10 +160,6 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
return true;
}
if (recompui::get_current_menu() != recompui::Menu::Config) {
recompui::set_current_menu(recompui::Menu::Config);
}
zelda64::open_quit_game_prompt();
recompui::activate_mouse();
break;
@@ -275,6 +275,18 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
InputState.pending_mouse_delta[0] += motion_event->xrel;
InputState.pending_mouse_delta[1] += motion_event->yrel;
}
queue_if_enabled(event);
break;
case SDL_EventType::SDL_DROPBEGIN:
DropState.files_dropped.clear();
break;
case SDL_EventType::SDL_DROPFILE:
DropState.files_dropped.emplace_back(std::filesystem::path(std::u8string_view((const char8_t *)(event->drop.file))));
SDL_free(event->drop.file);
break;
case SDL_EventType::SDL_DROPCOMPLETE:
recompui::drop_files(DropState.files_dropped);
break;
default:
queue_if_enabled(event);
break;
@@ -284,6 +296,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
void recomp::handle_events() {
SDL_Event cur_event;
static bool started = false;
static bool exited = false;
while (SDL_PollEvent(&cur_event) && !exited) {
exited = sdl_event_filter(nullptr, &cur_event);
@@ -300,6 +313,11 @@ void recomp::handle_events() {
SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE);
SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE);
}
if (!started && ultramodern::is_game_started()) {
started = true;
recompui::process_game_started();
}
}
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;
@@ -711,8 +729,8 @@ void recomp::set_right_analog_suppressed(bool suppressed) {
}
bool recomp::game_input_disabled() {
// Disable input if any menu is open.
return recompui::get_current_menu() != recompui::Menu::None;
// Disable input if any menu that blocks input is open.
return recompui::is_context_capturing_input();
}
bool recomp::all_input_disabled() {

View File

@@ -0,0 +1,237 @@
#include <vector>
#include <mutex>
#include "slot_map.h"
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "ultramodern/error_handling.hpp"
#include "recomp_ui.h"
#include "recomp_data.h"
#include "../patches/actor_funcs.h"
struct ExtensionInfo {
// Either the actor's type ID, or 0xFFFFFFFF if this is for generic data.
uint32_t actor_type;
// The offset from either the start of the actor's data or the start of the actor's specific extension data depending on the value of actor_type.
uint32_t data_offset;
};
struct ExtensionData {
uint32_t actor_spawn_index;
PTR(void) data_addr;
};
std::mutex actor_data_mutex{};
// The total size of actor-specific extension data for each actor type.
std::vector<uint32_t> actor_data_sizes{};
// The total size of all generic actor extension data.
uint32_t generic_data_size;
// The registered actor extensions.
std::vector<ExtensionInfo> actor_extensions{};
// The extension data for every actor.
using actor_data_map_t = dod::slot_map32<ExtensionData>;
actor_data_map_t actor_data{};
// The number of actors spawned since the last reset.
uint32_t actor_spawn_count = 0;
// Whether or not extensions can be registered at this time.
bool can_register = false;
// Debug counters.
size_t alloc_count = 0;
size_t free_count = 0;
void recomputil::init_extended_actor_data() {
std::lock_guard lock{ actor_data_mutex };
actor_data_sizes.clear();
generic_data_size = 0;
actor_extensions.clear();
actor_data.reset();
actor_spawn_count = 0;
can_register = true;
// Create a dummy extension so the first extension handle is nonzero, should help catch bugs.
actor_extensions.push_back({});
}
void recomputil::reset_actor_data() {
std::lock_guard lock{ actor_data_mutex };
actor_data.reset();
actor_spawn_count = 0;
assert(alloc_count == free_count);
alloc_count = 0;
free_count = 0;
}
constexpr uint32_t round_up_16(uint32_t value) {
return (value + 15) & (~15);
}
extern "C" void recomp_register_actor_extension(uint8_t* rdram, recomp_context* ctx) {
u32 actor_type = _arg<0, u32>(rdram, ctx);
u32 size = _arg<1, u32>(rdram, ctx);
if (!can_register) {
recompui::message_box("Fatal error in mod - attempted to register actor extension data after actors have been spawned.");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
if (actor_data_sizes.size() <= actor_type) {
actor_data_sizes.resize(2 * actor_type);
}
// Increase the actor type's extension data size by the provided size (rounded up to a multiple of 16).
uint32_t data_offset = actor_data_sizes[actor_type];
actor_data_sizes[actor_type] += round_up_16(size);
// Register the extension.
uint32_t ret = static_cast<uint32_t>(actor_extensions.size());
actor_extensions.emplace_back(ExtensionInfo{.actor_type = actor_type, .data_offset = data_offset});
// printf("Registered actor extension data for type %u (size 0x%08X, offset 0x%08X)\n", actor_type, size, data_offset);
_return<u32>(ctx, ret);
}
extern "C" void recomp_register_actor_extension_generic(uint8_t* rdram, recomp_context* ctx) {
u32 size = _arg<0, u32>(rdram, ctx);
// Increase the generic extension data size by the provided size (rounded up to a multiple of 16).
uint32_t data_offset = generic_data_size;
generic_data_size += round_up_16(size);
// Register the extension.
uint32_t ret = static_cast<uint32_t>(actor_extensions.size());
actor_extensions.emplace_back(ExtensionInfo{.actor_type = 0xFFFFFFFFU, .data_offset = data_offset});
// printf("Registered generic actor extension data (size 0x%08X, offset 0x%08X)\n", size, data_offset);
_return<u32>(ctx, ret);
}
extern "C" void recomp_clear_all_actor_data(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
(void)ctx;
recomputil::reset_actor_data();
}
extern "C" void recomp_create_actor_data(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
can_register = false;
// Determine the number of bytes to allocate based on the actor type's extensions and the generic extensions.
u32 actor_type = _arg<0, u32>(rdram, ctx);
u32 alloc_size = generic_data_size;
[[maybe_unused]] u32 type_data_size = 0;
if (actor_type < actor_data_sizes.size()) {
type_data_size = actor_data_sizes[actor_type];
alloc_size += type_data_size;
}
// Allocate the extension data if it's of nonzero size.
PTR(void) data_ptr = NULLPTR;
if (alloc_size != 0) {
void* data = recomp::alloc(rdram, alloc_size);
alloc_count++;
data_ptr = reinterpret_cast<uint8_t*>(data) - rdram + 0xFFFFFFFF80000000U;
// Zero the allocated memory.
// A memset should be fine here since this data is aligned, but use a byteswapped loop just to be safe.
for (size_t i = 0; i < alloc_size; i++) {
MEM_B(i, data_ptr) = 0;
}
}
// Add the actor's fields to the actor data slotmap.
u32 spawn_index = actor_spawn_count++;
dod::slot_map_key32<ExtensionData> key = actor_data.emplace(ExtensionData{.actor_spawn_index = spawn_index, .data_addr = data_ptr});
// printf("Allocated actor data: address 0x%08X with 0x%08X bytes total (0x%08X bytes generic and 0x%08X bytes specific), handle 0x%08X, spawn index %d\n",
// data_ptr, alloc_size, generic_data_size, type_data_size, key.raw, spawn_index);
_return<u32>(ctx, key.raw);
}
extern "C" void recomp_destroy_actor_data(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
u32 actor_handle = _arg<0, u32>(rdram, ctx);
actor_data_map_t::key actor_key{actor_handle};
ExtensionData* data = actor_data.get(actor_key);
if (data != nullptr) {
// printf("Freeing actor data: address 0x%08X handle 0x%08X\n", data->data_addr, actor_handle);
if (data->data_addr != NULLPTR) {
recomp::free(rdram, TO_PTR(void, data->data_addr));
free_count++;
}
actor_data.erase(actor_data_map_t::key{actor_handle});
}
else {
// Not an irrecoverable error, but catch it in debug mode with an assert to help find bugs.
assert(false);
}
}
extern "C" void recomp_get_actor_data(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
u32 actor_handle = _arg<0, u32>(rdram, ctx);
u32 extension_handle = _arg<1, u32>(rdram, ctx);
u32 actor_type = _arg<2, u32>(rdram, ctx);
// Check if the extension handle is valid.
if (extension_handle == 0 || extension_handle >= actor_extensions.size()) {
_return<PTR(void)>(ctx, NULLPTR);
return;
}
ExtensionInfo& extension = actor_extensions[extension_handle];
bool generic_extension = extension.actor_type == 0xFFFFFFFFU;
// Check if the extension is generic or for the provided actor type.
if (!generic_extension && extension.actor_type != actor_type) {
_return<PTR(void)>(ctx, NULLPTR);
return;
}
actor_data_map_t::key actor_key{actor_handle};
ExtensionData* data = actor_data.get(actor_key);
// Check if actor handle is valid.
if (data == nullptr) {
_return<PTR(void)>(ctx, NULLPTR);
return;
}
// Calculate the address for this specific extension's data.
PTR(void) base_address = data->data_addr;
u32 offset = extension.data_offset;
// Specific actor data is after generic actor data, so increase the offset by the total generic actor data if this isn't generic data.
if (!generic_extension) {
offset += generic_data_size;
}
PTR(void) ret = base_address + offset;
_return<PTR(void)>(ctx, ret);
}
extern "C" void recomp_get_actor_spawn_index(uint8_t* rdram, recomp_context* ctx) {
std::lock_guard lock{ actor_data_mutex };
u32 actor_handle = _arg<0, u32>(rdram, ctx);
actor_data_map_t::key actor_key{actor_handle};
ExtensionData* data = actor_data.get(actor_key);
// Check if actor handle is valid.
if (data == nullptr) {
_return<u32>(ctx, 0xFFFFFFFFU);
return;
}
_return<u32>(ctx, data->actor_spawn_index);
}

View File

@@ -0,0 +1,727 @@
#include <vector>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include "slot_map.h"
#include "recomp_data.h"
#include "recomp_ui.h"
#include "librecomp/helpers.hpp"
#include "librecomp/overlays.hpp"
#include "librecomp/addresses.hpp"
#include "ultramodern/error_handling.hpp"
template <typename KeyType, typename ValueType>
class LockedMap {
private:
std::mutex mutex{};
std::unordered_map<KeyType, ValueType> map{};
public:
bool get(const KeyType& key, ValueType& out) {
std::lock_guard lock{mutex};
auto find_it = map.find(key);
if (find_it == map.end()) {
return false;
}
out = find_it->second;
return true;
}
bool insert(const KeyType& key, ValueType val) {
std::lock_guard lock{mutex};
auto ret = map.insert_or_assign(key, val);
return ret.second;
}
bool erase(const KeyType& key) {
std::lock_guard lock{mutex};
size_t num_erased = map.erase(key);
return num_erased != 0;
}
void clear() {
std::lock_guard lock{mutex};
map.clear();
}
bool erase_first(ValueType& out) {
std::lock_guard lock{mutex};
auto it = map.begin();
if (it == map.end()) {
return false;
}
out = it->second;
map.erase(it);
return true;
}
bool contains(const KeyType& key) {
std::lock_guard lock{mutex};
return map.contains(key);
}
size_t size() {
std::lock_guard lock{mutex};
return map.size();
}
};
template <typename KeyType>
class LockedSet {
private:
std::mutex mutex{};
std::unordered_set<KeyType> set{};
public:
bool contains(const KeyType& key) {
std::lock_guard lock{mutex};
return set.contains(key);
}
bool insert(const KeyType& key) {
std::lock_guard lock{mutex};
auto it = set.insert(key);
return it.second;
}
bool erase(const KeyType& key) {
std::lock_guard lock{mutex};
size_t num_erased = set.erase(key);
return num_erased != 0;
}
void clear() {
std::lock_guard lock{mutex};
set.clear();
}
size_t size() {
std::lock_guard lock{mutex};
return set.size();
}
};
template <typename ValueType>
class LockedSlotmap {
private:
std::mutex mutex{};
dod::slot_map32<ValueType> map{};
using key_t = typename dod::slot_map32<ValueType>::key;
public:
bool get(uint32_t key, ValueType** out) {
std::lock_guard lock{mutex};
ValueType* ret = map.get(key_t{key});
*out = ret;
return ret != nullptr;
}
uint32_t create() {
std::lock_guard lock{mutex};
return map.emplace().raw;
}
bool erase(uint32_t key) {
std::lock_guard lock{mutex};
if (!map.has_key(key_t{ key })) {
return false;
}
map.erase(key_t{ key });
return true;
}
void clear() {
std::lock_guard lock{mutex};
map.clear();
}
bool erase_first(ValueType& out) {
std::lock_guard lock{mutex};
auto it = map.items().begin();
if (it == map.items().end()) {
return false;
}
out = it->second;
map.erase(it->first);
return true;
}
size_t size() {
std::lock_guard lock{mutex};
return map.size();
}
};
using U32ValueMap = LockedMap<uint32_t, uint32_t>;
using U32MemoryMap = std::pair<LockedMap<uint32_t, PTR(void)>, u32>;
using U32HashSet = LockedSet<uint32_t>;
using U32Slotmap = LockedSlotmap<uint32_t>;
using MemorySlotmap = std::pair<LockedSlotmap<PTR(void)>, u32>;
LockedSlotmap<U32ValueMap> u32_value_hashmaps{};
LockedSlotmap<U32MemoryMap> u32_memory_hashmaps{};
LockedSlotmap<U32HashSet> u32_hashsets{};
LockedSlotmap<U32Slotmap> u32_slotmaps{};
LockedSlotmap<MemorySlotmap> memory_slotmaps{};
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
static void show_fatal_error_message_box(const char* funcname, const char* errstr) {
std::string message = std::string{"Fatal error in mod - "} + funcname + " : " + errstr;
recompui::message_box(message.c_str());
}
#define HANDLE_INVALID_ERROR() \
show_fatal_error_message_box(__FUNCTION__, "handle is invalid"); \
assert(false); \
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
#define SLOTMAP_KEY_INVALID_ERROR() \
show_fatal_error_message_box(__FUNCTION__, "slotmap key is invalid"); \
assert(false); \
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
// u32 -> 32-bit value hashmap.
void recomputil_create_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_value_hashmaps.create());
}
void recomputil_destroy_u32_value_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_value_hashmaps.erase(mapkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_value_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->contains(key));
}
void recomputil_u32_value_hashmap_insert(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->insert(key, value));
}
void recomputil_u32_value_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t ret;
if (map->get(key, ret)) {
MEM_W(0, val_out) = ret;
_return(ctx, 1);
return;
}
else {
_return(ctx, 0);
return;
}
}
void recomputil_u32_value_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->erase(key));
}
void recomputil_u32_value_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32ValueMap* map;
if (!u32_value_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->size()));
}
// u32 -> memory hashmap.
void recomputil_create_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t element_size = _arg<0, uint32_t>(rdram, ctx);
// Create the map.
uint32_t map_key = u32_memory_hashmaps.create();
// Retrieve the map and set its element size to the provided value.
U32MemoryMap* map;
u32_memory_hashmaps.get(map_key, &map);
map->second = element_size;
// Return the created map's key.
_return(ctx, map_key);
}
void recomputil_destroy_u32_memory_hashmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
// Retrieve the map.
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free all of the entries in the map.
PTR(void) cur_mem;
while (map->first.erase_first(cur_mem)) {
recomp::free(rdram, TO_PTR(void, cur_mem));
}
// Destroy the map itself.
u32_memory_hashmaps.erase(mapkey);
}
void recomputil_u32_memory_hashmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->first.contains(key));
}
void recomputil_u32_memory_hashmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Check if the map contains the key already to prevent inserting it twice.
PTR(void) dummy;
if (map->first.get(key, dummy)) {
_return(ctx, 0);
return;
}
// Allocate the map's size and return the pointer.
void* mem = recomp::alloc(rdram, map->second);
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
// Zero the memory.
for (size_t i = 0; i < map->second; i++) {
MEM_B(i, addr) = 0;
}
PTR(void) ret = static_cast<PTR(void)>(addr);
map->first.insert(key, ret);
_return(ctx, 1);
}
void recomputil_u32_memory_hashmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void) ret;
if (map->first.get(key, ret)) {
_return(ctx, ret);
return;
}
else {
_return(ctx, NULLPTR);
return;
}
}
void recomputil_u32_memory_hashmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free the memory for this key if the key exists.
PTR(void) addr;
bool has_value = map->first.get(key, addr);
if (has_value) {
void* mem = TO_PTR(void, addr);
recomp::free(rdram, mem);
}
_return(ctx, map->first.erase(key));
}
void recomputil_u32_memory_hashmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32MemoryMap* map;
if (!u32_memory_hashmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->first.size()));
}
// u32 hashset.
void recomputil_create_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_hashsets.create());
}
void recomputil_destroy_u32_hashset(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_hashsets.erase(setkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_hashset_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->contains(key));
}
void recomputil_u32_hashset_insert(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->insert(key));
}
void recomputil_u32_hashset_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, set->erase(key));
}
void recomputil_u32_hashset_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t setkey = _arg<0, uint32_t>(rdram, ctx);
U32HashSet* set;
if (!u32_hashsets.get(setkey, &set)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(set->size()));
}
// u32 value slotmap.
void recomputil_create_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, u32_slotmaps.create());
}
void recomputil_destroy_u32_slotmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
if (!u32_slotmaps.erase(mapkey)) {
HANDLE_INVALID_ERROR();
}
}
void recomputil_u32_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* dummy_ptr;
_return(ctx, map->get(key, &dummy_ptr));
}
void recomputil_u32_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, map->create());
}
void recomputil_u32_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* ret;
if (!map->get(key, &ret)) {
_return(ctx, 0);
}
MEM_W(0, val_out) = *ret;
_return(ctx, 1);
}
void recomputil_u32_slotmap_set(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
uint32_t value = _arg<2, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
uint32_t* value_ptr;
if (!map->get(key, &value_ptr)) {
_return(ctx, 0);
}
*value_ptr = value;
_return(ctx, 1);
}
void recomputil_u32_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
if (!map->erase(key)) {
_return(ctx, 0);
}
_return(ctx, 1);
}
void recomputil_u32_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
U32Slotmap* map;
if (!u32_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->size()));
}
// memory slotmap.
void recomputil_create_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
_return(ctx, memory_slotmaps.create());
}
void recomputil_destroy_memory_slotmap(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
// Retrieve the map.
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free all of the entries in the map.
PTR(void) cur_mem;
while (map->first.erase_first(cur_mem)) {
recomp::free(rdram, TO_PTR(void, cur_mem));
}
// Destroy the map itself.
memory_slotmaps.erase(mapkey);
}
void recomputil_memory_slotmap_contains(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void)* dummy_ptr;
_return(ctx, map->first.get(key, &dummy_ptr));
}
void recomputil_memory_slotmap_create(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Create the slotmap element.
u32 key = map->first.create();
// Allocate the map's element size.
void* mem = recomp::alloc(rdram, map->second);
gpr addr = reinterpret_cast<uint8_t*>(mem) - rdram + 0xFFFFFFFF80000000ULL;
// Zero the memory.
for (size_t i = 0; i < map->second; i++) {
MEM_B(i, addr) = 0;
}
// Store the allocated pointer.
PTR(void)* value_ptr;
map->first.get(key, &value_ptr);
MEM_W(0, *value_ptr) = addr;
// Return the key.
_return(ctx, key);
}
void recomputil_memory_slotmap_get(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
PTR(uint32_t) val_out = _arg<2, PTR(uint32_t)>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
PTR(void)* ret;
if (!map->first.get(key, &ret)) {
SLOTMAP_KEY_INVALID_ERROR();
}
MEM_W(0, val_out) = *ret;
}
void recomputil_memory_slotmap_erase(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
uint32_t key = _arg<1, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
// Free the memory for this key if the key exists.
PTR(void)* addr;
bool has_value = map->first.get(key, &addr);
if (has_value) {
void* mem = TO_PTR(void, addr);
recomp::free(rdram, mem);
}
_return(ctx, map->first.erase(key));
}
void recomputil_memory_slotmap_size(uint8_t* rdram, recomp_context* ctx) {
uint32_t mapkey = _arg<0, uint32_t>(rdram, ctx);
MemorySlotmap* map;
if (!memory_slotmaps.get(mapkey, &map)) {
HANDLE_INVALID_ERROR();
}
_return(ctx, static_cast<uint32_t>(map->first.size()));
}
// Exports.
void recomputil::register_data_api_exports() {
REGISTER_FUNC(recomputil_create_u32_value_hashmap);
REGISTER_FUNC(recomputil_destroy_u32_value_hashmap);
REGISTER_FUNC(recomputil_u32_value_hashmap_contains);
REGISTER_FUNC(recomputil_u32_value_hashmap_insert);
REGISTER_FUNC(recomputil_u32_value_hashmap_get);
REGISTER_FUNC(recomputil_u32_value_hashmap_erase);
REGISTER_FUNC(recomputil_u32_value_hashmap_size);
REGISTER_FUNC(recomputil_create_u32_memory_hashmap);
REGISTER_FUNC(recomputil_destroy_u32_memory_hashmap);
REGISTER_FUNC(recomputil_u32_memory_hashmap_contains);
REGISTER_FUNC(recomputil_u32_memory_hashmap_create);
REGISTER_FUNC(recomputil_u32_memory_hashmap_get);
REGISTER_FUNC(recomputil_u32_memory_hashmap_erase);
REGISTER_FUNC(recomputil_u32_memory_hashmap_size);
REGISTER_FUNC(recomputil_create_u32_hashset);
REGISTER_FUNC(recomputil_destroy_u32_hashset);
REGISTER_FUNC(recomputil_u32_hashset_contains);
REGISTER_FUNC(recomputil_u32_hashset_insert);
REGISTER_FUNC(recomputil_u32_hashset_erase);
REGISTER_FUNC(recomputil_u32_hashset_size);
REGISTER_FUNC(recomputil_create_u32_slotmap);
REGISTER_FUNC(recomputil_destroy_u32_slotmap);
REGISTER_FUNC(recomputil_u32_slotmap_contains);
REGISTER_FUNC(recomputil_u32_slotmap_create);
REGISTER_FUNC(recomputil_u32_slotmap_get);
REGISTER_FUNC(recomputil_u32_slotmap_set);
REGISTER_FUNC(recomputil_u32_slotmap_erase);
REGISTER_FUNC(recomputil_u32_slotmap_size);
REGISTER_FUNC(recomputil_create_memory_slotmap);
REGISTER_FUNC(recomputil_destroy_memory_slotmap);
REGISTER_FUNC(recomputil_memory_slotmap_contains);
REGISTER_FUNC(recomputil_memory_slotmap_create);
REGISTER_FUNC(recomputil_memory_slotmap_get);
REGISTER_FUNC(recomputil_memory_slotmap_erase);
REGISTER_FUNC(recomputil_memory_slotmap_size);
}

View File

@@ -27,6 +27,7 @@
#include "zelda_render.h"
#include "zelda_support.h"
#include "zelda_game.h"
#include "recomp_data.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
#include "librecomp/mods.hpp"
@@ -37,6 +38,8 @@
#include "../../patches/sound.h"
#include "../../patches/misc_funcs.h"
#include "mods/mm_recomp_dpad_builtin.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
@@ -544,15 +547,17 @@ void release_preload(PreloadContext& context) {
#endif
void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
zelda64::renderer::enable_texture_pack(mod);
zelda64::renderer::enable_texture_pack(context, mod);
}
void disable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
void disable_texture_pack(recomp::mods::ModContext&, const recomp::mods::ModHandle& mod) {
zelda64::renderer::disable_texture_pack(mod);
}
void reorder_texture_pack(recomp::mods::ModContext&) {
zelda64::renderer::trigger_texture_pack_update();
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
int main(int argc, char** argv) {
@@ -614,6 +619,8 @@ int main(int argc, char** argv) {
recomp::register_game(game);
}
recomp::mods::register_embedded_mod("mm_recomp_dpad_builtin", { (const uint8_t*)(mm_recomp_dpad_builtin), std::size(mm_recomp_dpad_builtin)});
REGISTER_FUNC(recomp_get_window_resolution);
REGISTER_FUNC(recomp_get_target_aspect_ratio);
REGISTER_FUNC(recomp_get_target_framerate);
@@ -627,9 +634,12 @@ int main(int argc, char** argv) {
REGISTER_FUNC(recomp_get_mouse_deltas);
REGISTER_FUNC(recomp_get_inverted_axes);
REGISTER_FUNC(recomp_get_analog_inverted_axes);
recompui::register_ui_exports();
recomputil::register_data_api_exports();
zelda64::register_overlays();
zelda64::register_patches();
recomputil::init_extended_actor_data();
zelda64::load_config();
recomp::rsp::callbacks_t rsp_callbacks{
@@ -678,39 +688,13 @@ int main(int argc, char** argv) {
.allow_runtime_toggle = true,
.on_enabled = enable_texture_pack,
.on_disabled = disable_texture_pack,
.on_reordered = reorder_texture_pack,
};
auto texture_pack_content_type_id = recomp::mods::register_mod_content_type(texture_pack_content_type);
// Register the .rtz texture pack file format with the previous content type as its only allowed content type.
recomp::mods::register_mod_container_type("rtz", std::vector{ texture_pack_content_type_id }, false);
recomp::mods::scan_mods();
printf("Found mods:\n");
for (const auto& mod : recomp::mods::get_mod_details("mm")) {
printf(" %s(%s)\n", mod.mod_id.c_str(), mod.version.to_string().c_str());
if (!mod.authors.empty()) {
printf(" Authors: %s", mod.authors[0].c_str());
for (size_t author_index = 1; author_index < mod.authors.size(); author_index++) {
const std::string& author = mod.authors[author_index];
printf(", %s", author.c_str());
}
printf("\n");
printf(" Runtime toggleable: %d\n", mod.runtime_toggleable);
}
if (!mod.dependencies.empty()) {
printf(" Dependencies: %s:%s", mod.dependencies[0].mod_id.c_str(), mod.dependencies[0].version.to_string().c_str());
for (size_t dep_index = 1; dep_index < mod.dependencies.size(); dep_index++) {
const recomp::mods::Dependency& dep = mod.dependencies[dep_index];
printf(", %s:%s", dep.mod_id.c_str(), dep.version.to_string().c_str());
}
printf("\n");
}
// TODO load all mods as a temporary solution to not having a UI yet.
recomp::mods::enable_mod(mod.mod_id, true);
}
printf("\n");
recomp::start(
project_version,
{},

View File

@@ -1,10 +1,12 @@
#include <memory>
#include <cstring>
#include <variant>
#include <algorithm>
#define HLSL_CPU
#include "hle/rt64_application.h"
#include "rt64_render_hooks.h"
#include "overloaded.h"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
@@ -13,12 +15,6 @@
#include "recomp_ui.h"
#include "concurrentqueue.h"
// Helper class for variant visiting.
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
static bool sample_positions_supported = false;
static bool high_precision_fb_enabled = false;
@@ -27,14 +23,25 @@ static uint8_t DMEM[0x1000];
static uint8_t IMEM[0x1000];
struct TexturePackEnableAction {
std::filesystem::path path;
std::string mod_id;
};
struct TexturePackDisableAction {
std::filesystem::path path;
std::string mod_id;
};
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction>;
struct TexturePackSecondaryEnableAction {
std::string mod_id;
};
struct TexturePackSecondaryDisableAction {
std::string mod_id;
};
struct TexturePackUpdateAction {
};
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction, TexturePackSecondaryEnableAction, TexturePackSecondaryDisableAction, TexturePackUpdateAction>;
static moodycamel::ConcurrentQueue<TexturePackAction> texture_pack_action_queue;
@@ -171,6 +178,7 @@ void set_application_user_config(RT64::Application* application, const ultramode
application->userConfig.refreshRate = to_rt64(config.rr_option);
application->userConfig.refreshRateTarget = config.rr_manual_value;
application->userConfig.internalColorFormat = to_rt64(config.hpfb_option);
application->userConfig.displayBuffering = RT64::UserConfiguration::DisplayBuffering::Triple;
}
ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
@@ -192,6 +200,23 @@ ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResu
std::exit(EXIT_FAILURE);
}
ultramodern::renderer::GraphicsApi map_graphics_api(RT64::UserConfiguration::GraphicsAPI api) {
switch (api) {
case RT64::UserConfiguration::GraphicsAPI::D3D12:
return ultramodern::renderer::GraphicsApi::D3D12;
case RT64::UserConfiguration::GraphicsAPI::Vulkan:
return ultramodern::renderer::GraphicsApi::Vulkan;
case RT64::UserConfiguration::GraphicsAPI::Metal:
return ultramodern::renderer::GraphicsApi::Metal;
case RT64::UserConfiguration::GraphicsAPI::Automatic:
return ultramodern::renderer::GraphicsApi::Auto;
}
fprintf(stderr, "Unhandled `RT64::UserConfiguration::GraphicsAPI` ?\n");
assert(false);
std::exit(EXIT_FAILURE);
}
zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool debug) {
static unsigned char dummy_rom_header[0x40];
recompui::set_render_hooks();
@@ -266,9 +291,8 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
case ultramodern::renderer::GraphicsApi::Metal:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Metal;
break;
default:
case ultramodern::renderer::GraphicsApi::Auto:
// Don't override if auto is selected.
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Automatic;
break;
}
@@ -278,6 +302,8 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
thread_id = window_handle.thread_id;
#endif
setup_result = map_setup_result(app->setup(thread_id));
// Get the API that RT64 chose.
chosen_api = map_graphics_api(app->chosenGraphicsAPI);
if (setup_result != ultramodern::renderer::SetupResult::Success) {
app = nullptr;
return;
@@ -306,32 +332,7 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
zelda64::renderer::RT64Context::~RT64Context() = default;
void zelda64::renderer::RT64Context::send_dl(const OSTask* task) {
bool packs_disabled = false;
TexturePackAction cur_action;
while (texture_pack_action_queue.try_dequeue(cur_action)) {
std::visit(overloaded{
[&](TexturePackDisableAction& to_disable) {
enabled_texture_packs.erase(to_disable.path);
packs_disabled = true;
},
[&](TexturePackEnableAction& to_enable) {
enabled_texture_packs.insert(to_enable.path);
// Load the pack now if no packs have been disabled.
if (!packs_disabled) {
app->textureCache->loadReplacementDirectory(to_enable.path);
}
}
}, cur_action);
}
// If any packs were disabled, unload all packs and load all the active ones.
if (packs_disabled) {
app->textureCache->clearReplacementDirectories();
for (const std::filesystem::path& cur_pack_path : enabled_texture_packs) {
app->textureCache->loadReplacementDirectory(cur_pack_path);
}
}
check_texture_pack_actions();
app->state->rsp->reset();
app->interpreter->loadUCodeGBI(task->t.ucode & 0x3FFFFFF, task->t.ucode_data & 0x3FFFFFF, true);
app->processDisplayLists(app->core.RDRAM, task->t.data_ptr & 0x3FFFFFF, 0, true);
@@ -397,6 +398,66 @@ float zelda64::renderer::RT64Context::get_resolution_scale() const {
}
}
void zelda64::renderer::RT64Context::check_texture_pack_actions() {
bool packs_changed = false;
TexturePackAction cur_action;
while (texture_pack_action_queue.try_dequeue(cur_action)) {
std::visit(overloaded{
[&](TexturePackDisableAction &to_disable) {
enabled_texture_packs.erase(to_disable.mod_id);
packs_changed = true;
},
[&](TexturePackEnableAction &to_enable) {
enabled_texture_packs.insert(to_enable.mod_id);
packs_changed = true;
},
[&](TexturePackSecondaryDisableAction &to_override_disable) {
secondary_disabled_texture_packs.insert(to_override_disable.mod_id);
packs_changed = true;
},
[&](TexturePackSecondaryEnableAction &to_override_enable) {
secondary_disabled_texture_packs.erase(to_override_enable.mod_id);
packs_changed = true;
},
[&](TexturePackUpdateAction &) {
packs_changed = true;
}
}, cur_action);
}
// If any packs were disabled, unload all packs and load all the active ones.
if (packs_changed) {
// Sort the enabled texture packs in reverse order so that earlier ones override later ones.
std::vector<std::string> sorted_texture_packs{};
sorted_texture_packs.reserve(enabled_texture_packs.size());
for (const std::string& mod : enabled_texture_packs) {
if (!secondary_disabled_texture_packs.contains(mod)) {
sorted_texture_packs.emplace_back(mod);
}
}
std::sort(sorted_texture_packs.begin(), sorted_texture_packs.end(),
[](const std::string& lhs, const std::string& rhs) {
return recomp::mods::get_mod_order_index(lhs) > recomp::mods::get_mod_order_index(rhs);
}
);
// Build the path list from the sorted mod list.
std::vector<RT64::ReplacementDirectory> replacement_directories;
replacement_directories.reserve(enabled_texture_packs.size());
for (const std::string &mod_id : sorted_texture_packs) {
replacement_directories.emplace_back(RT64::ReplacementDirectory(recomp::mods::get_mod_filename(mod_id)));
}
if (!replacement_directories.empty()) {
app->textureCache->loadReplacementDirectories(replacement_directories);
}
else {
app->textureCache->clearReplacementDirectories();
}
}
}
RT64::UserConfiguration::Antialiasing zelda64::renderer::RT64MaxMSAA() {
return device_max_msaa;
}
@@ -413,10 +474,72 @@ bool zelda64::renderer::RT64HighPrecisionFBEnabled() {
return high_precision_fb_enabled;
}
void zelda64::renderer::enable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_root_path});
void zelda64::renderer::trigger_texture_pack_update() {
texture_pack_action_queue.enqueue(TexturePackUpdateAction{});
}
void zelda64::renderer::enable_texture_pack(const recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_id});
// Check for the texture pack enabled config option.
const recomp::mods::ConfigSchema& config_schema = context.get_mod_config_schema(mod.manifest.mod_id);
auto find_it = config_schema.options_by_id.find(zelda64::renderer::special_option_texture_pack_enabled);
if (find_it != config_schema.options_by_id.end()) {
const recomp::mods::ConfigOption& config_option = config_schema.options[find_it->second];
if (is_texture_pack_enable_config_option(config_option, false)) {
recomp::mods::ConfigValueVariant value_variant = context.get_mod_config_value(mod.manifest.mod_id, config_option.id);
uint32_t value;
if (uint32_t* value_ptr = std::get_if<uint32_t>(&value_variant)) {
value = *value_ptr;
}
else {
value = 0;
}
if (value) {
zelda64::renderer::secondary_enable_texture_pack(mod.manifest.mod_id);
}
else {
zelda64::renderer::secondary_disable_texture_pack(mod.manifest.mod_id);
}
}
}
}
void zelda64::renderer::disable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_root_path});
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_id});
}
void zelda64::renderer::secondary_enable_texture_pack(const std::string& mod_id) {
texture_pack_action_queue.enqueue(TexturePackSecondaryEnableAction{mod_id});
}
void zelda64::renderer::secondary_disable_texture_pack(const std::string& mod_id) {
texture_pack_action_queue.enqueue(TexturePackSecondaryDisableAction{mod_id});
}
// HD texture enable option. Must be an enum with two options.
// The first option is treated as disabled and the second option is treated as enabled.
bool zelda64::renderer::is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors) {
if (option.id == zelda64::renderer::special_option_texture_pack_enabled) {
if (option.type != recomp::mods::ConfigOptionType::Enum) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + zelda64::renderer::special_option_texture_pack_enabled + "\"), but the config option is not an enum.").c_str());
}
return false;
}
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
if (option_enum.options.size() != 2) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + zelda64::renderer::special_option_texture_pack_enabled + "\"), but the config option doesn't have exactly 2 values.").c_str());
}
return false;
}
return true;
}
return false;
}

View File

@@ -20,6 +20,29 @@ namespace zelda64 {
callback(success, path);
}
void perform_file_dialog_operation_multiple(const std::function<void(bool, const std::list<std::filesystem::path>&)>& callback) {
const nfdpathset_t* native_paths = nullptr;
nfdresult_t result = NFD_OpenDialogMultipleN(&native_paths, nullptr, 0, nullptr);
bool success = (result == NFD_OKAY);
std::list<std::filesystem::path> paths;
nfdpathsetsize_t count = 0;
if (success) {
NFD_PathSet_GetCount(native_paths, &count);
for (nfdpathsetsize_t i = 0; i < count; i++) {
nfdnchar_t* cur_path = nullptr;
nfdresult_t cur_result = NFD_PathSet_GetPathN(native_paths, i, &cur_path);
if (cur_result == NFD_OKAY) {
paths.emplace_back(std::filesystem::path{cur_path});
}
}
NFD_PathSet_Free(native_paths);
}
callback(success, paths);
}
// MARK: - Public API
std::filesystem::path get_asset_path(const char* asset) {
@@ -41,6 +64,16 @@ namespace zelda64 {
#endif
}
void open_file_dialog_multiple(std::function<void(bool success, const std::list<std::filesystem::path>& paths)> callback) {
#ifdef __APPLE__
dispatch_on_ui_thread([callback]() {
perform_file_dialog_operation_multiple(callback);
});
#else
perform_file_dialog_operation_multiple(callback);
#endif
}
void show_error_message_box(const char *title, const char *message) {
#ifdef __APPLE__
std::string title_copy(title);

668
src/ui/core/ui_context.cpp Normal file
View File

@@ -0,0 +1,668 @@
#include <mutex>
#include <string>
#include <unordered_map>
#include <fstream>
#include "slot_map.h"
#include "RmlUi/Core/StreamMemory.h"
#include "ultramodern/error_handling.hpp"
#include "recomp_ui.h"
#include "ui_context.h"
#include "../elements/ui_element.h"
// Hash implementations for ContextId and ResourceId.
template <>
struct std::hash<recompui::ContextId> {
std::size_t operator()(const recompui::ContextId& id) const {
return std::hash<uint32_t>()(id.slot_id);
}
};
template <>
struct std::hash<recompui::ResourceId> {
std::size_t operator()(const recompui::ResourceId& id) const {
return std::hash<uint32_t>()(id.slot_id);
}
};
using resource_slotmap = dod::slot_map32<std::unique_ptr<recompui::Style>>;
namespace recompui {
struct Context {
std::mutex context_lock;
resource_slotmap resources;
Rml::ElementDocument* document;
Element root_element;
Element* autofocus_element = nullptr;
std::vector<Element*> loose_elements;
std::unordered_set<ResourceId> to_update;
bool captures_input = true;
bool captures_mouse = true;
Context(Rml::ElementDocument* document) : document(document), root_element(document) {}
};
} // namespace recompui
using context_slotmap = dod::slot_map32<recompui::Context>;
static struct {
std::recursive_mutex all_contexts_lock;
context_slotmap all_contexts;
std::unordered_set<recompui::ContextId> opened_contexts;
std::unordered_map<Rml::ElementDocument*, recompui::ContextId> documents_to_contexts;
Rml::SharedPtr<Rml::StyleSheetContainer> style_sheet;
} context_state;
thread_local recompui::Context* opened_context = nullptr;
thread_local recompui::ContextId opened_context_id = recompui::ContextId::null();
enum class ContextErrorType {
OpenWithoutClose,
OpenInvalidContext,
CloseWithoutOpen,
CloseWrongContext,
DestroyInvalidContext,
GetContextWithoutOpen,
AddResourceWithoutOpen,
AddResourceToWrongContext,
UpdateElementWithoutContext,
UpdateElementInWrongContext,
GetResourceWithoutOpen,
GetResourceFailed,
DestroyResourceWithoutOpen,
DestroyResourceInWrongContext,
DestroyResourceNotFound,
GetDocumentInvalidContext,
GetAutofocusInvalidContext,
SetAutofocusInvalidContext,
InternalError,
};
enum class SlotTag : uint8_t {
Style = 0,
Element = 1,
};
void context_error(recompui::ContextId id, ContextErrorType type) {
(void)id;
const char* error_message = "";
switch (type) {
case ContextErrorType::OpenWithoutClose:
error_message = "Attempted to open a UI context without closing another UI context";
break;
case ContextErrorType::OpenInvalidContext:
error_message = "Attempted to open an invalid UI context";
break;
case ContextErrorType::CloseWithoutOpen:
error_message = "Attempted to close a UI context without one being open";
break;
case ContextErrorType::CloseWrongContext:
error_message = "Attempted to close a different UI context than the one that's open";
break;
case ContextErrorType::DestroyInvalidContext:
error_message = "Attempted to destroy an invalid UI element";
break;
case ContextErrorType::GetContextWithoutOpen:
error_message = "Attempted to get the current UI context with no UI context open";
break;
case ContextErrorType::AddResourceWithoutOpen:
error_message = "Attempted to create a UI resource with no open UI context";
break;
case ContextErrorType::AddResourceToWrongContext:
error_message = "Attempted to create a UI resource in a different UI context than the one that's open";
break;
case ContextErrorType::UpdateElementWithoutContext:
error_message = "Attempted to update a UI element with no open UI context";
break;
case ContextErrorType::UpdateElementInWrongContext:
error_message = "Attempted to update a UI element in a different UI context than the one that's open";
break;
case ContextErrorType::GetResourceWithoutOpen:
error_message = "Attempted to get a UI resource with no open UI context";
break;
case ContextErrorType::GetResourceFailed:
error_message = "Failed to get a UI resource from the current open UI context";
break;
case ContextErrorType::DestroyResourceWithoutOpen:
error_message = "Attempted to destroy a UI resource with no open UI context";
break;
case ContextErrorType::DestroyResourceInWrongContext:
error_message = "Attempted to destroy a UI resource in a different UI context than the one that's open";
break;
case ContextErrorType::DestroyResourceNotFound:
error_message = "Attempted to destroy a UI resource that doesn't exist in the current context";
break;
case ContextErrorType::GetDocumentInvalidContext:
error_message = "Attempted to get the document of an invalid UI context";
break;
case ContextErrorType::GetAutofocusInvalidContext:
error_message = "Attempted to get the autofocus element of an invalid UI context";
break;
case ContextErrorType::SetAutofocusInvalidContext:
error_message = "Attempted to set the autofocus element of an invalid UI context";
break;
case ContextErrorType::InternalError:
error_message = "Internal error in UI context";
break;
default:
error_message = "Unknown UI context error";
break;
}
// This assumes the error is coming from a mod, as it's unlikely that an end user will see a UI context error
// in the base recomp.
recompui::message_box((std::string{"Fatal error in mod - "} + error_message + ".").c_str());
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
recompui::ContextId create_context_impl(Rml::ElementDocument* document) {
static Rml::ElementDocument dummy_document{""};
bool add_to_dict = true;
if (document == nullptr) {
document = &dummy_document;
add_to_dict = false;
}
recompui::ContextId ret;
{
std::lock_guard lock{ context_state.all_contexts_lock };
ret = { context_state.all_contexts.emplace(document).raw };
if (add_to_dict) {
context_state.documents_to_contexts.emplace(document, ret);
}
}
return ret;
}
void recompui::init_styling(const std::filesystem::path& rcss_file) {
std::string style{};
{
std::ifstream style_stream{rcss_file};
style_stream.seekg(0, std::ios::end);
style.resize(style_stream.tellg());
style_stream.seekg(0, std::ios::beg);
style_stream.read(style.data(), style.size());
}
std::unique_ptr<Rml::StreamMemory> rml_stream = std::make_unique<Rml::StreamMemory>(reinterpret_cast<Rml::byte*>(style.data()), style.size());
rml_stream->SetSourceURL(rcss_file.filename().string());
context_state.style_sheet = Rml::Factory::InstanceStyleSheetStream(rml_stream.get());
}
recompui::ContextId recompui::create_context(const std::filesystem::path& path) {
ContextId new_context = create_context_impl(nullptr);
auto workingdir = std::filesystem::current_path();
new_context.open();
Rml::ElementDocument* doc = recompui::load_document(path.string());
opened_context->document = doc;
opened_context->root_element.base = doc;
new_context.close();
{
std::lock_guard lock{ context_state.all_contexts_lock };
context_state.documents_to_contexts.emplace(doc, new_context);
}
return new_context;
}
recompui::ContextId recompui::create_context(Rml::ElementDocument* document) {
assert(document != nullptr);
return create_context_impl(document);
}
recompui::ContextId recompui::create_context() {
Rml::ElementDocument* doc = create_empty_document();
doc->SetStyleSheetContainer(context_state.style_sheet);
ContextId ret = create_context_impl(doc);
Element* root = ret.get_root_element();
// Mark the root element as not being a shim, as that's only needed for elements that were parented to Rml ones manually.
root->shim = false;
ret.open();
root->set_width(100.0f, Unit::Percent);
root->set_height(100.0f, Unit::Percent);
root->set_display(Display::Flex);
ret.close();
doc->Hide();
return ret;
}
void recompui::destroy_context(ContextId id) {
bool existed = false;
// TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping
// from the slotmap?
// Check if the provided id exists.
{
std::lock_guard lock{ context_state.all_contexts_lock };
// Check if the target context is currently open.
existed = context_state.all_contexts.has_key(context_slotmap::key{ id.slot_id });
}
// Raise an error if the context didn't exist.
if (!existed) {
context_error(id, ContextErrorType::DestroyInvalidContext);
}
id.open();
id.clear_children();
id.close();
// Delete the provided id.
{
std::lock_guard lock{ context_state.all_contexts_lock };
context_state.all_contexts.erase(context_slotmap::key{ id.slot_id });
}
}
void recompui::destroy_all_contexts() {
recompui::hide_all_contexts();
std::lock_guard lock{ context_state.all_contexts_lock };
// TODO prevent deletion of a context while its mutex is in use. Second lock on the context's mutex before popping
// from the slotmap
std::vector<context_slotmap::key> keys{};
for (const auto& [key, item] : context_state.all_contexts.items()) {
keys.push_back(key);
}
for (auto key : keys) {
Context* ctx = context_state.all_contexts.get(key);
std::lock_guard context_lock{ ctx->context_lock };
opened_context = ctx;
opened_context_id = ContextId{ key };
opened_context_id.clear_children();
opened_context = nullptr;
opened_context_id = ContextId::null();
}
context_state.all_contexts.reset();
context_state.documents_to_contexts.clear();
}
void recompui::ContextId::open() {
// Ensure no other context is opened by this thread already.
if (opened_context_id != ContextId::null()) {
context_error(*this, ContextErrorType::OpenWithoutClose);
}
// Get the context with this id.
Context* ctx;
{
std::lock_guard lock{ context_state.all_contexts_lock };
ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
// If the context was found, add it to the opened contexts.
if (ctx != nullptr) {
context_state.opened_contexts.emplace(*this);
}
}
// Check if the context exists.
if (ctx == nullptr) {
context_error(*this, ContextErrorType::OpenInvalidContext);
}
// Take ownership of the target context.
ctx->context_lock.lock();
opened_context = ctx;
opened_context_id = *this;
}
bool recompui::ContextId::open_if_not_already() {
if (opened_context_id == *this) {
return false;
}
open();
return true;
}
void recompui::ContextId::close() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::CloseWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::CloseWrongContext);
}
// Release ownership of the target context.
opened_context->context_lock.unlock();
opened_context = nullptr;
opened_context_id = ContextId::null();
// Remove this context from the opened contexts.
{
std::lock_guard lock{ context_state.all_contexts_lock };
context_state.opened_contexts.erase(*this);
}
}
recompui::ContextId recompui::try_close_current_context() {
if (opened_context_id != ContextId::null()) {
ContextId prev_context = opened_context_id;
opened_context_id.close();
return prev_context;
}
return ContextId::null();
}
void recompui::ContextId::process_updates() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::InternalError);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::InternalError);
}
// Move the current update set into a local variable. This clears the update set
// and allows it to be used to queue updates from any element callbacks.
std::unordered_set<ResourceId> to_update = std::move(opened_context->to_update);
Event update_event = Event::update_event();
for (auto cur_resource_id : to_update) {
resource_slotmap::key cur_key{ cur_resource_id.slot_id };
// Ignore any resources that aren't elements.
if (cur_key.get_tag() != static_cast<uint8_t>(SlotTag::Element)) {
// Assert to catch errors of queueing other resource types for update.
// This isn't an actual error, so there's no issue with continuing in release builds.
assert(false);
continue;
}
// Get the resource being updaten from the context.
std::unique_ptr<Style>* cur_resource = opened_context->resources.get(cur_key);
// Make sure the resource exists before dispatching the event. It may have been deleted
// after being queued for a update, so just continue to the next element if it doesn't exist.
if (cur_resource == nullptr) {
continue;
}
static_cast<Element*>(cur_resource->get())->handle_event(update_event);
}
}
bool recompui::ContextId::captures_input() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return false;
}
return ctx->captures_input;
}
bool recompui::ContextId::captures_mouse() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return false;
}
return ctx->captures_mouse;
}
void recompui::ContextId::set_captures_input(bool captures_input) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return;
}
ctx->captures_input = captures_input;
}
void recompui::ContextId::set_captures_mouse(bool captures_mouse) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
return;
}
ctx->captures_mouse = captures_mouse;
}
recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&& resource) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::AddResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::AddResourceToWrongContext);
}
bool is_element = resource->is_element();
Style* resource_ptr = resource.get();
auto key = opened_context->resources.emplace(std::move(resource));
if (is_element) {
Element* element_ptr = static_cast<Element*>(resource_ptr);
element_ptr->set_id(std::string{element_ptr->get_type_name()} + "-" + std::to_string(key.raw));
key.set_tag(static_cast<uint8_t>(SlotTag::Element));
// Send one update to the element.
opened_context->to_update.emplace(ResourceId{ key.raw });
}
else {
key.set_tag(static_cast<uint8_t>(SlotTag::Style));
}
resource_ptr->resource_id = { key.raw };
return resource_ptr;
}
void recompui::ContextId::add_loose_element(Element* element) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::AddResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::AddResourceToWrongContext);
}
opened_context->loose_elements.emplace_back(element);
}
void recompui::ContextId::queue_element_update(ResourceId element) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::UpdateElementWithoutContext);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::UpdateElementInWrongContext);
}
// Check that the element that was specified is in the open context.
auto* elementPtr = opened_context->resources.get(resource_slotmap::key{ element.slot_id });
if (elementPtr == nullptr) {
context_error(*this, ContextErrorType::UpdateElementInWrongContext);
}
opened_context->to_update.emplace(element);
}
recompui::Style* recompui::ContextId::create_style() {
return add_resource_impl(std::make_unique<Style>());
}
void recompui::ContextId::destroy_resource(Style* resource) {
destroy_resource(resource->resource_id);
}
void recompui::ContextId::destroy_resource(ResourceId resource) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::DestroyResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::DestroyResourceInWrongContext);
}
// Try to remove the resource from the current context.
auto pop_result = opened_context->resources.pop(resource_slotmap::key{ resource.slot_id });
if (!pop_result.has_value()) {
context_error(*this, ContextErrorType::DestroyResourceNotFound);
}
}
void recompui::ContextId::clear_children() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(*this, ContextErrorType::DestroyResourceWithoutOpen);
}
// Check that the context that was specified is the same one that's currently open.
if (*this != opened_context_id) {
context_error(*this, ContextErrorType::DestroyResourceInWrongContext);
}
// Remove the root element's children.
opened_context->root_element.clear_children();
// Remove any loose resources.
for (Element* e : opened_context->loose_elements) {
destroy_resource(e->resource_id);
}
opened_context->loose_elements.clear();
}
Rml::ElementDocument* recompui::ContextId::get_document() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
context_error(*this, ContextErrorType::GetDocumentInvalidContext);
}
return ctx->document;
}
recompui::Element* recompui::ContextId::get_root_element() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
context_error(*this, ContextErrorType::GetDocumentInvalidContext);
}
return &ctx->root_element;
}
recompui::Element* recompui::ContextId::get_autofocus_element() {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
context_error(*this, ContextErrorType::GetAutofocusInvalidContext);
}
return ctx->autofocus_element;
}
void recompui::ContextId::set_autofocus_element(Element* element) {
std::lock_guard lock{ context_state.all_contexts_lock };
Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id });
if (ctx == nullptr) {
context_error(*this, ContextErrorType::SetAutofocusInvalidContext);
}
ctx->autofocus_element = element;
}
recompui::ContextId recompui::get_current_context() {
// Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) {
context_error(ContextId::null(), ContextErrorType::GetContextWithoutOpen);
}
return opened_context_id;
}
recompui::Style* get_resource_from_current_context(resource_slotmap::key key) {
// Ensure a context is currently opened by this thread.
if (opened_context_id == recompui::ContextId::null()) {
context_error(recompui::ContextId::null(), ContextErrorType::GetResourceWithoutOpen);
}
auto* value = opened_context->resources.get(key);
if (value == nullptr) {
context_error(opened_context_id, ContextErrorType::GetResourceFailed);
}
return value->get();
}
const recompui::Style* recompui::ResourceId::operator*() const {
resource_slotmap::key key{ slot_id };
return get_resource_from_current_context(key);
}
recompui::Style* recompui::ResourceId::operator*() {
resource_slotmap::key key{ slot_id };
return get_resource_from_current_context(key);
}
const recompui::Element* recompui::ResourceId::as_element() const {
resource_slotmap::key key{ slot_id };
uint8_t tag = key.get_tag();
assert(tag == static_cast<uint8_t>(SlotTag::Element));
return static_cast<Element*>(get_resource_from_current_context(key));
}
recompui::Element* recompui::ResourceId::as_element() {
resource_slotmap::key key{ slot_id };
uint8_t tag = key.get_tag();
assert(tag == static_cast<uint8_t>(SlotTag::Element));
return static_cast<Element*>(get_resource_from_current_context(key));
}
recompui::ContextId recompui::get_context_from_document(Rml::ElementDocument* document) {
std::lock_guard lock{ context_state.all_contexts_lock };
auto find_it = context_state.documents_to_contexts.find(document);
if (find_it == context_state.documents_to_contexts.end()) {
return ContextId::null();
}
return find_it->second;
}

69
src/ui/core/ui_context.h Normal file
View File

@@ -0,0 +1,69 @@
#pragma once
#include <cstdint>
#include <memory>
#include <utility>
#include <filesystem>
#include <functional>
#include "RmlUi/Core.h"
#include "ui_resource.h"
namespace recompui {
class Style;
class Element;
class ContextId {
Style* add_resource_impl(std::unique_ptr<Style>&& resource);
public:
uint32_t slot_id;
auto operator<=>(const ContextId& rhs) const = default;
template <typename T, typename... Args>
T* create_element(Args... args) {
return static_cast<T*>(add_resource_impl(std::make_unique<T>(std::forward<Args>(args)...)));
}
template <typename T>
T* create_element(T&& element) {
return static_cast<T*>(add_resource_impl(std::make_unique<T>(std::move(element))));
}
void add_loose_element(Element* element);
void queue_element_update(ResourceId element);
Style* create_style();
void destroy_resource(Style* resource);
void destroy_resource(ResourceId resource);
void clear_children();
Rml::ElementDocument* get_document();
Element* get_root_element();
Element* get_autofocus_element();
void set_autofocus_element(Element* element);
void open();
bool open_if_not_already();
void close();
void process_updates();
static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; }
bool captures_input();
bool captures_mouse();
void set_captures_input(bool captures_input);
void set_captures_mouse(bool captures_input);
};
ContextId create_context(const std::filesystem::path& path);
ContextId create_context(Rml::ElementDocument* document);
ContextId create_context();
void destroy_context(ContextId id);
ContextId get_current_context();
ContextId get_context_from_document(Rml::ElementDocument* document);
void destroy_all_contexts();
void register_ui_exports();
} // namespace recompui

24
src/ui/core/ui_resource.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
namespace recompui {
class Style;
class Element;
struct ResourceId {
uint32_t slot_id;
bool operator==(const ResourceId& rhs) const = default;
const Style* operator*() const;
Style* operator*();
const Style* operator->() const { return *(*this); }
Style* operator->() { return *(*this); }
const Element* as_element() const;
Element* as_element();
static constexpr ResourceId null() { return ResourceId{ uint32_t(-1) }; }
};
} // namespace recompui

View File

@@ -0,0 +1,114 @@
#include "ui_button.h"
#include <cassert>
namespace recompui {
Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "button", true) {
this->style = style;
enable_focus();
set_text(text);
set_display(Display::Block);
set_padding(23.0f);
set_border_width(1.1f);
set_border_radius(12.0f);
set_font_size(28.0f);
set_letter_spacing(3.08f);
set_line_height(28.0f);
set_font_style(FontStyle::Normal);
set_font_weight(700);
set_cursor(Cursor::Pointer);
set_color(Color{ 204, 204, 204, 255 });
set_tab_index(TabIndex::Auto);
hover_style.set_color(Color{ 242, 242, 242, 255 });
focus_style.set_color(Color{ 242, 242, 242, 255 });
disabled_style.set_color(Color{ 204, 204, 204, 128 });
hover_disabled_style.set_color(Color{ 242, 242, 242, 128 });
const uint8_t border_opacity = 204;
const uint8_t background_opacity = 13;
const uint8_t border_hover_opacity = 255;
const uint8_t background_hover_opacity = 76;
switch (style) {
case ButtonStyle::Primary: {
set_border_color({ 185, 125, 242, border_opacity });
set_background_color({ 185, 125, 242, background_opacity });
hover_style.set_border_color({ 185, 125, 242, border_hover_opacity });
hover_style.set_background_color({ 185, 125, 242, background_hover_opacity });
focus_style.set_border_color({ 185, 125, 242, border_hover_opacity });
focus_style.set_background_color({ 185, 125, 242, background_hover_opacity });
disabled_style.set_border_color({ 185, 125, 242, border_opacity / 4 });
disabled_style.set_background_color({ 185, 125, 242, background_opacity / 4 });
hover_disabled_style.set_border_color({ 185, 125, 242, border_hover_opacity / 4 });
hover_disabled_style.set_background_color({ 185, 125, 242, background_hover_opacity / 4 });
break;
}
case ButtonStyle::Secondary: {
set_border_color({ 23, 214, 232, border_opacity });
set_background_color({ 23, 214, 232, background_opacity });
hover_style.set_border_color({ 23, 214, 232, border_hover_opacity });
hover_style.set_background_color({ 23, 214, 232, background_hover_opacity });
focus_style.set_border_color({ 23, 214, 232, border_hover_opacity });
focus_style.set_background_color({ 23, 214, 232, background_hover_opacity });
disabled_style.set_border_color({ 23, 214, 232, border_opacity / 4 });
disabled_style.set_background_color({ 23, 214, 232, background_opacity / 4 });
hover_disabled_style.set_border_color({ 23, 214, 232, border_hover_opacity / 4 });
hover_disabled_style.set_background_color({ 23, 214, 232, background_hover_opacity / 4 });
break;
}
default:
assert(false && "Unknown button style.");
break;
}
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
add_style(&disabled_style, disabled_state);
add_style(&hover_disabled_style, { hover_state, disabled_state });
// transition: color 0.05s linear-in-out, background-color 0.05s linear-in-out;
}
void Button::process_event(const Event &e) {
switch (e.type) {
case EventType::Click:
if (is_enabled()) {
for (const auto &function : pressed_callbacks) {
function();
}
}
break;
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
{
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
}
break;
case EventType::Focus:
set_style_enabled(focus_state, std::get<EventFocus>(e.variant).active);
break;
case EventType::Update:
break;
default:
assert(false && "Unknown event type.");
break;
}
}
void Button::add_pressed_callback(std::function<void()> callback) {
pressed_callbacks.emplace_back(callback);
}
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class ButtonStyle {
Primary,
Secondary
};
class Button : public Element {
protected:
ButtonStyle style = ButtonStyle::Primary;
Style hover_style;
Style focus_style;
Style disabled_style;
Style hover_disabled_style;
std::list<std::function<void()>> pressed_callbacks;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Button"; }
public:
Button(Element *parent, const std::string &text, ButtonStyle style);
void add_pressed_callback(std::function<void()> callback);
Style* get_hover_style() { return &hover_style; }
Style* get_focus_style() { return &focus_style; }
Style* get_disabled_style() { return &disabled_style; }
Style* get_hover_disabled_style() { return &hover_disabled_style; }
};
} // namespace recompui

View File

@@ -0,0 +1,62 @@
#include "ui_clickable.h"
namespace recompui {
Clickable::Clickable(Element *parent, bool draggable) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, draggable ? EventType::Drag : EventType::None)) {
set_cursor(Cursor::Pointer);
if (draggable) {
set_drag(Drag::Drag);
}
}
void Clickable::process_event(const Event &e) {
switch (e.type) {
case EventType::Click: {
if (is_enabled()) {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : pressed_callbacks) {
function(click.x, click.y);
}
break;
}
}
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
break;
case EventType::Enable:
{
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
}
break;
case EventType::Drag: {
if (is_enabled()) {
const EventDrag &drag = std::get<EventDrag>(e.variant);
for (const auto &function : dragged_callbacks) {
function(drag.x, drag.y, drag.phase);
}
break;
}
}
default:
break;
}
}
void Clickable::add_pressed_callback(std::function<void(float, float)> callback) {
pressed_callbacks.emplace_back(callback);
}
void Clickable::add_dragged_callback(std::function<void(float, float, DragPhase)> callback) {
dragged_callbacks.emplace_back(callback);
}
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Clickable : public Element {
protected:
std::vector<std::function<void(float, float)>> pressed_callbacks;
std::vector<std::function<void(float, float, DragPhase)>> dragged_callbacks;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Clickable"; }
public:
Clickable(Element *parent, bool draggable = false);
void add_pressed_callback(std::function<void(float, float)> callback);
void add_dragged_callback(std::function<void(float, float, DragPhase)> callback);
};
} // namespace recompui

View File

@@ -0,0 +1,13 @@
#include "ui_container.h"
#include <cassert>
namespace recompui {
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled) : Element(parent, events_enabled) {
set_display(Display::Flex);
set_flex_direction(direction);
set_justify_content(justify_content);
}
};

View File

@@ -0,0 +1,14 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Container : public Element {
protected:
std::string_view get_type_name() override { return "Container"; }
public:
Container(Element* parent, FlexDirection direction, JustifyContent justify_content, uint32_t events_enabled = 0);
};
} // namespace recompui

View File

@@ -0,0 +1,478 @@
#include "RmlUi/Core/StringUtilities.h"
#include "overloaded.h"
#include "recomp_ui.h"
#include "ui_element.h"
#include "../core/ui_context.h"
#include <cassert>
namespace recompui {
Element::Element(Rml::Element *base) {
assert(base != nullptr);
this->base = base;
this->base_owning = {};
this->shim = true;
}
Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class, bool can_set_text) : can_set_text(can_set_text) {
ContextId context = get_current_context();
base_owning = context.get_document()->CreateElement(base_class);
if (parent != nullptr) {
base = parent->base->AppendChild(std::move(base_owning));
parent->add_child(this);
}
else {
base = base_owning.get();
}
set_display(Display::Block);
set_property(Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox);
register_event_listeners(events_enabled);
}
Element::~Element() {
if (!shim) {
clear_children();
if (!base_owning) {
base->GetParentNode()->RemoveChild(base);
}
}
}
void Element::add_child(Element *child) {
assert(child != nullptr);
if (can_set_text) {
assert(false && "Elements with settable text cannot have children");
return;
}
children.emplace_back(child);
if (shim) {
ContextId context = get_current_context();
context.add_loose_element(child);
}
}
void Element::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
assert(base != nullptr);
base->SetProperty(property_id, property);
Style::set_property(property_id, property);
}
void Element::register_event_listeners(uint32_t events_enabled) {
assert(base != nullptr);
this->events_enabled = events_enabled;
if (events_enabled & Events(EventType::Click)) {
base->AddEventListener(Rml::EventId::Click, this);
}
if (events_enabled & Events(EventType::Focus)) {
base->AddEventListener(Rml::EventId::Focus, this);
base->AddEventListener(Rml::EventId::Blur, this);
}
if (events_enabled & Events(EventType::Hover)) {
base->AddEventListener(Rml::EventId::Mouseover, this);
base->AddEventListener(Rml::EventId::Mouseout, this);
}
if (events_enabled & Events(EventType::Drag)) {
base->AddEventListener(Rml::EventId::Drag, this);
base->AddEventListener(Rml::EventId::Dragstart, this);
base->AddEventListener(Rml::EventId::Dragend, this);
}
if (events_enabled & Events(EventType::Text)) {
base->AddEventListener(Rml::EventId::Change, this);
}
if (events_enabled & Events(EventType::Navigate)) {
base->AddEventListener(Rml::EventId::Keydown, this);
}
}
void Element::apply_style(Style *style) {
for (auto it : style->property_map) {
// Skip redundant SetProperty calls to prevent dirtying unnecessary state.
// This avoids expensive layout operations when a simple color-only style is applied.
const Rml::Property* cur_value = base->GetLocalProperty(it.first);
if (*cur_value != it.second) {
base->SetProperty(it.first, it.second);
}
}
}
void Element::apply_styles() {
apply_style(this);
for (size_t i = 0; i < styles_counter.size(); i++) {
if (styles_counter[i] == 0) {
apply_style(styles[i]);
}
}
}
void Element::propagate_disabled(bool disabled) {
disabled_from_parent = disabled;
bool attribute_state = disabled_from_parent || !enabled;
if (disabled_attribute != attribute_state) {
disabled_attribute = attribute_state;
base->SetAttribute("disabled", attribute_state);
if (events_enabled & Events(EventType::Enable)) {
handle_event(Event::enable_event(!attribute_state));
}
for (auto &child : children) {
child->propagate_disabled(attribute_state);
}
}
}
void Element::handle_event(const Event& event) {
for (const auto& callback : callbacks) {
recompui::queue_ui_callback(resource_id, event, callback);
}
process_event(event);
}
void Element::set_id(const std::string& new_id) {
id = new_id;
base->SetId(new_id);
}
void Element::ProcessEvent(Rml::Event &event) {
ContextId prev_context = recompui::try_close_current_context();
ContextId context = ContextId::null();
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
if (doc != nullptr) {
context = get_context_from_document(doc);
}
bool did_open = false;
// TODO disallow null contexts once the entire UI system has been migrated.
if (context != ContextId::null()) {
did_open = context.open_if_not_already();
}
// Events that are processed during any phase.
switch (event.GetId()) {
case Rml::EventId::Click:
handle_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
break;
case Rml::EventId::Keydown:
switch ((Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0)) {
case Rml::Input::KeyIdentifier::KI_LEFT:
handle_event(Event::navigate_event(NavDirection::Left));
break;
case Rml::Input::KeyIdentifier::KI_UP:
handle_event(Event::navigate_event(NavDirection::Up));
break;
case Rml::Input::KeyIdentifier::KI_RIGHT:
handle_event(Event::navigate_event(NavDirection::Right));
break;
case Rml::Input::KeyIdentifier::KI_DOWN:
handle_event(Event::navigate_event(NavDirection::Down));
break;
}
break;
case Rml::EventId::Drag:
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
break;
default:
break;
}
// Events that are only processed during the Target phase.
if (event.GetPhase() == Rml::EventPhase::Target) {
switch (event.GetId()) {
case Rml::EventId::Mouseover:
handle_event(Event::hover_event(true));
break;
case Rml::EventId::Mouseout:
handle_event(Event::hover_event(false));
break;
case Rml::EventId::Focus:
handle_event(Event::focus_event(true));
break;
case Rml::EventId::Blur:
handle_event(Event::focus_event(false));
break;
case Rml::EventId::Dragstart:
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
break;
case Rml::EventId::Dragend:
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End));
break;
case Rml::EventId::Change: {
if (events_enabled & Events(EventType::Text)) {
Rml::Variant *value_variant = base->GetAttribute("value");
if (value_variant != nullptr) {
handle_event(Event::text_event(value_variant->Get<std::string>()));
}
}
break;
}
default:
break;
}
}
if (context != ContextId::null() && did_open) {
context.close();
}
if (prev_context != ContextId::null()) {
prev_context.open();
}
}
void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) {
base->SetAttribute(attribute_key, attribute_value);
}
void Element::process_event(const Event &) {
// Does nothing by default.
}
void Element::enable_focus() {
set_tab_index_auto();
set_focusable(true);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_nav_auto(NavDirection::Left);
set_nav_auto(NavDirection::Right);
}
void Element::clear_children() {
if (children.empty()) {
return;
}
ContextId context = get_current_context();
// Remove the children from the context.
for (Element* child : children) {
context.destroy_resource(child);
}
// Clear the child list.
children.clear();
}
bool Element::remove_child(ResourceId child) {
bool found = false;
ContextId context = get_current_context();
for (auto it = children.begin(); it != children.end(); ++it) {
Element* cur_child = *it;
if (cur_child->get_resource_id() == child) {
children.erase(it);
context.destroy_resource(cur_child);
found = true;
break;
}
}
return found;
}
void Element::add_style(Style *style, const std::string_view style_name) {
add_style(style, { style_name });
}
void Element::add_style(Style *style, const std::initializer_list<std::string_view> &style_names) {
for (const std::string_view &style_name : style_names) {
style_name_index_map.emplace(style_name, styles.size());
}
styles.emplace_back(style);
uint32_t initial_style_counter = style_names.size();
for (const std::string_view &style_name : style_names) {
if (style_active_set.find(style_name) != style_active_set.end()) {
initial_style_counter--;
}
}
styles_counter.push_back(initial_style_counter);
}
void Element::set_enabled(bool enabled) {
this->enabled = enabled;
propagate_disabled(disabled_from_parent);
}
bool Element::is_enabled() const {
return enabled && !disabled_from_parent;
}
// Adapted from RmlUi's `EncodeRml`.
std::string escape_rml(std::string_view string)
{
std::string result;
result.reserve(string.size());
for (char c : string)
{
switch (c)
{
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '&': result += "&amp;"; break;
case '"': result += "&quot;"; break;
case '\n': result += "<br/>"; break;
default: result += c; break;
}
}
return result;
}
void Element::set_text(std::string_view text) {
if (can_set_text) {
// Escape the string into Rml to prevent element injection.
base->SetInnerRML(escape_rml(text));
}
else {
assert(false && "Attempted to set text of an element that cannot have its text set.");
}
}
std::string Element::get_input_text() {
return base->GetAttribute("value", std::string{});
}
void Element::set_input_text(std::string_view val) {
base->SetAttribute("value", std::string{ val });
}
void Element::set_src(std::string_view src) {
base->SetAttribute("src", std::string(src));
}
void Element::set_style_enabled(std::string_view style_name, bool enable) {
if (enable && style_active_set.find(style_name) == style_active_set.end()) {
// Style was disabled and will be enabled.
style_active_set.emplace(style_name);
}
else if (!enable && style_active_set.find(style_name) != style_active_set.end()) {
// Style was enabled and will be disabled.
style_active_set.erase(style_name);
}
else {
// Do nothing.
return;
}
auto range = style_name_index_map.equal_range(style_name);
for (auto it = range.first; it != range.second; it++) {
if (enable) {
styles_counter[it->second]--;
}
else {
styles_counter[it->second]++;
}
}
apply_styles();
}
bool Element::is_style_enabled(std::string_view style_name) {
return style_active_set.contains(style_name);
}
float Element::get_absolute_left() {
return base->GetAbsoluteLeft();
}
float Element::get_absolute_top() {
return base->GetAbsoluteTop();
}
float Element::get_client_left() {
return base->GetClientLeft();
}
float Element::get_client_top() {
return base->GetClientTop();
}
float Element::get_client_width() {
return base->GetClientWidth();
}
float Element::get_client_height() {
return base->GetClientHeight();
}
uint32_t Element::get_input_value_u32() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return (uint32_t)d; },
[](float f) { return (uint32_t)f; },
[](uint32_t u) { return u; },
[](std::monostate) { return 0U; }
}, value);
}
float Element::get_input_value_float() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return (float)d; },
[](float f) { return f; },
[](uint32_t u) { return (float)u; },
[](std::monostate) { return 0.0f; }
}, value);
}
double Element::get_input_value_double() {
ElementValue value = get_element_value();
return std::visit(overloaded {
[](double d) { return d; },
[](float f) { return (double)f; },
[](uint32_t u) { return (double)u; },
[](std::monostate) { return 0.0; }
}, value);
}
void Element::focus() {
base->Focus();
}
void Element::blur() {
base->Blur();
}
void Element::queue_update() {
ContextId cur_context = get_current_context();
// TODO disallow null contexts once the entire UI system has been migrated.
if (cur_context == ContextId::null()) {
return;
}
cur_context.queue_element_update(resource_id);
}
void Element::register_callback(ContextId context, PTR(void) callback, PTR(void) userdata) {
callbacks.emplace_back(UICallback{.context = context, .callback = callback, .userdata = userdata});
}
}

View File

@@ -0,0 +1,106 @@
#pragma once
#include "ui_style.h"
#include "../core/ui_context.h"
#include "recomp.h"
#include <ultramodern/ultra64.h>
#include <unordered_set>
#include <variant>
namespace recompui {
struct UICallback {
ContextId context;
PTR(void) callback;
PTR(void) userdata;
};
using ElementValue = std::variant<uint32_t, float, double, std::monostate>;
class ContextId;
class Element : public Style, public Rml::EventListener {
friend ContextId create_context(const std::filesystem::path& path);
friend ContextId create_context();
friend class ContextId; // To allow ContextId to call the handle_event method directly.
private:
Rml::Element *base = nullptr;
Rml::ElementPtr base_owning = {};
uint32_t events_enabled = 0;
std::vector<Style *> styles;
std::vector<uint32_t> styles_counter;
std::unordered_set<std::string_view> style_active_set;
std::unordered_multimap<std::string_view, uint32_t> style_name_index_map;
std::vector<UICallback> callbacks;
std::vector<Element *> children;
std::string id;
bool shim = false;
bool enabled = true;
bool disabled_attribute = false;
bool disabled_from_parent = false;
bool can_set_text = false;
void add_child(Element *child);
void register_event_listeners(uint32_t events_enabled);
void apply_style(Style *style);
void propagate_disabled(bool disabled);
void handle_event(const Event &e);
void set_id(const std::string& new_id);
// Style overrides.
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override;
// Rml::EventListener overrides.
void ProcessEvent(Rml::Event &event) override final;
protected:
// Use of this method in inherited classes is discouraged unless it's necessary.
void set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value);
virtual void process_event(const Event &e);
virtual ElementValue get_element_value() { return std::monostate{}; }
virtual void set_input_value(const ElementValue&) {}
virtual std::string_view get_type_name() { return "Element"; }
public:
// Used for backwards compatibility with legacy UI elements.
Element(Rml::Element *base);
// Used to actually construct elements.
Element(Element* parent, uint32_t events_enabled = 0, Rml::String base_class = "div", bool can_set_text = false);
virtual ~Element();
void clear_children();
bool remove_child(ResourceId child);
bool remove_child(Element *child) { return remove_child(child->get_resource_id()); }
void add_style(Style *style, std::string_view style_name);
void add_style(Style *style, const std::initializer_list<std::string_view> &style_names);
void set_enabled(bool enabled);
bool is_enabled() const;
void set_text(std::string_view text);
std::string get_input_text();
void set_input_text(std::string_view text);
void set_src(std::string_view src);
void set_style_enabled(std::string_view style_name, bool enabled);
bool is_style_enabled(std::string_view style_name);
void apply_styles();
bool is_element() override { return true; }
float get_absolute_left();
float get_absolute_top();
float get_client_left();
float get_client_top();
float get_client_width();
float get_client_height();
void enable_focus();
void focus();
void blur();
void queue_update();
void register_callback(ContextId context, PTR(void) callback, PTR(void) userdata);
uint32_t get_input_value_u32();
float get_input_value_float();
double get_input_value_double();
void set_input_value_u32(uint32_t val) { set_input_value(val); }
void set_input_value_float(float val) { set_input_value(val); }
void set_input_value_double(double val) { set_input_value(val); }
const std::string& get_id() { return id; }
};
void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback);
} // namespace recompui

View File

@@ -0,0 +1,11 @@
#include "ui_image.h"
#include <cassert>
namespace recompui {
Image::Image(Element *parent, std::string_view src) : Element(parent, 0, "img") {
set_src(src);
}
};

View File

@@ -0,0 +1,14 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Image : public Element {
protected:
std::string_view get_type_name() override { return "ImageView"; }
public:
Image(Element *parent, std::string_view src);
};
} // namespace recompui

View File

@@ -0,0 +1,43 @@
#include "ui_label.h"
#include <cassert>
namespace recompui {
Label::Label(Element *parent, LabelStyle label_style) : Element(parent, 0U, "div", true) {
switch (label_style) {
case LabelStyle::Annotation:
set_color(Color{ 185, 125, 242, 255 });
set_font_size(18.0f);
set_letter_spacing(2.52f);
set_line_height(18.0f);
set_font_weight(400);
break;
case LabelStyle::Small:
set_font_size(20.0f);
set_letter_spacing(0.0f);
set_line_height(20.0f);
set_font_weight(400);
break;
case LabelStyle::Normal:
set_font_size(28.0f);
set_letter_spacing(3.08f);
set_line_height(28.0f);
set_font_weight(700);
break;
case LabelStyle::Large:
set_font_size(36.0f);
set_letter_spacing(2.52f);
set_line_height(36.0f);
set_font_weight(700);
break;
}
set_font_style(FontStyle::Normal);
}
Label::Label(Element *parent, const std::string &text, LabelStyle label_style) : Label(parent, label_style) {
set_text(text);
}
};

View File

@@ -0,0 +1,22 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class LabelStyle {
Annotation,
Small,
Normal,
Large
};
class Label : public Element {
protected:
std::string_view get_type_name() override { return "Label"; }
public:
Label(Element *parent, LabelStyle label_style);
Label(Element *parent, const std::string &text, LabelStyle label_style);
};
} // namespace recompui

View File

@@ -0,0 +1,240 @@
#include "overloaded.h"
#include "ui_radio.h"
#include "../ui_utils.h"
namespace recompui {
// RadioOption
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable, EventType::Update), "label", true) {
this->index = index;
enable_focus();
set_text(name);
set_cursor(Cursor::Pointer);
set_font_size(20.0f);
set_letter_spacing(2.8f);
set_line_height(20.0f);
set_font_weight(400);
set_font_style(FontStyle::Normal);
set_border_color(Color{ 242, 242, 242, 0 });
set_border_bottom_width(1.0f);
set_color(Color{ 255, 255, 255, 153 });
set_padding_bottom(8.0f);
set_text_transform(TextTransform::Uppercase);
set_height_auto();
hover_style.set_color(Color{ 255, 255, 255, 204 });
checked_style.set_color(Color{ 255, 255, 255, 255 });
checked_style.set_border_color(Color{ 242, 242, 242, 255 });
pulsing_style.set_border_color(Color{ 23, 214, 232, 244 });
add_style(&hover_style, { hover_state });
add_style(&checked_style, { checked_state });
add_style(&pulsing_style, { focus_state });
}
void RadioOption::set_pressed_callback(std::function<void(uint32_t)> callback) {
pressed_callback = callback;
}
void RadioOption::set_selected_state(bool enable) {
set_style_enabled(checked_state, enable);
}
void RadioOption::process_event(const Event &e) {
switch (e.type) {
case EventType::Click:
pressed_callback(index);
break;
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
break;
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(e.variant).active);
break;
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, active);
if (active) {
queue_update();
}
}
break;
case EventType::Update:
if (is_style_enabled(focus_state)) {
pulsing_style.set_color(recompui::get_pulse_color(750));
apply_styles();
queue_update();
}
if (focus_queued) {
focus_queued = false;
focus();
}
break;
default:
break;
}
}
// Radio
void Radio::set_index_internal(uint32_t index, bool setup, bool trigger_callbacks) {
if (this->index != index || setup) {
options[this->index]->set_selected_state(false);
this->index = index;
options[index]->set_selected_state(true);
if (trigger_callbacks) {
for (const auto &function : index_changed_callbacks) {
function(index);
}
}
}
}
void Radio::option_selected(uint32_t index) {
set_index_internal(index, false, true);
}
void Radio::set_input_value(const ElementValue& val) {
std::visit(overloaded {
[this](uint32_t u) { set_index(u); },
[this](float f) { set_index(f); },
[this](double d) { set_index(d); },
[](std::monostate) {}
}, val);
}
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus)) {
set_gap(24.0f);
set_align_items(AlignItems::FlexStart);
enable_focus();
}
void Radio::process_event(const Event &e) {
switch (e.type) {
case EventType::Focus:
if (!options.empty()) {
if (std::get<EventFocus>(e.variant).active) {
blur();
options[index]->queue_focus();
}
}
break;
}
}
Radio::~Radio() {
}
void Radio::add_option(std::string_view name) {
RadioOption *option = get_current_context().create_element<RadioOption>(this, name, uint32_t(options.size()));
option->set_pressed_callback([this](uint32_t index){ option_selected(index); });
options.emplace_back(option);
// The first option was added, select it.
if (options.size() == 1) {
set_index_internal(0, true, false);
}
// At least one other option already existed, so set up navigation.
else {
options[options.size() - 2]->set_nav(NavDirection::Right, options[options.size() - 1]);
options[options.size() - 1]->set_nav(NavDirection::Left, options[options.size() - 2]);
}
}
void Radio::set_index(uint32_t index) {
set_index_internal(index, false, false);
}
uint32_t Radio::get_index() const {
return index;
}
void Radio::add_index_changed_callback(std::function<void(uint32_t)> callback) {
index_changed_callbacks.emplace_back(callback);
}
void Radio::set_nav_auto(NavDirection dir) {
Element::set_nav_auto(dir);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_auto(dir);
}
break;
case NavDirection::Left:
options.front()->set_nav_auto(dir);
break;
case NavDirection::Right:
options.back()->set_nav_auto(dir);
break;
}
}
}
void Radio::set_nav_none(NavDirection dir) {
Element::set_nav_none(dir);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_none(dir);
}
break;
case NavDirection::Left:
options.front()->set_nav_none(dir);
break;
case NavDirection::Right:
options.back()->set_nav_none(dir);
break;
}
}
}
void Radio::set_nav(NavDirection dir, Element* element) {
Element::set_nav(dir, element);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav(dir, element);
}
break;
case NavDirection::Left:
options.front()->set_nav(dir, element);
break;
case NavDirection::Right:
options.back()->set_nav(dir, element);
break;
}
}
}
void Radio::set_nav_manual(NavDirection dir, const std::string& target) {
Element::set_nav_manual(dir, target);
if (!options.empty()) {
switch (dir) {
case NavDirection::Up:
case NavDirection::Down:
for (Element* e : options) {
e->set_nav_manual(dir, target);
}
break;
case NavDirection::Left:
options.front()->set_nav_manual(dir, target);
break;
case NavDirection::Right:
options.back()->set_nav_manual(dir, target);
break;
}
}
}
};

View File

@@ -0,0 +1,54 @@
#pragma once
#include "ui_container.h"
#include "ui_label.h"
namespace recompui {
class RadioOption : public Element {
private:
Style hover_style;
Style checked_style;
Style pulsing_style;
std::function<void(uint32_t)> pressed_callback = nullptr;
uint32_t index = 0;
bool focus_queued = false;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "LabelRadioOption"; }
public:
RadioOption(Element *parent, std::string_view name, uint32_t index);
void set_pressed_callback(std::function<void(uint32_t)> callback);
void set_selected_state(bool enable);
void queue_focus() { focus_queued = true; queue_update(); }
};
class Radio : public Container {
private:
std::vector<RadioOption *> options;
uint32_t index = 0;
std::vector<std::function<void(uint32_t)>> index_changed_callbacks;
void set_index_internal(uint32_t index, bool setup, bool trigger_callbacks);
void option_selected(uint32_t index);
void set_input_value(const ElementValue& val) override;
ElementValue get_element_value() override { return get_index(); }
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "LabelRadio"; }
public:
Radio(Element *parent);
virtual ~Radio();
void add_option(std::string_view name);
void set_index(uint32_t index);
uint32_t get_index() const;
void add_index_changed_callback(std::function<void(uint32_t)> callback);
size_t num_options() const { return options.size(); }
RadioOption* get_option_element(size_t option_index) { return options[option_index]; }
RadioOption* get_current_option_element() { return options.empty() ? nullptr : options[index]; }
void set_nav_auto(NavDirection dir) override;
void set_nav_none(NavDirection dir) override;
void set_nav(NavDirection dir, Element* element) override;
void set_nav_manual(NavDirection dir, const std::string& target) override;
};
} // namespace recompui

View File

@@ -0,0 +1,27 @@
#include "ui_scroll_container.h"
#include <cassert>
namespace recompui {
ScrollContainer::ScrollContainer(Element *parent, ScrollDirection direction) : Element(parent) {
set_flex(1.0f, 1.0f, 100.0f);
set_width(100.0f, Unit::Percent);
set_height(100.0f, Unit::Percent);
switch (direction) {
case ScrollDirection::Horizontal:
set_max_width(100.0f, Unit::Percent);
set_overflow_x(Overflow::Auto);
break;
case ScrollDirection::Vertical:
set_max_height(100.0f, Unit::Percent);
set_overflow_y(Overflow::Auto);
break;
default:
assert(false && "Unknown scroll direction.");
break;
}
}
};

View File

@@ -0,0 +1,19 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class ScrollDirection {
Horizontal,
Vertical
};
class ScrollContainer : public Element {
protected:
std::string_view get_type_name() override { return "ScrollContainer"; }
public:
ScrollContainer(Element *parent, ScrollDirection direction);
};
} // namespace recompui

View File

@@ -0,0 +1,236 @@
#include "overloaded.h"
#include "ui_slider.h"
#include "../ui_utils.h"
#include <cmath>
#include <charconv>
namespace recompui {
void Slider::set_value_internal(double v, bool setup, bool trigger_callbacks) {
if (step_value != 0.0) {
v = std::lround(v / step_value) * step_value;
}
if (value != v || setup) {
value = v;
update_circle_position();
update_label_text();
if (trigger_callbacks) {
for (auto callback : value_changed_callbacks) {
callback(v);
}
}
}
}
void Slider::bar_clicked(float x, float) {
update_value_from_mouse(x);
}
void Slider::bar_dragged(float x, float, DragPhase) {
update_value_from_mouse(x);
}
void Slider::circle_dragged(float x, float, DragPhase) {
update_value_from_mouse(x);
}
void Slider::update_value_from_mouse(float x) {
double left = slider_element->get_absolute_left();
double width = slider_element->get_client_width();
double ratio = std::clamp((x - left) / width, 0.0, 1.0);
set_value_internal(min_value + ratio * (max_value - min_value), false, true);
}
void Slider::update_circle_position() {
double ratio = std::clamp((value - min_value) / (max_value - min_value), 0.0, 1.0);
circle_element->set_left(ratio * 100.0, Unit::Percent);
}
void Slider::update_label_text() {
char text_buffer[32];
if (type == SliderType::Double) {
std::snprintf(text_buffer, sizeof(text_buffer), "%.1f", value);
} else if (type == SliderType::Percent) {
std::snprintf(text_buffer, sizeof(text_buffer), "%d%%", static_cast<int>(value));
} else {
std::snprintf(text_buffer, sizeof(text_buffer), "%d", static_cast<int>(value));
}
value_label->set_text(text_buffer);
}
void Slider::set_input_value(const ElementValue& val) {
std::visit(overloaded {
[this](uint32_t u) { set_value(u); },
[this](float f) { set_value(f); },
[this](double d) { set_value(d); },
[](std::monostate) {}
}, val);
}
void Slider::process_event(const Event& e) {
switch (e.type) {
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
circle_element->set_style_enabled(focus_state, active);
if (active) {
queue_update();
}
}
break;
case EventType::Update:
if (is_enabled()) {
if (circle_element->is_style_enabled(focus_state)) {
circle_element->set_background_color(recompui::get_pulse_color(750));
queue_update();
}
else {
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
}
}
else {
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
}
break;
case EventType::Navigate:
{
NavDirection dir = std::get<EventNavigate>(e.variant).direction;
if (dir == NavDirection::Left) {
do_step(false);
}
else if (dir == NavDirection::Right) {
do_step(true);
}
}
break;
case EventType::Enable:
{
bool enable_active = std::get<EventEnable>(e.variant).active;
circle_element->set_enabled(enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
}
else {
set_cursor(Cursor::None);
set_focusable(false);
circle_element->set_background_color(Color{ 102, 102, 102, 255 });
}
}
break;
default:
break;
}
}
Slider::Slider(Element *parent, SliderType type) : Element(parent, Events(EventType::Focus, EventType::Update, EventType::Navigate, EventType::Enable)) {
this->type = type;
set_cursor(Cursor::Pointer);
set_display(Display::Flex);
set_flex_direction(FlexDirection::Row);
set_text_align(TextAlign::Left);
set_min_width(120.0f);
enable_focus();
set_nav_none(NavDirection::Left);
set_nav_none(NavDirection::Right);
ContextId context = get_current_context();
value_label = context.create_element<Label>(this, "0", LabelStyle::Small);
value_label->set_margin_right(20.0f);
value_label->set_min_width(60.0f);
value_label->set_max_width(60.0f);
slider_element = context.create_element<Clickable>(this, true);
slider_element->set_flex(1.0f, 0.0f);
slider_element->add_pressed_callback([this](float x, float y){ bar_clicked(x, y); focus(); });
slider_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
{
bar_element = context.create_element<Clickable>(slider_element, true);
bar_element->set_width(100.0f, Unit::Percent);
bar_element->set_height(2.0f);
bar_element->set_margin_top(8.0f);
bar_element->set_background_color(Color{ 255, 255, 255, 50 });
bar_element->add_pressed_callback([this](float x, float y){ bar_clicked(x, y); focus(); });
bar_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ bar_dragged(x, y, phase); focus(); });
circle_element = context.create_element<Clickable>(bar_element, true);
circle_element->set_position(Position::Relative);
circle_element->set_width(16.0f);
circle_element->set_height(16.0f);
circle_element->set_margin_top(-7.0f);
circle_element->set_margin_right(-8.0f);
circle_element->set_margin_left(-8.0f);
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
circle_element->set_border_radius(8.0f);
circle_element->add_pressed_callback([this](float, float){ focus(); });
circle_element->add_dragged_callback([this](float x, float y, recompui::DragPhase phase){ circle_dragged(x, y, phase); focus(); });
circle_element->set_cursor(Cursor::Pointer);
}
set_value_internal(value, true, false);
}
Slider::~Slider() {
}
void Slider::set_value(double v) {
set_value_internal(v, false, false);
}
double Slider::get_value() const {
return value;
}
void Slider::set_min_value(double v) {
min_value = v;
}
double Slider::get_min_value() const {
return min_value;
}
void Slider::set_max_value(double v) {
max_value = v;
}
double Slider::get_max_value() const {
return max_value;
}
void Slider::set_step_value(double v) {
step_value = v;
}
double Slider::get_step_value() const {
return step_value;
}
void Slider::add_value_changed_callback(std::function<void(double)> callback) {
value_changed_callbacks.emplace_back(callback);
}
void Slider::do_step(bool increment) {
double new_value = value;
if (increment) {
new_value += step_value;
}
else {
new_value -= step_value;
}
new_value = std::clamp(new_value, min_value, max_value);
if (new_value != value) {
set_value_internal(new_value, false, true);
}
}
} // namespace recompui

View File

@@ -0,0 +1,56 @@
#pragma once
#include "ui_clickable.h"
#include "ui_label.h"
namespace recompui {
enum SliderType {
Double,
Percent,
Integer
};
class Slider : public Element {
private:
SliderType type = SliderType::Percent;
Label *value_label = nullptr;
Clickable *slider_element = nullptr;
Clickable *bar_element = nullptr;
Clickable *circle_element = nullptr;
double value = 50.0;
double min_value = 0.0;
double max_value = 100.0;
double step_value = 0.0;
std::vector<std::function<void(double)>> value_changed_callbacks;
void set_value_internal(double v, bool setup, bool trigger_callbacks);
void bar_clicked(float x, float y);
void bar_dragged(float x, float y, DragPhase phase);
void circle_dragged(float x, float y, DragPhase phase);
void update_value_from_mouse(float x);
void update_circle_position();
void update_label_text();
void set_input_value(const ElementValue& val) override;
ElementValue get_element_value() override { return get_value(); }
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Slider"; }
public:
Slider(Element *parent, SliderType type);
virtual ~Slider();
void set_value(double v);
double get_value() const;
void set_min_value(double v);
double get_min_value() const;
void set_max_value(double v);
double get_max_value() const;
void set_step_value(double v);
double get_step_value() const;
void add_value_changed_callback(std::function<void(double)> callback);
void do_step(bool increment);
};
} // namespace recompui

View File

@@ -0,0 +1,15 @@
#include "ui_span.h"
#include <cassert>
namespace recompui {
Span::Span(Element *parent) : Element(parent, 0, "span", true) {
set_font_style(FontStyle::Normal);
}
Span::Span(Element *parent, const std::string &text) : Span(parent) {
set_text(text);
}
};

16
src/ui/elements/ui_span.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "ui_element.h"
#include "ui_label.h"
namespace recompui {
class Span : public Element {
protected:
std::string_view get_type_name() override { return "Span"; }
public:
Span(Element *parent);
Span(Element *parent, const std::string &text);
};
} // namespace recompui

View File

@@ -0,0 +1,612 @@
#include "ui_style.h"
#include "ui_element.h"
#include <cassert>
namespace recompui {
static Rml::Unit to_rml(Unit unit) {
switch (unit) {
case Unit::Px:
return Rml::Unit::PX;
case Unit::Dp:
return Rml::Unit::DP;
case Unit::Percent:
return Rml::Unit::PERCENT;
default:
return Rml::Unit::UNKNOWN;
}
}
static Rml::Style::AlignItems to_rml(AlignItems align_items) {
switch (align_items) {
case AlignItems::FlexStart:
return Rml::Style::AlignItems::FlexStart;
case AlignItems::FlexEnd:
return Rml::Style::AlignItems::FlexEnd;
case AlignItems::Center:
return Rml::Style::AlignItems::Center;
case AlignItems::Baseline:
return Rml::Style::AlignItems::Baseline;
case AlignItems::Stretch:
return Rml::Style::AlignItems::Stretch;
default:
assert(false && "Unknown align items.");
return Rml::Style::AlignItems::FlexStart;
}
}
static Rml::Style::Overflow to_rml(Overflow overflow) {
switch (overflow) {
case Overflow::Visible:
return Rml::Style::Overflow::Visible;
case Overflow::Hidden:
return Rml::Style::Overflow::Hidden;
case Overflow::Auto:
return Rml::Style::Overflow::Auto;
case Overflow::Scroll:
return Rml::Style::Overflow::Scroll;
default:
assert(false && "Unknown overflow.");
return Rml::Style::Overflow::Visible;
}
}
static Rml::Style::TextAlign to_rml(TextAlign text_align) {
switch (text_align) {
case TextAlign::Left:
return Rml::Style::TextAlign::Left;
case TextAlign::Right:
return Rml::Style::TextAlign::Right;
case TextAlign::Center:
return Rml::Style::TextAlign::Center;
case TextAlign::Justify:
return Rml::Style::TextAlign::Justify;
default:
assert(false && "Unknown text align.");
return Rml::Style::TextAlign::Left;
}
}
static Rml::Style::TextTransform to_rml(TextTransform text_transform) {
switch (text_transform) {
case TextTransform::None:
return Rml::Style::TextTransform::None;
case TextTransform::Capitalize:
return Rml::Style::TextTransform::Capitalize;
case TextTransform::Uppercase:
return Rml::Style::TextTransform::Uppercase;
case TextTransform::Lowercase:
return Rml::Style::TextTransform::Lowercase;
default:
assert(false && "Unknown text transform.");
return Rml::Style::TextTransform::None;
}
}
static Rml::Style::Drag to_rml(Drag drag) {
switch (drag) {
case Drag::None:
return Rml::Style::Drag::None;
case Drag::Drag:
return Rml::Style::Drag::Drag;
case Drag::DragDrop:
return Rml::Style::Drag::DragDrop;
case Drag::Block:
return Rml::Style::Drag::Block;
case Drag::Clone:
return Rml::Style::Drag::Clone;
default:
assert(false && "Unknown drag.");
return Rml::Style::Drag::None;
}
}
static Rml::Style::TabIndex to_rml(TabIndex tab_index) {
switch (tab_index) {
case TabIndex::None:
return Rml::Style::TabIndex::None;
case TabIndex::Auto:
return Rml::Style::TabIndex::Auto;
default:
assert(false && "Unknown tab index.");
return Rml::Style::TabIndex::None;
}
}
static Rml::Style::Display to_rml(Display display) {
switch (display) {
case Display::None:
return Rml::Style::Display::None;
case Display::Block:
return Rml::Style::Display::Block;
case Display::Inline:
return Rml::Style::Display::Inline;
case Display::InlineBlock:
return Rml::Style::Display::InlineBlock;
case Display::FlowRoot:
return Rml::Style::Display::FlowRoot;
case Display::Flex:
return Rml::Style::Display::Flex;
case Display::InlineFlex:
return Rml::Style::Display::InlineFlex;
case Display::Table:
return Rml::Style::Display::Table;
case Display::InlineTable:
return Rml::Style::Display::InlineTable;
case Display::TableRow:
return Rml::Style::Display::TableRow;
case Display::TableRowGroup:
return Rml::Style::Display::TableRowGroup;
case Display::TableColumn:
return Rml::Style::Display::TableColumn;
case Display::TableColumnGroup:
return Rml::Style::Display::TableColumnGroup;
case Display::TableCell:
return Rml::Style::Display::TableCell;
default:
assert(false && "Unknown display.");
return Rml::Style::Display::Block;
}
}
static Rml::Style::JustifyContent to_rml(JustifyContent justify_content) {
switch (justify_content) {
case JustifyContent::FlexStart:
return Rml::Style::JustifyContent::FlexStart;
case JustifyContent::FlexEnd:
return Rml::Style::JustifyContent::FlexEnd;
case JustifyContent::Center:
return Rml::Style::JustifyContent::Center;
case JustifyContent::SpaceBetween:
return Rml::Style::JustifyContent::SpaceBetween;
case JustifyContent::SpaceAround:
return Rml::Style::JustifyContent::SpaceAround;
case JustifyContent::SpaceEvenly:
return Rml::Style::JustifyContent::SpaceEvenly;
default:
assert(false && "Unknown justify content.");
return Rml::Style::JustifyContent::FlexStart;
}
}
static Rml::PropertyId nav_to_property(NavDirection dir) {
switch (dir) {
case NavDirection::Up:
return Rml::PropertyId::NavUp;
case NavDirection::Right:
return Rml::PropertyId::NavRight;
case NavDirection::Down:
return Rml::PropertyId::NavDown;
case NavDirection::Left:
return Rml::PropertyId::NavLeft;
default:
assert(false && "Unknown nav direction.");
return Rml::PropertyId::Invalid;
}
}
void Style::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
property_map[property_id] = property;
}
Style::Style() {
}
Style::~Style() {
}
void Style::set_visibility(Visibility visibility) {
switch (visibility) {
case Visibility::Visible:
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
break;
case Visibility::Hidden:
set_property(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden);
break;
}
}
void Style::set_position(Position position) {
switch (position) {
case Position::Absolute:
set_property(Rml::PropertyId::Position, Rml::Style::Position::Absolute);
break;
case Position::Relative:
set_property(Rml::PropertyId::Position, Rml::Style::Position::Relative);
break;
default:
assert(false && "Unknown position.");
break;
}
}
void Style::set_left(float left, Unit unit) {
set_property(Rml::PropertyId::Left, Rml::Property(left, to_rml(unit)));
}
void Style::set_top(float top, Unit unit) {
set_property(Rml::PropertyId::Top, Rml::Property(top, to_rml(unit)));
}
void Style::set_right(float right, Unit unit) {
set_property(Rml::PropertyId::Right, Rml::Property(right, to_rml(unit)));
}
void Style::set_bottom(float bottom, Unit unit) {
set_property(Rml::PropertyId::Bottom, Rml::Property(bottom, to_rml(unit)));
}
void Style::set_width(float width, Unit unit) {
set_property(Rml::PropertyId::Width, Rml::Property(width, to_rml(unit)));
}
void Style::set_width_auto() {
set_property(Rml::PropertyId::Width, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_height(float height, Unit unit) {
set_property(Rml::PropertyId::Height, Rml::Property(height, to_rml(unit)));
}
void Style::set_height_auto() {
set_property(Rml::PropertyId::Height, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_min_width(float width, Unit unit) {
set_property(Rml::PropertyId::MinWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_min_height(float height, Unit unit) {
set_property(Rml::PropertyId::MinHeight, Rml::Property(height, to_rml(unit)));
}
void Style::set_max_width(float width, Unit unit) {
set_property(Rml::PropertyId::MaxWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_max_height(float height, Unit unit) {
set_property(Rml::PropertyId::MaxHeight, Rml::Property(height, to_rml(unit)));
}
void Style::set_padding(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingLeft, Rml::Property(padding, to_rml(unit)));
set_property(Rml::PropertyId::PaddingTop, Rml::Property(padding, to_rml(unit)));
set_property(Rml::PropertyId::PaddingRight, Rml::Property(padding, to_rml(unit)));
set_property(Rml::PropertyId::PaddingBottom, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_left(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingLeft, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_top(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingTop, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_right(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingRight, Rml::Property(padding, to_rml(unit)));
}
void Style::set_padding_bottom(float padding, Unit unit) {
set_property(Rml::PropertyId::PaddingBottom, Rml::Property(padding, to_rml(unit)));
}
void Style::set_margin(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(margin, to_rml(unit)));
set_property(Rml::PropertyId::MarginTop, Rml::Property(margin, to_rml(unit)));
set_property(Rml::PropertyId::MarginRight, Rml::Property(margin, to_rml(unit)));
set_property(Rml::PropertyId::MarginBottom, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_left(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_top(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginTop, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_right(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginRight, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_bottom(float margin, Unit unit) {
set_property(Rml::PropertyId::MarginBottom, Rml::Property(margin, to_rml(unit)));
}
void Style::set_margin_auto() {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
set_property(Rml::PropertyId::MarginTop, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
set_property(Rml::PropertyId::MarginRight, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
set_property(Rml::PropertyId::MarginBottom, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_left_auto() {
set_property(Rml::PropertyId::MarginLeft, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_top_auto() {
set_property(Rml::PropertyId::MarginTop, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_right_auto() {
set_property(Rml::PropertyId::MarginRight, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_margin_bottom_auto() {
set_property(Rml::PropertyId::MarginBottom, Rml::Property(Rml::Style::Margin::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_border_width(float width, Unit unit) {
Rml::Property property(width, to_rml(unit));
set_property(Rml::PropertyId::BorderTopWidth, property);
set_property(Rml::PropertyId::BorderBottomWidth, property);
set_property(Rml::PropertyId::BorderLeftWidth, property);
set_property(Rml::PropertyId::BorderRightWidth, property);
}
void Style::set_border_left_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderLeftWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_top_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderTopWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_right_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderRightWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_bottom_width(float width, Unit unit) {
set_property(Rml::PropertyId::BorderBottomWidth, Rml::Property(width, to_rml(unit)));
}
void Style::set_border_radius(float radius, Unit unit) {
Rml::Property property(radius, to_rml(unit));
set_property(Rml::PropertyId::BorderTopLeftRadius, property);
set_property(Rml::PropertyId::BorderTopRightRadius, property);
set_property(Rml::PropertyId::BorderBottomLeftRadius, property);
set_property(Rml::PropertyId::BorderBottomRightRadius, property);
}
void Style::set_border_top_left_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderTopLeftRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_border_top_right_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderTopRightRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_border_bottom_left_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderBottomLeftRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_border_bottom_right_radius(float radius, Unit unit) {
set_property(Rml::PropertyId::BorderBottomRightRadius, Rml::Property(radius, to_rml(unit)));
}
void Style::set_background_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BackgroundColor, property);
}
void Style::set_border_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderTopColor, property);
set_property(Rml::PropertyId::BorderBottomColor, property);
set_property(Rml::PropertyId::BorderLeftColor, property);
set_property(Rml::PropertyId::BorderRightColor, property);
}
void Style::set_border_left_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderLeftColor, property);
}
void Style::set_border_top_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderTopColor, property);
}
void Style::set_border_right_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderRightColor, property);
}
void Style::set_border_bottom_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::BorderBottomColor, property);
}
void Style::set_color(const Color &color) {
Rml::Property property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
set_property(Rml::PropertyId::Color, property);
}
void Style::set_cursor(Cursor cursor) {
switch (cursor) {
case Cursor::None:
set_property(Rml::PropertyId::Cursor, Rml::Property("", Rml::Unit::STRING));
break;
case Cursor::Pointer:
set_property(Rml::PropertyId::Cursor, Rml::Property("pointer", Rml::Unit::STRING));
break;
default:
assert(false && "Unknown cursor.");
break;
}
}
void Style::set_opacity(float opacity) {
set_property(Rml::PropertyId::Opacity, Rml::Property(opacity, Rml::Unit::NUMBER));
}
void Style::set_display(Display display) {
set_property(Rml::PropertyId::Display, to_rml(display));
}
void Style::set_justify_content(JustifyContent justify_content) {
set_property(Rml::PropertyId::JustifyContent, to_rml(justify_content));
}
void Style::set_flex_grow(float grow) {
set_property(Rml::PropertyId::FlexGrow, Rml::Property(grow, Rml::Unit::NUMBER));
}
void Style::set_flex_shrink(float shrink) {
set_property(Rml::PropertyId::FlexShrink, Rml::Property(shrink, Rml::Unit::NUMBER));
}
void Style::set_flex_basis_auto() {
set_property(Rml::PropertyId::FlexBasis, Rml::Property(Rml::Style::FlexBasis::Type::Auto, Rml::Unit::KEYWORD));
}
void Style::set_flex_basis(float basis, Unit unit) {
set_property(Rml::PropertyId::FlexBasis, Rml::Property(basis, to_rml(unit)));
}
void Style::set_flex(float grow, float shrink) {
set_flex_grow(grow);
set_flex_shrink(shrink);
set_flex_basis_auto();
}
void Style::set_flex(float grow, float shrink, float basis, Unit basis_unit) {
set_flex_grow(grow);
set_flex_shrink(shrink);
set_flex_basis(basis, basis_unit);
}
void Style::set_flex_direction(FlexDirection flex_direction) {
switch (flex_direction) {
case FlexDirection::Row:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row);
break;
case FlexDirection::Column:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column);
break;
case FlexDirection::RowReverse:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::RowReverse);
break;
case FlexDirection::ColumnReverse:
set_property(Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::ColumnReverse);
break;
default:
assert(false && "Unknown flex direction.");
break;
}
}
void Style::set_align_items(AlignItems align_items) {
set_property(Rml::PropertyId::AlignItems, to_rml(align_items));
}
void Style::set_overflow(Overflow overflow) {
set_property(Rml::PropertyId::OverflowX, to_rml(overflow));
set_property(Rml::PropertyId::OverflowY, to_rml(overflow));
}
void Style::set_overflow_x(Overflow overflow) {
set_property(Rml::PropertyId::OverflowX, to_rml(overflow));
}
void Style::set_overflow_y(Overflow overflow) {
set_property(Rml::PropertyId::OverflowY, to_rml(overflow));
}
void Style::set_font_size(float size, Unit unit) {
set_property(Rml::PropertyId::FontSize, Rml::Property(size, to_rml(unit)));
}
void Style::set_letter_spacing(float spacing, Unit unit) {
set_property(Rml::PropertyId::LetterSpacing, Rml::Property(spacing, to_rml(unit)));
}
void Style::set_line_height(float height, Unit unit) {
set_property(Rml::PropertyId::LineHeight, Rml::Property(height, to_rml(unit)));
}
void Style::set_font_style(FontStyle style) {
switch (style) {
case FontStyle::Normal:
set_property(Rml::PropertyId::FontStyle, Rml::Style::FontStyle::Normal);
break;
case FontStyle::Italic:
set_property(Rml::PropertyId::FontStyle, Rml::Style::FontStyle::Italic);
break;
default:
assert(false && "Unknown font style.");
break;
}
}
void Style::set_font_weight(uint32_t weight) {
set_property(Rml::PropertyId::FontWeight, Rml::Style::FontWeight(weight));
}
void Style::set_text_align(TextAlign text_align) {
set_property(Rml::PropertyId::TextAlign, to_rml(text_align));
}
void Style::set_text_transform(TextTransform text_transform) {
set_property(Rml::PropertyId::TextTransform, to_rml(text_transform));
}
void Style::set_gap(float size, Unit unit) {
set_row_gap(size, unit);
set_column_gap(size, unit);
}
void Style::set_row_gap(float size, Unit unit) {
set_property(Rml::PropertyId::RowGap, Rml::Property(size, to_rml(unit)));
}
void Style::set_column_gap(float size, Unit unit) {
set_property(Rml::PropertyId::ColumnGap, Rml::Property(size, to_rml(unit)));
}
void Style::set_drag(Drag drag) {
set_property(Rml::PropertyId::Drag, to_rml(drag));
}
void Style::set_tab_index(TabIndex tab_index) {
set_property(Rml::PropertyId::TabIndex, to_rml(tab_index));
}
void Style::set_font_family(std::string_view family) {
set_property(Rml::PropertyId::FontFamily, Rml::Property(Rml::String{ family }, Rml::Unit::UNKNOWN));
}
void Style::set_nav_auto(NavDirection dir) {
set_property(nav_to_property(dir), Rml::Style::Nav::Auto);
}
void Style::set_nav_none(NavDirection dir) {
set_property(nav_to_property(dir), Rml::Style::Nav::None);
}
void Style::set_nav(NavDirection dir, Element* element) {
set_property(nav_to_property(dir), Rml::Property(Rml::String{ "#" + element->get_id() }, Rml::Unit::STRING));
}
void Style::set_nav_manual(NavDirection dir, const std::string& target) {
set_property(nav_to_property(dir), Rml::Property(target, Rml::Unit::STRING));
}
void Style::set_tab_index_auto() {
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::Auto);
}
void Style::set_tab_index_none() {
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::None);
}
void Style::set_focusable(bool focusable) {
set_property(Rml::PropertyId::Focus, focusable ? Rml::Style::Focus::Auto : Rml::Style::Focus::None);
}
} // namespace recompui

108
src/ui/elements/ui_style.h Normal file
View File

@@ -0,0 +1,108 @@
#pragma once
#include <string_view>
#include "RmlUi/Core.h"
#include "../core/ui_resource.h"
#include "ui_types.h"
namespace recompui {
class ContextId;
class Style {
friend class Element; // For access to property_map without making it visible to element subclasses.
friend class ContextId;
private:
std::map<Rml::PropertyId, Rml::Property> property_map;
protected:
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property);
ResourceId resource_id = ResourceId::null();
public:
Style();
virtual ~Style();
void set_visibility(Visibility visibility);
void set_position(Position position);
void set_left(float left, Unit unit = Unit::Dp);
void set_top(float top, Unit unit = Unit::Dp);
void set_right(float right, Unit unit = Unit::Dp);
void set_bottom(float bottom, Unit unit = Unit::Dp);
void set_width(float width, Unit unit = Unit::Dp);
void set_width_auto();
void set_height(float height, Unit unit = Unit::Dp);
void set_height_auto();
void set_min_width(float width, Unit unit = Unit::Dp);
void set_min_height(float height, Unit unit = Unit::Dp);
void set_max_width(float width, Unit unit = Unit::Dp);
void set_max_height(float height, Unit unit = Unit::Dp);
void set_padding(float padding, Unit unit = Unit::Dp);
void set_padding_left(float padding, Unit unit = Unit::Dp);
void set_padding_top(float padding, Unit unit = Unit::Dp);
void set_padding_right(float padding, Unit unit = Unit::Dp);
void set_padding_bottom(float padding, Unit unit = Unit::Dp);
void set_margin(float margin, Unit unit = Unit::Dp);
void set_margin_left(float margin, Unit unit = Unit::Dp);
void set_margin_top(float margin, Unit unit = Unit::Dp);
void set_margin_right(float margin, Unit unit = Unit::Dp);
void set_margin_bottom(float margin, Unit unit = Unit::Dp);
void set_margin_auto();
void set_margin_left_auto();
void set_margin_top_auto();
void set_margin_right_auto();
void set_margin_bottom_auto();
void set_border_width(float width, Unit unit = Unit::Dp);
void set_border_left_width(float width, Unit unit = Unit::Dp);
void set_border_top_width(float width, Unit unit = Unit::Dp);
void set_border_right_width(float width, Unit unit = Unit::Dp);
void set_border_bottom_width(float width, Unit unit = Unit::Dp);
void set_border_radius(float radius, Unit unit = Unit::Dp);
void set_border_top_left_radius(float radius, Unit unit = Unit::Dp);
void set_border_top_right_radius(float radius, Unit unit = Unit::Dp);
void set_border_bottom_left_radius(float radius, Unit unit = Unit::Dp);
void set_border_bottom_right_radius(float radius, Unit unit = Unit::Dp);
void set_background_color(const Color &color);
void set_border_color(const Color &color);
void set_border_left_color(const Color &color);
void set_border_top_color(const Color &color);
void set_border_right_color(const Color &color);
void set_border_bottom_color(const Color &color);
void set_color(const Color &color);
void set_cursor(Cursor cursor);
void set_opacity(float opacity);
void set_display(Display display);
void set_justify_content(JustifyContent justify_content);
void set_flex_grow(float grow);
void set_flex_shrink(float shrink);
void set_flex_basis_auto();
void set_flex_basis(float basis, Unit unit = Unit::Percent);
void set_flex(float grow, float shrink);
void set_flex(float grow, float shrink, float basis, Unit basis_unit = Unit::Percent);
void set_flex_direction(FlexDirection flex_direction);
void set_align_items(AlignItems align_items);
void set_overflow(Overflow overflow);
void set_overflow_x(Overflow overflow);
void set_overflow_y(Overflow overflow);
void set_font_size(float size, Unit unit = Unit::Dp);
void set_letter_spacing(float spacing, Unit unit = Unit::Dp);
void set_line_height(float height, Unit unit = Unit::Dp);
void set_font_style(FontStyle style);
void set_font_weight(uint32_t weight);
void set_text_align(TextAlign text_align);
void set_text_transform(TextTransform text_transform);
void set_gap(float size, Unit unit = Unit::Dp);
void set_row_gap(float size, Unit unit = Unit::Dp);
void set_column_gap(float size, Unit unit = Unit::Dp);
void set_drag(Drag drag);
void set_tab_index(TabIndex focus);
void set_font_family(std::string_view family);
virtual void set_nav_auto(NavDirection dir);
virtual void set_nav_none(NavDirection dir);
virtual void set_nav(NavDirection dir, Element* element);
virtual void set_nav_manual(NavDirection dir, const std::string& target);
void set_tab_index_auto();
void set_tab_index_none();
void set_focusable(bool focusable);
virtual bool is_element() { return false; }
ResourceId get_resource_id() { return resource_id; }
};
} // namespace recompui

View File

@@ -0,0 +1,51 @@
#include "ui_text_input.h"
#include <cassert>
namespace recompui {
void TextInput::process_event(const Event &e) {
switch (e.type) {
case EventType::Text: {
const EventText &event = std::get<EventText>(e.variant);
text = event.text;
for (const auto &function : text_changed_callbacks) {
function(text);
}
break;
}
default:
break;
}
}
TextInput::TextInput(Element *parent, bool text_visible) : Element(parent, Events(EventType::Text), "input") {
if (!text_visible) {
set_attribute("type", "password");
}
set_min_width(60.0f);
set_border_color(Color{ 242, 242, 242, 255 });
set_border_bottom_width(1.0f);
set_padding_bottom(6.0f);
set_focusable(true);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_tab_index_auto();
}
void TextInput::set_text(std::string_view text) {
this->text = std::string(text);
set_attribute("value", this->text);
}
const std::string &TextInput::get_text() {
return text;
}
void TextInput::add_text_changed_callback(std::function<void(const std::string &)> callback) {
text_changed_callbacks.emplace_back(callback);
}
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class TextInput : public Element {
private:
std::string text;
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "TextInput"; }
public:
TextInput(Element *parent, bool text_visible = true);
void set_text(std::string_view text);
const std::string &get_text();
void add_text_changed_callback(std::function<void(const std::string &)> callback);
};
} // namespace recompui

View File

@@ -0,0 +1,161 @@
#include "ui_toggle.h"
#include <cassert>
#include <ultramodern/ultramodern.hpp>
namespace recompui {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
enable_focus();
set_width(162.0f);
set_height(72.0f);
set_border_radius(36.0f);
set_opacity(0.9f);
set_cursor(Cursor::Pointer);
set_border_width(2.0f);
set_border_color(Color{ 177, 76, 34, 255 });
set_background_color(Color{ 0, 0, 0, 0 });
checked_style.set_border_color(Color{ 34, 177, 76, 255 });
hover_style.set_border_color(Color{ 177, 76, 34, 255 });
hover_style.set_background_color(Color{ 206, 120, 68, 76 });
focus_style.set_border_color(Color{ 177, 76, 34, 255 });
focus_style.set_background_color(Color{ 206, 120, 68, 76 });
checked_hover_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_hover_style.set_background_color(Color{ 68, 206, 120, 76 });
checked_focus_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_focus_style.set_background_color(Color{ 68, 206, 120, 76 });
disabled_style.set_border_color(Color{ 177, 76, 34, 128 });
checked_disabled_style.set_border_color(Color{ 34, 177, 76, 128 });
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
add_style(&checked_hover_style, { checked_state, hover_state });
add_style(&checked_focus_style, { checked_state, focus_state });
add_style(&disabled_style, disabled_state);
add_style(&checked_disabled_style, { checked_state, disabled_state });
ContextId context = get_current_context();
floater = context.create_element<Element>(this);
floater->set_position(Position::Relative);
floater->set_top(2.0f);
floater->set_width(80.0f);
floater->set_height(64.0f);
floater->set_border_radius(32.0f);
floater->set_background_color(Color{ 177, 76, 34, 255 });
floater_checked_style.set_background_color(Color{ 34, 177, 76, 255 });
floater_disabled_style.set_background_color(Color{ 177, 76, 34, 128 });
floater_disabled_checked_style.set_background_color(Color{ 34, 177, 76, 128 });
floater->add_style(&floater_checked_style, checked_state);
floater->add_style(&floater_disabled_style, disabled_state);
floater->add_style(&floater_disabled_checked_style, { checked_state, disabled_state });
set_checked_internal(false, false, true, false);
}
void Toggle::set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks) {
if (this->checked != checked || setup) {
this->checked = checked;
if (animate) {
last_time = ultramodern::time_since_start();
queue_update();
}
else {
floater_left = floater_left_target();
}
floater->set_left(floater_left, Unit::Dp);
if (trigger_callbacks) {
for (const auto &function : checked_callbacks) {
function(checked);
}
}
set_style_enabled(checked_state, checked);
floater->set_style_enabled(checked_state, checked);
}
}
float Toggle::floater_left_target() const {
return checked ? 78.0f : 4.0f;
}
void Toggle::process_event(const Event &e) {
switch (e.type) {
case EventType::Click:
if (is_enabled()) {
set_checked_internal(!checked, true, false, true);
}
break;
case EventType::Hover: {
bool hover_active = std::get<EventHover>(e.variant).active && is_enabled();
set_style_enabled(hover_state, hover_active);
floater->set_style_enabled(hover_state, hover_active);
break;
}
case EventType::Focus: {
bool focus_active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, focus_active);
break;
}
case EventType::Enable: {
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
floater->set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
break;
}
case EventType::Update: {
std::chrono::high_resolution_clock::duration now = ultramodern::time_since_start();
float delta_time = std::max(std::chrono::duration<float>(now - last_time).count(), 0.0f);
last_time = now;
constexpr float dp_speed = 740.0f;
const float target = floater_left_target();
if (target < floater_left) {
floater_left += std::max(-dp_speed * delta_time, target - floater_left);
}
else {
floater_left += std::min(dp_speed * delta_time, target - floater_left);
}
if (abs(target - floater_left) < 1e-4f) {
floater_left = target;
}
else {
queue_update();
}
floater->set_left(floater_left, Unit::Dp);
break;
}
default:
break;
}
}
void Toggle::set_checked(bool checked) {
set_checked_internal(checked, false, false, false);
}
bool Toggle::is_checked() const {
return checked;
}
void Toggle::add_checked_callback(std::function<void(bool)> callback) {
checked_callbacks.emplace_back(callback);
}
};

View File

@@ -0,0 +1,38 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Toggle : public Element {
protected:
Element *floater;
float floater_left = 0.0f;
std::chrono::high_resolution_clock::duration last_time;
std::list<std::function<void(bool)>> checked_callbacks;
Style checked_style;
Style hover_style;
Style focus_style;
Style checked_hover_style;
Style checked_focus_style;
Style disabled_style;
Style checked_disabled_style;
Style floater_checked_style;
Style floater_disabled_style;
Style floater_disabled_checked_style;
bool checked = false;
void set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks);
float floater_left_target() const;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Toggle"; }
public:
Toggle(Element *parent);
void set_checked(bool checked);
bool is_checked() const;
void add_checked_callback(std::function<void(bool)> callback);
};
} // namespace recompui

260
src/ui/elements/ui_types.h Normal file
View File

@@ -0,0 +1,260 @@
#pragma once
#include <stdint.h>
#include <variant>
namespace recompui {
constexpr std::string_view checked_state = "checked";
constexpr std::string_view hover_state = "hover";
constexpr std::string_view focus_state = "focus";
constexpr std::string_view disabled_state = "disabled";
struct Color {
uint8_t r = 255;
uint8_t g = 255;
uint8_t b = 255;
uint8_t a = 255;
};
enum class Cursor {
None,
Pointer
};
// These two enums must be kept in sync with patches/recompui_event_structs.h!
enum class EventType {
None,
Click,
Focus,
Hover,
Enable,
Drag,
Text,
Update,
Navigate,
Count
};
enum class DragPhase {
None,
Start,
Move,
End
};
enum class NavDirection {
Up,
Right,
Down,
Left
};
template <typename Enum, typename = std::enable_if_t<std::is_enum_v<Enum>>>
constexpr uint32_t Events(Enum first) {
return 1u << static_cast<uint32_t>(first);
}
template <typename Enum, typename... Enums, typename = std::enable_if_t<std::is_enum_v<Enum>>>
constexpr uint32_t Events(Enum first, Enums... rest) {
return Events(first) | Events(rest...);
}
struct EventClick {
float x;
float y;
};
struct EventFocus {
bool active;
};
struct EventHover {
bool active;
};
struct EventEnable {
bool active;
};
struct EventDrag {
float x;
float y;
DragPhase phase;
};
struct EventText {
std::string text;
};
struct EventNavigate {
NavDirection direction;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, std::monostate>;
struct Event {
EventType type;
EventVariant variant;
// Factory methods for creating specific events
static Event click_event(float x, float y) {
Event e;
e.type = EventType::Click;
e.variant = EventClick{ x, y };
return e;
}
static Event focus_event(bool active) {
Event e;
e.type = EventType::Focus;
e.variant = EventFocus{ active };
return e;
}
static Event hover_event(bool active) {
Event e;
e.type = EventType::Hover;
e.variant = EventHover{ active };
return e;
}
static Event enable_event(bool enable) {
Event e;
e.type = EventType::Enable;
e.variant = EventEnable{ enable };
return e;
}
static Event drag_event(float x, float y, DragPhase phase) {
Event e;
e.type = EventType::Drag;
e.variant = EventDrag{ x, y, phase };
return e;
}
static Event text_event(const std::string &text) {
Event e;
e.type = EventType::Text;
e.variant = EventText{ text };
return e;
}
static Event update_event() {
Event e;
e.type = EventType::Update;
e.variant = std::monostate{};
return e;
}
static Event navigate_event(NavDirection direction) {
Event e;
e.type = EventType::Navigate;
e.variant = EventNavigate{ direction };
return e;
}
};
enum class Display {
None,
Block,
Inline,
InlineBlock,
FlowRoot,
Flex,
InlineFlex,
Table,
InlineTable,
TableRow,
TableRowGroup,
TableColumn,
TableColumnGroup,
TableCell
};
enum class Visibility {
Visible,
Hidden
};
enum class Position {
Absolute,
Relative
};
enum class JustifyContent {
FlexStart,
FlexEnd,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly
};
enum class FlexDirection {
Row,
Column,
RowReverse,
ColumnReverse
};
enum class AlignItems {
FlexStart,
FlexEnd,
Center,
Baseline,
Stretch
};
enum class Overflow {
Visible,
Hidden,
Auto,
Scroll
};
enum class Unit {
Px,
Dp,
Percent
};
enum class AnimationType : uint32_t {
None,
Set,
Tween
};
enum class FontStyle {
Normal,
Italic
};
enum class TextAlign {
Left,
Right,
Center,
Justify
};
enum class TextTransform {
None,
Capitalize,
Uppercase,
Lowercase
};
enum class Drag {
None,
Drag,
DragDrop,
Block,
Clone
};
enum class TabIndex {
None,
Auto
};
} // namespace recompui

1031
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
View File

@@ -0,0 +1,118 @@
#include "concurrentqueue.h"
#include "overloaded.h"
#include "recomp_ui.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
#include "elements/ui_element.h"
#include "elements/ui_button.h"
#include "elements/ui_clickable.h"
#include "elements/ui_container.h"
#include "elements/ui_image.h"
#include "elements/ui_label.h"
#include "elements/ui_radio.h"
#include "elements/ui_scroll_container.h"
#include "elements/ui_slider.h"
#include "elements/ui_style.h"
#include "elements/ui_text_input.h"
#include "elements/ui_toggle.h"
#include "elements/ui_types.h"
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "../patches/ui_funcs.h"
struct QueuedCallback {
recompui::ResourceId resource;
recompui::Event event;
recompui::UICallback callback;
};
moodycamel::ConcurrentQueue<QueuedCallback> queued_callbacks{};
void recompui::queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback) {
queued_callbacks.enqueue(QueuedCallback{ .resource = resource, .event = e, .callback = callback });
}
bool convert_event(const recompui::Event& in, RecompuiEventData& out) {
bool skip = false;
out = {};
out.type = static_cast<RecompuiEventType>(in.type);
switch (in.type) {
default:
case recompui::EventType::None:
case recompui::EventType::Count:
skip = true;
break;
case recompui::EventType::Click:
{
const recompui::EventClick &click = std::get<recompui::EventClick>(in.variant);
out.data.click.x = click.x;
out.data.click.y = click.y;
}
break;
case recompui::EventType::Focus:
{
const recompui::EventFocus &focus = std::get<recompui::EventFocus>(in.variant);
out.data.focus.active = focus.active;
}
break;
case recompui::EventType::Hover:
{
const recompui::EventHover &hover = std::get<recompui::EventHover>(in.variant);
out.data.hover.active = hover.active;
}
break;
case recompui::EventType::Enable:
{
const recompui::EventEnable &enable = std::get<recompui::EventEnable>(in.variant);
out.data.enable.active = enable.active;
}
break;
case recompui::EventType::Drag:
{
const recompui::EventDrag &drag = std::get<recompui::EventDrag>(in.variant);
out.data.drag.phase = static_cast<RecompuiDragPhase>(drag.phase);
out.data.drag.x = drag.x;
out.data.drag.y = drag.y;
}
break;
case recompui::EventType::Text:
skip = true; // Text events aren't supported in the UI mod API.
break;
case recompui::EventType::Update:
// No data for an update event.
break;
}
return !skip;
}
extern "C" void recomp_run_ui_callbacks(uint8_t* rdram, recomp_context* ctx) {
// Allocate the event on the stack.
gpr stack_frame = ctx->r29;
ctx->r29 -= sizeof(RecompuiEventData);
RecompuiEventData* event_data = TO_PTR(RecompuiEventData, stack_frame);
QueuedCallback cur_callback;
while (queued_callbacks.try_dequeue(cur_callback)) {
if (convert_event(cur_callback.event, *event_data)) {
recompui::ContextId cur_context = cur_callback.callback.context;
cur_context.open();
ctx->r4 = static_cast<int32_t>(cur_callback.resource.slot_id);
ctx->r5 = stack_frame;
ctx->r6 = cur_callback.callback.userdata;
LOOKUP_FUNC(cur_callback.callback.callback)(rdram, ctx);
cur_context.close();
}
}
ctx->r29 += sizeof(RecompuiEventData);
}

131
src/ui/ui_api_images.cpp Normal file
View File

@@ -0,0 +1,131 @@
#include <mutex>
#include <unordered_set>
#include "recomp_ui.h"
#include "librecomp/overlays.hpp"
#include "librecomp/helpers.hpp"
#include "ultramodern/error_handling.hpp"
#include "ui_helpers.h"
#include "ui_api_images.h"
#include "elements/ui_image.h"
using namespace recompui;
struct {
std::mutex mutex;
std::unordered_set<uint32_t> textures{};
uint32_t textures_created = 0;
} TextureState;
const std::string mod_texture_prefix = "?/mod_api/";
static std::string get_texture_name(uint32_t texture_id) {
return mod_texture_prefix + std::to_string(texture_id);
}
static uint32_t get_new_texture_id() {
std::lock_guard lock{TextureState.mutex};
uint32_t cur_id = TextureState.textures_created++;
TextureState.textures.emplace(cur_id);
return cur_id;
}
static void release_texture(uint32_t texture_id) {
std::string texture_name = get_texture_name(texture_id);
std::lock_guard lock{TextureState.mutex};
if (TextureState.textures.erase(texture_id) == 0) {
recompui::message_box("Fatal error in mod - attempted to destroy texture that doesn't exist!");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
recompui::release_image(texture_name);
}
thread_local std::vector<char> swapped_image_bytes;
void recompui_create_texture_rgba32(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t width = _arg<1, uint32_t>(rdram, ctx);
uint32_t height = _arg<2, uint32_t>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's pixel data.
size_t size_bytes = width * height * 4 * sizeof(uint8_t);
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the pixel data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_rgba32(texture_name, swapped_image_bytes, width, height);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_create_texture_image_bytes(uint8_t* rdram, recomp_context* ctx) {
PTR(void) data_in = _arg<0, PTR(void)>(rdram, ctx);
uint32_t size_bytes = _arg<1, u32>(rdram, ctx);
uint32_t cur_id = get_new_texture_id();
// The size in bytes of the image's data.
swapped_image_bytes.resize(size_bytes);
// Byteswap copy the image's data.
for (size_t i = 0; i < size_bytes; i++) {
swapped_image_bytes[i] = MEM_B(i, data_in);
}
// Create a texture name from the ID and queue its bytes.
std::string texture_name = get_texture_name(cur_id);
recompui::queue_image_from_bytes_file(texture_name, swapped_image_bytes);
// Return the new texture ID.
_return(ctx, cur_id);
}
void recompui_destroy_texture(uint8_t* rdram, recomp_context* ctx) {
uint32_t texture_id = _arg<0, uint32_t>(rdram, ctx);
release_texture(texture_id);
}
void recompui_create_imageview(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
uint32_t texture_id = _arg<2, uint32_t>(rdram, ctx);
Element* ret = ui_context.create_element<Image>(parent, get_texture_name(texture_id));
return_resource(ctx, ret->get_resource_id());
}
void recompui_set_imageview_texture(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t texture_id = _arg<1, uint32_t>(rdram, ctx);
if (!resource->is_element()) {
recompui::message_box("Fatal error in mod - attempted to set texture of non-element");
assert(false);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
Element* element = static_cast<Element*>(resource);
element->set_src(get_texture_name(texture_id));
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_image_exports() {
REGISTER_FUNC(recompui_create_texture_rgba32);
REGISTER_FUNC(recompui_create_texture_image_bytes);
REGISTER_FUNC(recompui_destroy_texture);
REGISTER_FUNC(recompui_create_imageview);
REGISTER_FUNC(recompui_set_imageview_texture);
}

10
src/ui/ui_api_images.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef __UI_API_IMAGES_H__
#define __UI_API_IMAGES_H__
#include <cstdint>
namespace recompui {
void register_ui_image_exports();
}
#endif

View File

@@ -7,166 +7,166 @@
using ColourMap = Rml::UnorderedMap<Rml::String, Rml::Colourb>;
namespace recompui {
class PropertyParserColorHack : public Rml::PropertyParser {
public:
PropertyParserColorHack();
virtual ~PropertyParserColorHack();
bool ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const override;
private:
static ColourMap html_colours;
};
static_assert(sizeof(PropertyParserColorHack) == sizeof(Rml::PropertyParserColour));
PropertyParserColorHack::PropertyParserColorHack() {
html_colours["black"] = Rml::Colourb(0, 0, 0);
html_colours["silver"] = Rml::Colourb(192, 192, 192);
html_colours["gray"] = Rml::Colourb(128, 128, 128);
html_colours["grey"] = Rml::Colourb(128, 128, 128);
html_colours["white"] = Rml::Colourb(255, 255, 255);
html_colours["maroon"] = Rml::Colourb(128, 0, 0);
html_colours["red"] = Rml::Colourb(255, 0, 0);
html_colours["orange"] = Rml::Colourb(255, 165, 0);
html_colours["purple"] = Rml::Colourb(128, 0, 128);
html_colours["fuchsia"] = Rml::Colourb(255, 0, 255);
html_colours["green"] = Rml::Colourb(0, 128, 0);
html_colours["lime"] = Rml::Colourb(0, 255, 0);
html_colours["olive"] = Rml::Colourb(128, 128, 0);
html_colours["yellow"] = Rml::Colourb(255, 255, 0);
html_colours["navy"] = Rml::Colourb(0, 0, 128);
html_colours["blue"] = Rml::Colourb(0, 0, 255);
html_colours["teal"] = Rml::Colourb(0, 128, 128);
html_colours["aqua"] = Rml::Colourb(0, 255, 255);
html_colours["transparent"] = Rml::Colourb(0, 0, 0, 0);
html_colours["whitesmoke"] = Rml::Colourb(245, 245, 245);
}
class PropertyParserColorHack : public Rml::PropertyParser {
public:
PropertyParserColorHack();
virtual ~PropertyParserColorHack();
bool ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const override;
private:
static ColourMap html_colours;
};
static_assert(sizeof(PropertyParserColorHack) == sizeof(Rml::PropertyParserColour));
PropertyParserColorHack::PropertyParserColorHack() {
html_colours["black"] = Rml::Colourb(0, 0, 0);
html_colours["silver"] = Rml::Colourb(192, 192, 192);
html_colours["gray"] = Rml::Colourb(128, 128, 128);
html_colours["grey"] = Rml::Colourb(128, 128, 128);
html_colours["white"] = Rml::Colourb(255, 255, 255);
html_colours["maroon"] = Rml::Colourb(128, 0, 0);
html_colours["red"] = Rml::Colourb(255, 0, 0);
html_colours["orange"] = Rml::Colourb(255, 165, 0);
html_colours["purple"] = Rml::Colourb(128, 0, 128);
html_colours["fuchsia"] = Rml::Colourb(255, 0, 255);
html_colours["green"] = Rml::Colourb(0, 128, 0);
html_colours["lime"] = Rml::Colourb(0, 255, 0);
html_colours["olive"] = Rml::Colourb(128, 128, 0);
html_colours["yellow"] = Rml::Colourb(255, 255, 0);
html_colours["navy"] = Rml::Colourb(0, 0, 128);
html_colours["blue"] = Rml::Colourb(0, 0, 255);
html_colours["teal"] = Rml::Colourb(0, 128, 128);
html_colours["aqua"] = Rml::Colourb(0, 255, 255);
html_colours["transparent"] = Rml::Colourb(0, 0, 0, 0);
html_colours["whitesmoke"] = Rml::Colourb(245, 245, 245);
}
PropertyParserColorHack::~PropertyParserColorHack() {}
PropertyParserColorHack::~PropertyParserColorHack() {}
bool PropertyParserColorHack::ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const {
if (value.empty())
return false;
bool PropertyParserColorHack::ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const {
if (value.empty())
return false;
Rml::Colourb colour;
Rml::Colourb colour;
// Check for a hex colour.
if (value[0] == '#')
{
char hex_values[4][2] = { {'f', 'f'}, {'f', 'f'}, {'f', 'f'}, {'f', 'f'} };
// Check for a hex colour.
if (value[0] == '#')
{
char hex_values[4][2] = { {'f', 'f'}, {'f', 'f'}, {'f', 'f'}, {'f', 'f'} };
switch (value.size())
{
// Single hex digit per channel, RGB and alpha.
case 5:
hex_values[3][0] = hex_values[3][1] = value[4];
//-fallthrough
// Single hex digit per channel, RGB only.
case 4:
hex_values[0][0] = hex_values[0][1] = value[1];
hex_values[1][0] = hex_values[1][1] = value[2];
hex_values[2][0] = hex_values[2][1] = value[3];
break;
switch (value.size())
{
// Single hex digit per channel, RGB and alpha.
case 5:
hex_values[3][0] = hex_values[3][1] = value[4];
//-fallthrough
// Single hex digit per channel, RGB only.
case 4:
hex_values[0][0] = hex_values[0][1] = value[1];
hex_values[1][0] = hex_values[1][1] = value[2];
hex_values[2][0] = hex_values[2][1] = value[3];
break;
// Two hex digits per channel, RGB and alpha.
case 9:
hex_values[3][0] = value[7];
hex_values[3][1] = value[8];
//-fallthrough
// Two hex digits per channel, RGB only.
case 7: memcpy(hex_values, &value.c_str()[1], sizeof(char) * 6); break;
// Two hex digits per channel, RGB and alpha.
case 9:
hex_values[3][0] = value[7];
hex_values[3][1] = value[8];
//-fallthrough
// Two hex digits per channel, RGB only.
case 7: memcpy(hex_values, &value.c_str()[1], sizeof(char) * 6); break;
default: return false;
}
default: return false;
}
// Parse each of the colour elements.
for (int i = 0; i < 4; i++)
{
int tens = Rml::Math::HexToDecimal(hex_values[i][0]);
int ones = Rml::Math::HexToDecimal(hex_values[i][1]);
if (tens == -1 || ones == -1)
return false;
// Parse each of the colour elements.
for (size_t i = 0; i < 4; i++)
{
int tens = Rml::Math::HexToDecimal(hex_values[i][0]);
int ones = Rml::Math::HexToDecimal(hex_values[i][1]);
if (tens == -1 || ones == -1)
return false;
colour[i] = (Rml::byte)(tens * 16 + ones);
}
}
else if (value.substr(0, 3) == "rgb")
{
Rml::StringList values;
values.reserve(4);
colour[i] = (Rml::byte)(tens * 16 + ones);
}
}
else if (value.substr(0, 3) == "rgb")
{
Rml::StringList values;
values.reserve(4);
size_t find = value.find('(');
if (find == Rml::String::npos)
return false;
size_t find = value.find('(');
if (find == Rml::String::npos)
return false;
size_t begin_values = find + 1;
size_t begin_values = find + 1;
Rml::StringUtilities::ExpandString(values, value.substr(begin_values, value.rfind(')') - begin_values), ',');
Rml::StringUtilities::ExpandString(values, value.substr(begin_values, value.rfind(')') - begin_values), ',');
// Check if we're parsing an 'rgba' or 'rgb' colour declaration.
if (value.size() > 3 && value[3] == 'a')
{
if (values.size() != 4)
return false;
}
else
{
if (values.size() != 3)
return false;
// Check if we're parsing an 'rgba' or 'rgb' colour declaration.
if (value.size() > 3 && value[3] == 'a')
{
if (values.size() != 4)
return false;
}
else
{
if (values.size() != 3)
return false;
values.push_back("255");
}
values.push_back("255");
}
// Parse the three RGB values.
for (int i = 0; i < 3; ++i)
{
int component;
// Parse the three RGB values.
for (size_t i = 0; i < 3; ++i)
{
int component;
// We're parsing a percentage value.
if (values[i].size() > 0 && values[i][values[i].size() - 1] == '%')
component = int((float)atof(values[i].substr(0, values[i].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 255 integer value.
else
component = atoi(values[i].c_str());
// We're parsing a percentage value.
if (values[i].size() > 0 && values[i][values[i].size() - 1] == '%')
component = int((float)atof(values[i].substr(0, values[i].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 255 integer value.
else
component = atoi(values[i].c_str());
colour[i] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
// Parse the alpha value. Modified from the original RmlUi implementation to use 0-1 instead of 0-255.
{
int component;
colour[i] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
// Parse the alpha value. Modified from the original RmlUi implementation to use 0-1 instead of 0-255.
{
int component;
// We're parsing a percentage value.
if (values[3].size() > 0 && values[3][values[3].size() - 1] == '%')
component = ((float)atof(values[3].substr(0, values[3].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 1 float value.
else
component = atof(values[3].c_str()) * 255.0f;
// We're parsing a percentage value.
if (values[3].size() > 0 && values[3][values[3].size() - 1] == '%')
component = ((float)atof(values[3].substr(0, values[3].size() - 1).c_str()) * (255.0f / 100.0f));
// We're parsing a 0 -> 1 float value.
else
component = atof(values[3].c_str()) * 255.0f;
colour[3] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
}
else
{
// Check for the specification of an HTML colour.
ColourMap::const_iterator iterator = html_colours.find(Rml::StringUtilities::ToLower(value));
if (iterator == html_colours.end())
return false;
else
colour = (*iterator).second;
}
colour[3] = (Rml::byte)(Rml::Math::Clamp(component, 0, 255));
}
}
else
{
// Check for the specification of an HTML colour.
ColourMap::const_iterator iterator = html_colours.find(Rml::StringUtilities::ToLower(value));
if (iterator == html_colours.end())
return false;
else
colour = (*iterator).second;
}
property.value = Rml::Variant(colour);
property.unit = Rml::Unit::COLOUR;
property.value = Rml::Variant(colour);
property.unit = Rml::Unit::COLOUR;
return true;
}
return true;
}
// This hack overwrites the contents of a property parser pointer for "color" (which is known to point to a valid Rml::PropertyParserColour) with the contents of a PropertyParserColorHack.
// This overwrites the object's vtable, allowing us to override color parsing behavior to use 0-1 alpha instead of 0-255.
// Ideally we'd just replace the pointer itself, but RmlUi doesn't provide a way to do that currently.
void apply_color_hack() {
// Allocate and leak a parser to act as a vtable source.
PropertyParserColorHack* new_parser = new PropertyParserColorHack();
// Copy the allocated object into the color parser pointer to overwrite its vtable.
memcpy((void*)Rml::StyleSheetSpecification::GetParser("color"), (void*)new_parser, sizeof(*new_parser));
}
// This hack overwrites the contents of a property parser pointer for "color" (which is known to point to a valid Rml::PropertyParserColour) with the contents of a PropertyParserColorHack.
// This overwrites the object's vtable, allowing us to override color parsing behavior to use 0-1 alpha instead of 0-255.
// Ideally we'd just replace the pointer itself, but RmlUi doesn't provide a way to do that currently.
void apply_color_hack() {
// Allocate and leak a parser to act as a vtable source.
PropertyParserColorHack* new_parser = new PropertyParserColorHack();
// Copy the allocated object into the color parser pointer to overwrite its vtable.
memcpy((void*)Rml::StyleSheetSpecification::GetParser("color"), (void*)new_parser, sizeof(*new_parser));
}
ColourMap PropertyParserColorHack::html_colours{};
ColourMap PropertyParserColorHack::html_colours{};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
#include "ui_config_sub_menu.h"
#include <cassert>
#include <string_view>
#include "recomp_ui.h"
namespace recompui {
// ConfigOptionElement
void ConfigOptionElement::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover:
hover_callback(this, std::get<EventHover>(e.variant).active);
break;
case EventType::Update:
break;
default:
assert(false && "Unknown event type.");
break;
}
}
ConfigOptionElement::ConfigOptionElement(Element *parent) : Element(parent, Events(EventType::Hover)) {
set_display(Display::Flex);
set_flex_direction(FlexDirection::Column);
set_gap(16.0f);
set_height(100.0f);
name_label = get_current_context().create_element<Label>(this, LabelStyle::Normal);
}
ConfigOptionElement::~ConfigOptionElement() {
}
void ConfigOptionElement::set_option_id(std::string_view id) {
this->option_id = id;
}
void ConfigOptionElement::set_name(std::string_view name) {
this->name = name;
name_label->set_text(name);
}
void ConfigOptionElement::set_description(std::string_view description) {
this->description = description;
}
void ConfigOptionElement::set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback) {
hover_callback = callback;
}
const std::string &ConfigOptionElement::get_description() const {
return description;
}
// ConfigOptionSlider
void ConfigOptionSlider::slider_value_changed(double v) {
callback(option_id, v);
}
ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
slider = get_current_context().create_element<Slider>(this, percent ? SliderType::Percent : SliderType::Double);
slider->set_max_width(380.0f);
slider->set_min_value(min_value);
slider->set_max_value(max_value);
slider->set_step_value(step_value);
slider->set_value(value);
slider->add_value_changed_callback([this](double v){ slider_value_changed(v); });
}
// ConfigOptionTextInput
void ConfigOptionTextInput::text_changed(const std::string &text) {
callback(option_id, text);
}
ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
text_input = get_current_context().create_element<TextInput>(this);
text_input->set_max_width(400.0f);
text_input->set_text(value);
text_input->add_text_changed_callback([this](const std::string &text){ text_changed(text); });
}
// ConfigOptionRadio
void ConfigOptionRadio::index_changed(uint32_t index) {
callback(option_id, index);
}
ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
radio = get_current_context().create_element<Radio>(this);
radio->add_index_changed_callback([this](uint32_t index){ index_changed(index); });
for (std::string_view option : options) {
radio->add_option(option);
}
if (value < options.size()) {
radio->set_index(value);
}
}
// ConfigSubMenu
void ConfigSubMenu::back_button_pressed() {
// Hide the config sub menu and show the config menu.
ContextId config_context = recompui::get_config_context_id();
ContextId sub_menu_context = recompui::get_config_sub_menu_context_id();
recompui::hide_context(sub_menu_context);
recompui::show_context(config_context, "");
recompui::focus_mod_configure_button();
}
void ConfigSubMenu::option_hovered(ConfigOptionElement *option, bool active) {
if (active) {
hover_option_elements.emplace(option);
}
else {
hover_option_elements.erase(option);
}
if (hover_option_elements.empty()) {
description_label->set_text("");
}
else {
description_label->set_text((*hover_option_elements.begin())->get_description());
}
}
ConfigSubMenu::ConfigSubMenu(Element *parent) : Element(parent) {
using namespace std::string_view_literals;
set_display(Display::Flex);
set_flex(1, 1, 100.0f, Unit::Percent);
set_flex_direction(FlexDirection::Column);
set_height(100.0f, Unit::Percent);
recompui::ContextId context = get_current_context();
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
header_container->set_flex_grow(0.0f);
header_container->set_align_items(AlignItems::Center);
header_container->set_padding(12.0f);
header_container->set_gap(24.0f);
{
back_button = context.create_element<Button>(header_container, "Back", ButtonStyle::Secondary);
back_button->add_pressed_callback([this](){ back_button_pressed(); });
title_label = context.create_element<Label>(header_container, "Title", LabelStyle::Large);
}
body_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceEvenly);
body_container->set_padding(32.0f);
{
config_container = context.create_element<Container>(body_container, FlexDirection::Column, JustifyContent::Center);
config_container->set_display(Display::Block);
config_container->set_flex_basis(100.0f);
config_container->set_align_items(AlignItems::Center);
{
config_scroll_container = context.create_element<ScrollContainer>(config_container, ScrollDirection::Vertical);
}
description_label = context.create_element<Label>(body_container, "Description", LabelStyle::Small);
description_label->set_min_width(800.0f);
}
recompui::get_current_context().set_autofocus_element(back_button);
}
ConfigSubMenu::~ConfigSubMenu() {
}
void ConfigSubMenu::enter(std::string_view title) {
title_label->set_text(title);
}
void ConfigSubMenu::clear_options() {
config_scroll_container->clear_children();
config_option_elements.clear();
hover_option_elements.clear();
}
void ConfigSubMenu::add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description) {
option->set_option_id(id);
option->set_name(name);
option->set_description(description);
option->set_hover_callback([this](ConfigOptionElement *option, bool active){ option_hovered(option, active); });
if (config_option_elements.empty()) {
back_button->set_nav(NavDirection::Down, option->get_focus_element());
option->set_nav(NavDirection::Up, back_button);
}
else {
config_option_elements.back()->set_nav(NavDirection::Down, option->get_focus_element());
option->set_nav(NavDirection::Up, config_option_elements.back()->get_focus_element());
}
config_option_elements.emplace_back(option);
}
void ConfigSubMenu::add_slider_option(std::string_view id, std::string_view name, std::string_view description, double value, double min, double max, double step, bool percent, std::function<void(const std::string &, double)> callback) {
ConfigOptionSlider *option_slider = get_current_context().create_element<ConfigOptionSlider>(config_scroll_container, value, min, max, step, percent, callback);
add_option(option_slider, id, name, description);
}
void ConfigSubMenu::add_text_option(std::string_view id, std::string_view name, std::string_view description, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) {
ConfigOptionTextInput *option_text_input = get_current_context().create_element<ConfigOptionTextInput>(config_scroll_container, value, callback);
add_option(option_text_input, id, name, description);
}
void ConfigSubMenu::add_radio_option(std::string_view id, std::string_view name, std::string_view description, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) {
ConfigOptionRadio *option_radio = get_current_context().create_element<ConfigOptionRadio>(config_scroll_container, value, options, callback);
add_option(option_radio, id, name, description);
}
// ElementConfigSubMenu
ElementConfigSubMenu::ElementConfigSubMenu(const Rml::String &tag) : Rml::Element(tag) {
SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Flex);
SetProperty("width", "100%");
SetProperty("height", "100%");
recompui::Element this_compat(this);
recompui::ContextId context = get_current_context();
config_sub_menu = context.create_element<ConfigSubMenu>(&this_compat);
}
ElementConfigSubMenu::~ElementConfigSubMenu() {
}
void ElementConfigSubMenu::set_display(bool display) {
SetProperty(Rml::PropertyId::Display, display ? Rml::Style::Display::Block : Rml::Style::Display::None);
}
ConfigSubMenu *ElementConfigSubMenu::get_config_sub_menu_element() const {
return config_sub_menu;
}
}

115
src/ui/ui_config_sub_menu.h Normal file
View File

@@ -0,0 +1,115 @@
#ifndef RECOMPUI_CONFIG_SUB_MENU_H
#define RECOMPUI_CONFIG_SUB_MENU_H
#include <span>
#include "elements/ui_button.h"
#include "elements/ui_container.h"
#include "elements/ui_label.h"
#include "elements/ui_radio.h"
#include "elements/ui_scroll_container.h"
#include "elements/ui_slider.h"
#include "elements/ui_text_input.h"
namespace recompui {
class ConfigOptionElement : public Element {
protected:
Label *name_label = nullptr;
std::string option_id;
std::string name;
std::string description;
std::function<void(ConfigOptionElement *, bool)> hover_callback = nullptr;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ConfigOptionElement"; }
public:
ConfigOptionElement(Element *parent);
virtual ~ConfigOptionElement();
void set_option_id(std::string_view id);
void set_name(std::string_view name);
void set_description(std::string_view description);
void set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback);
const std::string &get_description() const;
void set_nav_auto(NavDirection dir) override { get_focus_element()->set_nav_auto(dir); }
void set_nav_none(NavDirection dir) override { get_focus_element()->set_nav_none(dir); }
void set_nav(NavDirection dir, Element* element) override { get_focus_element()->set_nav(dir, element); }
void set_nav_manual(NavDirection dir, const std::string& target) override { get_focus_element()->set_nav_manual(dir, target); }
virtual Element* get_focus_element() { return this; }
};
class ConfigOptionSlider : public ConfigOptionElement {
protected:
Slider *slider = nullptr;
std::function<void(const std::string &, double)> callback;
void slider_value_changed(double v);
std::string_view get_type_name() override { return "ConfigOptionSlider"; }
public:
ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback);
Element* get_focus_element() override { return slider; }
};
class ConfigOptionTextInput : public ConfigOptionElement {
protected:
TextInput *text_input = nullptr;
std::function<void(const std::string &, const std::string &)> callback;
void text_changed(const std::string &text);
std::string_view get_type_name() override { return "ConfigOptionTextInput"; }
public:
ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
Element* get_focus_element() override { return text_input; }
};
class ConfigOptionRadio : public ConfigOptionElement {
protected:
Radio *radio = nullptr;
std::function<void(const std::string &, uint32_t)> callback;
void index_changed(uint32_t index);
std::string_view get_type_name() override { return "ConfigOptionRadio"; }
public:
ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
Element* get_focus_element() override { return radio; }
};
class ConfigSubMenu : public Element {
private:
Container *header_container = nullptr;
Button *back_button = nullptr;
Label *title_label = nullptr;
Container *body_container = nullptr;
Label *description_label = nullptr;
Container *config_container = nullptr;
ScrollContainer *config_scroll_container = nullptr;
std::vector<ConfigOptionElement *> config_option_elements;
std::unordered_set<ConfigOptionElement *> hover_option_elements;
void back_button_pressed();
void option_hovered(ConfigOptionElement *option, bool active);
void add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description);
protected:
std::string_view get_type_name() override { return "ConfigSubMenu"; }
public:
ConfigSubMenu(Element *parent);
virtual ~ConfigSubMenu();
void enter(std::string_view title);
void clear_options();
void add_slider_option(std::string_view id, std::string_view name, std::string_view description, double value, double min, double max, double step, bool percent, std::function<void(const std::string &, double)> callback);
void add_text_option(std::string_view id, std::string_view name, std::string_view description, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
void add_radio_option(std::string_view id, std::string_view name, std::string_view description, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
};
class ElementConfigSubMenu : public Rml::Element {
public:
ElementConfigSubMenu(const Rml::String &tag);
virtual ~ElementConfigSubMenu();
void set_display(bool display);
ConfigSubMenu *get_config_sub_menu_element() const;
private:
ConfigSubMenu *config_sub_menu;
};
}
#endif

42
src/ui/ui_elements.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include "ui_elements.h"
struct RecompCustomElement {
Rml::String tag;
std::unique_ptr<Rml::ElementInstancer> instancer;
};
#define CUSTOM_ELEMENT(s, e) { s, std::make_unique< Rml::ElementInstancerGeneric< e > >() }
static RecompCustomElement custom_elements[] = {
CUSTOM_ELEMENT("recomp-mod-menu", recompui::ElementModMenu),
CUSTOM_ELEMENT("recomp-config-sub-menu", recompui::ElementConfigSubMenu),
};
void recompui::register_custom_elements() {
for (auto& element_config : custom_elements) {
Rml::Factory::RegisterElementInstancer(element_config.tag, element_config.instancer.get());
}
}
Rml::ElementInstancer* recompui::get_custom_element_instancer(std::string tag) {
for (auto& element_config : custom_elements) {
if (tag == element_config.tag) {
return element_config.instancer.get();
}
}
return nullptr;
}
Rml::ElementPtr recompui::create_custom_element(Rml::Element* parent, std::string tag) {
auto instancer = recompui::get_custom_element_instancer(tag);
const Rml::XMLAttributes attributes = {};
if (Rml::ElementPtr element = instancer->InstanceElement(parent, tag, attributes))
{
element->SetInstancer(instancer);
element->SetAttributes(attributes);
return element;
}
return nullptr;
}

16
src/ui/ui_elements.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef RECOMPUI_ELEMENTS_H
#define RECOMPUI_ELEMENTS_H
#include "recomp_ui.h"
#include "RmlUi/Core/Element.h"
#include "ui_mod_menu.h"
#include "ui_config_sub_menu.h"
namespace recompui {
void register_custom_elements();
Rml::ElementInstancer* get_custom_element_instancer(std::string tag);
}
#endif

154
src/ui/ui_helpers.h Normal file
View File

@@ -0,0 +1,154 @@
#ifndef __UI_HELPERS_H__
#define __UI_HELPERS_H__
#include "librecomp/helpers.hpp"
#include "librecomp/addresses.hpp"
#include "elements/ui_element.h"
#include "elements/ui_types.h"
#include "core/ui_context.h"
#include "core/ui_resource.h"
namespace recompui {
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
inline ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
return ContextId{ .slot_id = context_id };
}
inline float arg_float2(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<2, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float3(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = _arg<3, uint32_t>(rdram, ctx);
return val.f32;
}
inline float arg_float4(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x10, ctx->r29);
return val.f32;
}
inline float arg_float5(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x14, ctx->r29);
return val.f32;
}
inline float arg_float6(uint8_t* rdram, recomp_context* ctx) {
union {
float f32;
uint32_t u32;
} val;
val.u32 = MEM_W(0x18, ctx->r29);
return val.f32;
}
template <int arg_index>
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
return ResourceId{ .slot_id = slot_id };
}
template <int arg_index>
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
return ui_context.get_root_element();
}
return resource.as_element();
}
template <int arg_index>
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
ContextId ui_context = recompui::get_current_context();
return ui_context.get_root_element();
}
return *resource;
}
template <int arg_index>
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
Color ret{};
ret.r = MEM_B(0, color_arg);
ret.g = MEM_B(1, color_arg);
ret.b = MEM_B(2, color_arg);
ret.a = MEM_B(3, color_arg);
return ret;
}
inline void return_resource(recomp_context* ctx, ResourceId resource) {
_return<uint32_t>(ctx, resource.slot_id);
}
inline void return_string(uint8_t* rdram, recomp_context* ctx, const std::string& ret) {
gpr addr = (reinterpret_cast<uint8_t*>(recomp::alloc(rdram, ret.size() + 1)) - rdram) + 0xFFFFFFFF80000000ULL;
for (size_t i = 0; i < ret.size(); i++) {
MEM_B(i, addr) = ret[i];
}
MEM_B(ret.size(), addr) = '\x00';
_return<PTR(char)>(ctx, addr);
}
inline std::string decode_string(uint8_t* rdram, PTR(char) str) {
// Get the length of the byteswapped string.
size_t len = 0;
while (MEM_B(str, len) != 0x00) {
len++;
}
std::string ret{};
ret.reserve(len + 1);
for (size_t i = 0; i < len; i++) {
ret += (char)MEM_B(str, i);
}
return ret;
}
}
#endif

View File

@@ -15,97 +15,111 @@ bool mm_rom_valid = false;
extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
zelda64::open_file_dialog([](bool success, const std::filesystem::path& path) {
if (success) {
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
});
nfdnchar_t* native_path = nullptr;
zelda64::open_file_dialog([](bool success, const std::filesystem::path& path) {
if (success) {
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
});
}
recompui::ContextId launcher_context;
recompui::ContextId recompui::get_launcher_context_id() {
return launcher_context;
}
class LauncherMenu : public recompui::MenuController {
public:
LauncherMenu() {
mm_rom_valid = recomp::is_rom_valid(supported_games[0].game_id);
mm_rom_valid = recomp::is_rom_valid(supported_games[0].game_id);
}
~LauncherMenu() override {
~LauncherMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
const std::filesystem::path asset = zelda64::get_asset_path("launcher.rml");
return context->LoadDocument(asset.string());
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
[](const std::string& param, Rml::Event& event) {
select_rom();
}
);
recompui::register_event(listener, "rom_selected",
[](const std::string& param, Rml::Event& event) {
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
}
);
recompui::register_event(listener, "start_game",
[](const std::string& param, Rml::Event& event) {
recomp::start_game(supported_games[0].game_id);
recompui::set_current_menu(recompui::Menu::None);
}
);
}
void load_document() override {
launcher_context = recompui::create_context(zelda64::get_asset_path("launcher.rml"));
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",
[](const std::string& param, Rml::Event& event) {
select_rom();
}
);
recompui::register_event(listener, "rom_selected",
[](const std::string& param, Rml::Event& event) {
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
}
);
recompui::register_event(listener, "start_game",
[](const std::string& param, Rml::Event& event) {
recomp::start_game(supported_games[0].game_id);
recompui::hide_all_contexts();
}
);
recompui::register_event(listener, "open_controls",
[](const std::string& param, Rml::Event& event) {
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::Controls);
}
);
[](const std::string& param, Rml::Event& event) {
recompui::set_config_tab(recompui::ConfigTab::Controls);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
);
recompui::register_event(listener, "open_settings",
[](const std::string& param, Rml::Event& event) {
recompui::set_current_menu(recompui::Menu::Config);
recompui::set_config_submenu(recompui::ConfigSubmenu::General);
}
);
[](const std::string& param, Rml::Event& event) {
recompui::set_config_tab(recompui::ConfigTab::General);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
);
recompui::register_event(listener, "open_mods",
[](const std::string &param, Rml::Event &event) {
recompui::set_config_tab(recompui::ConfigTab::Mods);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
);
recompui::register_event(listener, "exit_game",
[](const std::string& param, Rml::Event& event) {
ultramodern::quit();
}
);
}
void make_bindings(Rml::Context* context) override {
Rml::DataModelConstructor constructor = context->CreateDataModel("launcher_model");
[](const std::string& param, Rml::Event& event) {
ultramodern::quit();
}
);
}
void make_bindings(Rml::Context* context) override {
Rml::DataModelConstructor constructor = context->CreateDataModel("launcher_model");
constructor.Bind("mm_rom_valid", &mm_rom_valid);
constructor.Bind("mm_rom_valid", &mm_rom_valid);
version_string = recomp::get_project_version().to_string();
constructor.Bind("version_number", &version_string);
version_string = recomp::get_project_version().to_string();
constructor.Bind("version_number", &version_string);
model_handle = constructor.GetModelHandle();
}
model_handle = constructor.GetModelHandle();
}
};
std::unique_ptr<recompui::MenuController> recompui::create_launcher_menu() {

View File

@@ -0,0 +1,153 @@
#include "ui_mod_details_panel.h"
#include "librecomp/mods.hpp"
namespace recompui {
extern const std::string mod_tab_id;
ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
set_flex(1.0f, 1.0f, 200.0f);
set_height(100.0f, Unit::Percent);
set_display(Display::Flex);
set_flex_direction(FlexDirection::Column);
set_background_color(Color{ 190, 184, 219, 25 });
ContextId context = get_current_context();
header_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
header_container->set_flex(0.0f, 0.0f);
header_container->set_padding(16.0f);
header_container->set_gap(16.0f);
header_container->set_background_color(Color{ 0, 0, 0, 89 });
header_container->set_border_bottom_width(1.1f);
header_container->set_border_bottom_color(Color{ 255, 255, 255, 25 });
{
thumbnail_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
thumbnail_container->set_flex(0.0f, 0.0f);
{
thumbnail_image = context.create_element<Image>(thumbnail_container, "");
thumbnail_image->set_width(100.0f);
thumbnail_image->set_height(100.0f);
thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 });
}
header_details_container = context.create_element<Container>(header_container, FlexDirection::Column, JustifyContent::SpaceEvenly);
header_details_container->set_flex(1.0f, 1.0f);
header_details_container->set_flex_basis(100.0f, Unit::Percent);
header_details_container->set_text_align(TextAlign::Left);
{
title_label = context.create_element<Label>(header_details_container, LabelStyle::Large);
version_label = context.create_element<Label>(header_details_container, LabelStyle::Normal);
}
}
body_container = context.create_element<ScrollContainer>(this, ScrollDirection::Vertical);
body_container->set_text_align(TextAlign::Left);
body_container->set_padding(16.0f);
{
authors_label = context.create_element<Label>(body_container, LabelStyle::Normal);
authors_label->set_margin_bottom(16.0f);
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
}
buttons_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceAround);
buttons_container->set_flex(0.0f, 0.0f);
buttons_container->set_padding(16.0f);
buttons_container->set_justify_content(JustifyContent::SpaceBetween);
buttons_container->set_border_top_width(1.1f);
buttons_container->set_border_top_color(Color{ 255, 255, 255, 25 });
buttons_container->set_background_color(Color{ 0, 0, 0, 89 });
{
enable_container = context.create_element<Container>(buttons_container, FlexDirection::Row, JustifyContent::FlexStart);
enable_container->set_align_items(AlignItems::Center);
enable_container->set_gap(16.0f);
{
enable_toggle = context.create_element<Toggle>(enable_container);
enable_toggle->add_checked_callback([this](bool checked){ enable_toggle_checked(checked); });
enable_toggle->set_nav_manual(NavDirection::Up, mod_tab_id);
enable_label = context.create_element<Label>(enable_container, "A currently enabled mod requires this mod", LabelStyle::Annotation);
}
configure_button = context.create_element<Button>(buttons_container, "Configure", recompui::ButtonStyle::Secondary);
configure_button->add_pressed_callback([this](){ configure_button_pressed(); });
configure_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
clear_mod_navigation();
}
ModDetailsPanel::~ModDetailsPanel() {
}
void ModDetailsPanel::disable_toggle() {
enable_toggle->set_enabled(false);
}
void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled) {
cur_details = details;
thumbnail_image->set_src(thumbnail);
title_label->set_text(cur_details.display_name);
version_label->set_text(cur_details.version.to_string());
std::string authors_str = "Authors:";
bool first = true;
for (const std::string& author : details.authors) {
authors_str += (first ? " " : ", ") + author;
first = false;
}
authors_label->set_text(authors_str);
description_label->set_text(cur_details.description);
enable_toggle->set_checked(toggle_checked);
enable_toggle->set_enabled(toggle_enabled);
configure_button->set_enabled(configure_enabled);
enable_label->set_display(toggle_label_visible ? Display::Block : Display::None);
if (configure_enabled) {
enable_toggle->set_nav(NavDirection::Right, configure_button);
}
else {
enable_toggle->set_nav_none(NavDirection::Right);
}
}
void ModDetailsPanel::set_mod_toggled_callback(std::function<void(bool)> callback) {
mod_toggled_callback = callback;
}
void ModDetailsPanel::set_mod_configure_pressed_callback(std::function<void()> callback) {
mod_configure_pressed_callback = callback;
}
void ModDetailsPanel::setup_mod_navigation(Element* nav_target) {
enable_toggle->set_nav(NavDirection::Left, nav_target);
if (enable_toggle->is_enabled()) {
configure_button->set_nav(NavDirection::Left, enable_toggle);
}
else {
configure_button->set_nav(NavDirection::Left, nav_target);
}
}
void ModDetailsPanel::clear_mod_navigation() {
enable_toggle->set_nav_none(NavDirection::Left);
configure_button->set_nav_none(NavDirection::Left);
}
void ModDetailsPanel::enable_toggle_checked(bool checked) {
if (mod_toggled_callback != nullptr) {
mod_toggled_callback(checked);
}
}
void ModDetailsPanel::configure_button_pressed() {
if (mod_configure_pressed_callback != nullptr) {
mod_configure_pressed_callback();
}
}
} // namespace recompui

View File

@@ -0,0 +1,52 @@
#ifndef RECOMPUI_ELEMENT_MOD_DETAILS_PANEL_H
#define RECOMPUI_ELEMENT_MOD_DETAILS_PANEL_H
#include "librecomp/mods.hpp"
#include "elements/ui_button.h"
#include "elements/ui_container.h"
#include "elements/ui_image.h"
#include "elements/ui_label.h"
#include "elements/ui_toggle.h"
#include "elements/ui_scroll_container.h"
namespace recompui {
class ModDetailsPanel : public Element {
public:
ModDetailsPanel(Element *parent);
virtual ~ModDetailsPanel();
void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled);
void set_mod_toggled_callback(std::function<void(bool)> callback);
void set_mod_configure_pressed_callback(std::function<void()> callback);
void setup_mod_navigation(Element* nav_target);
void clear_mod_navigation();
Toggle* get_enable_toggle() { return enable_toggle; }
Button* get_configure_button() { return configure_button; }
void disable_toggle();
protected:
std::string_view get_type_name() override { return "ModDetailsPanel"; }
private:
recomp::mods::ModDetails cur_details;
Container *thumbnail_container = nullptr;
Image *thumbnail_image = nullptr;
Container *header_container = nullptr;
Container *header_details_container = nullptr;
Label *title_label = nullptr;
Label *version_label = nullptr;
ScrollContainer *body_container = nullptr;
Label *description_label = nullptr;
Label *authors_label = nullptr;
Container *buttons_container = nullptr;
Container *enable_container = nullptr;
Toggle *enable_toggle = nullptr;
Label *enable_label = nullptr;
Button *configure_button = nullptr;
std::function<void(bool)> mod_toggled_callback = nullptr;
std::function<void()> mod_configure_pressed_callback = nullptr;
void enable_toggle_checked(bool checked);
void configure_button_pressed();
};
} // namespace recompui
#endif

Some files were not shown because too many files have changed in this diff Show More