Initial boot

This commit is contained in:
Mr-Wiseguy
2025-02-17 21:45:16 -05:00
parent bed19575f3
commit ff73b2107b
168 changed files with 23034 additions and 2 deletions
+504
View File
@@ -0,0 +1,504 @@
#include "banjo_config.h"
#include "recomp_input.h"
#include "banjo_sound.h"
#include "banjo_render.h"
#include "ultramodern/config.hpp"
#include "librecomp/files.hpp"
#include <filesystem>
#include <fstream>
#include <iomanip>
#if defined(_WIN32)
#include <Shlobj.h>
#elif defined(__linux__)
#include <unistd.h>
#include <pwd.h>
#endif
constexpr std::u8string_view general_filename = u8"general.json";
constexpr std::u8string_view graphics_filename = u8"graphics.json";
constexpr std::u8string_view controls_filename = u8"controls.json";
constexpr std::u8string_view sound_filename = u8"sound.json";
constexpr auto res_default = ultramodern::renderer::Resolution::Auto;
constexpr auto hr_default = ultramodern::renderer::HUDRatioMode::Clamp16x9;
constexpr auto api_default = ultramodern::renderer::GraphicsApi::Auto;
constexpr auto ar_default = ultramodern::renderer::AspectRatio::Expand;
constexpr auto msaa_default = ultramodern::renderer::Antialiasing::MSAA2X;
constexpr auto rr_default = ultramodern::renderer::RefreshRate::Display;
constexpr auto hpfb_default = ultramodern::renderer::HighPrecisionFramebuffer::Off;
constexpr int ds_default = 1;
constexpr int rr_manual_default = 60;
constexpr bool developer_mode_default = false;
static bool is_steam_deck = false;
ultramodern::renderer::WindowMode wm_default() {
return is_steam_deck ? ultramodern::renderer::WindowMode::Fullscreen : ultramodern::renderer::WindowMode::Windowed;
}
#ifdef __gnu_linux__
void detect_steam_deck() {
// Check if the board vendor is Valve.
std::ifstream board_vendor_file("/sys/devices/virtual/dmi/id/board_vendor");
std::string line;
if (std::getline(board_vendor_file, line).good() && line == "Valve") {
is_steam_deck = true;
return;
}
// Check if the SteamDeck variable is set to 1.
const char* steam_deck_env = getenv("SteamDeck");
if (steam_deck_env != nullptr && std::string{steam_deck_env} == "1") {
is_steam_deck = true;
return;
}
is_steam_deck = false;
return;
}
#else
void detect_steam_deck() { is_steam_deck = false; }
#endif
template <typename T>
T from_or_default(const json& j, const std::string& key, T default_value) {
T ret;
auto find_it = j.find(key);
if (find_it != j.end()) {
find_it->get_to(ret);
}
else {
ret = default_value;
}
return ret;
}
template <typename T>
void call_if_key_exists(void (*func)(T), const json& j, const std::string& key) {
auto find_it = j.find(key);
if (find_it != j.end()) {
T val;
find_it->get_to(val);
func(val);
}
}
namespace ultramodern {
void to_json(json& j, const renderer::GraphicsConfig& config) {
j = json{
{"res_option", config.res_option},
{"wm_option", config.wm_option},
{"hr_option", config.hr_option},
{"api_option", config.api_option},
{"ds_option", config.ds_option},
{"ar_option", config.ar_option},
{"msaa_option", config.msaa_option},
{"rr_option", config.rr_option},
{"hpfb_option", config.hpfb_option},
{"rr_manual_value", config.rr_manual_value},
{"developer_mode", config.developer_mode},
};
}
void from_json(const json& j, renderer::GraphicsConfig& config) {
config.res_option = from_or_default(j, "res_option", res_default);
config.wm_option = from_or_default(j, "wm_option", wm_default());
config.hr_option = from_or_default(j, "hr_option", hr_default);
config.api_option = from_or_default(j, "api_option", api_default);
config.ds_option = from_or_default(j, "ds_option", ds_default);
config.ar_option = from_or_default(j, "ar_option", ar_default);
config.msaa_option = from_or_default(j, "msaa_option", msaa_default);
config.rr_option = from_or_default(j, "rr_option", rr_default);
config.hpfb_option = from_or_default(j, "hpfb_option", hpfb_default);
config.rr_manual_value = from_or_default(j, "rr_manual_value", rr_manual_default);
config.developer_mode = from_or_default(j, "developer_mode", developer_mode_default);
}
}
namespace recomp {
void to_json(json& j, const InputField& field) {
j = json{ {"input_type", field.input_type}, {"input_id", field.input_id} };
}
void from_json(const json& j, InputField& field) {
j.at("input_type").get_to(field.input_type);
j.at("input_id").get_to(field.input_id);
}
}
std::filesystem::path banjo::get_app_folder_path() {
// directly check for portable.txt (windows and native linux binary)
if (std::filesystem::exists("portable.txt")) {
return std::filesystem::current_path();
}
std::filesystem::path recomp_dir{};
#if defined(_WIN32)
// Deduce local app data path.
PWSTR known_path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &known_path);
if (result == S_OK) {
recomp_dir = std::filesystem::path{known_path} / banjo::program_id;
}
CoTaskMemFree(known_path);
#elif defined(__linux__)
// check for APP_FOLDER_PATH env var used by AppImage
if (getenv("APP_FOLDER_PATH") != nullptr) {
return std::filesystem::path{getenv("APP_FOLDER_PATH")};
}
const char *homedir;
if ((homedir = getenv("HOME")) == nullptr) {
homedir = getpwuid(getuid())->pw_dir;
}
if (homedir != nullptr) {
recomp_dir = std::filesystem::path{homedir} / (std::u8string{u8".config/"} + std::u8string{banjo::program_id});
}
#endif
return recomp_dir;
}
bool read_json(std::ifstream input_file, nlohmann::json& json_out) {
if (!input_file.good()) {
return false;
}
try {
input_file >> json_out;
}
catch (nlohmann::json::parse_error&) {
return false;
}
return true;
}
bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& json_out) {
// Try reading and parsing the base file.
if (read_json(std::ifstream{path}, json_out)) {
return true;
}
// Try reading and parsing the backup file.
if (read_json(recomp::open_input_backup_file(path), json_out)) {
return true;
}
// Both reads failed.
return false;
}
bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) {
{
std::ofstream output_file = recomp::open_output_file_with_backup(path);
if (!output_file.good()) {
return false;
}
output_file << std::setw(4) << json_data;
}
return recomp::finalize_output_file_with_backup(path);
}
bool save_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
recomp::to_json(config_json["background_input_mode"], recomp::get_background_input_mode());
config_json["rumble_strength"] = recomp::get_rumble_strength();
config_json["gyro_sensitivity"] = recomp::get_gyro_sensitivity();
config_json["mouse_sensitivity"] = recomp::get_mouse_sensitivity();
config_json["joystick_deadzone"] = recomp::get_joystick_deadzone();
config_json["camera_invert_mode"] = banjo::get_camera_invert_mode();
config_json["analog_cam_mode"] = banjo::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = banjo::get_analog_camera_invert_mode();
config_json["debug_mode"] = banjo::get_debug_mode_enabled();
return save_json_with_backups(path, config_json);
}
void set_general_settings_from_json(const nlohmann::json& config_json) {
recomp::set_background_input_mode(from_or_default(config_json, "background_input_mode", recomp::BackgroundInputMode::On));
recomp::set_rumble_strength(from_or_default(config_json, "rumble_strength", 25));
recomp::set_gyro_sensitivity(from_or_default(config_json, "gyro_sensitivity", 50));
recomp::set_mouse_sensitivity(from_or_default(config_json, "mouse_sensitivity", is_steam_deck ? 50 : 0));
recomp::set_joystick_deadzone(from_or_default(config_json, "joystick_deadzone", 5));
banjo::set_camera_invert_mode(from_or_default(config_json, "camera_invert_mode", banjo::CameraInvertMode::InvertY));
banjo::set_analog_cam_mode(from_or_default(config_json, "analog_cam_mode", banjo::AnalogCamMode::Off));
banjo::set_analog_camera_invert_mode(from_or_default(config_json, "analog_camera_invert_mode", banjo::CameraInvertMode::InvertNone));
banjo::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false));
}
bool load_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
return false;
}
set_general_settings_from_json(config_json);
return true;
}
void assign_mapping(recomp::InputDevice device, recomp::GameInput input, const std::vector<recomp::InputField>& value) {
for (size_t binding_index = 0; binding_index < std::min(value.size(), recomp::bindings_per_input); binding_index++) {
recomp::set_input_binding(input, binding_index, device, value[binding_index]);
}
};
// same as assign_mapping, except will clear unassigned bindings if not in value
void assign_mapping_complete(recomp::InputDevice device, recomp::GameInput input, const std::vector<recomp::InputField>& value) {
for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) {
if (binding_index >= value.size()) {
recomp::set_input_binding(input, binding_index, device, recomp::InputField{});
} else {
recomp::set_input_binding(input, binding_index, device, value[binding_index]);
}
}
};
void assign_all_mappings(recomp::InputDevice device, const recomp::DefaultN64Mappings& values) {
assign_mapping_complete(device, recomp::GameInput::A, values.a);
assign_mapping_complete(device, recomp::GameInput::B, values.b);
assign_mapping_complete(device, recomp::GameInput::Z, values.z);
assign_mapping_complete(device, recomp::GameInput::START, values.start);
assign_mapping_complete(device, recomp::GameInput::DPAD_UP, values.dpad_up);
assign_mapping_complete(device, recomp::GameInput::DPAD_DOWN, values.dpad_down);
assign_mapping_complete(device, recomp::GameInput::DPAD_LEFT, values.dpad_left);
assign_mapping_complete(device, recomp::GameInput::DPAD_RIGHT, values.dpad_right);
assign_mapping_complete(device, recomp::GameInput::L, values.l);
assign_mapping_complete(device, recomp::GameInput::R, values.r);
assign_mapping_complete(device, recomp::GameInput::C_UP, values.c_up);
assign_mapping_complete(device, recomp::GameInput::C_DOWN, values.c_down);
assign_mapping_complete(device, recomp::GameInput::C_LEFT, values.c_left);
assign_mapping_complete(device, recomp::GameInput::C_RIGHT, values.c_right);
assign_mapping_complete(device, recomp::GameInput::X_AXIS_NEG, values.analog_left);
assign_mapping_complete(device, recomp::GameInput::X_AXIS_POS, values.analog_right);
assign_mapping_complete(device, recomp::GameInput::Y_AXIS_NEG, values.analog_down);
assign_mapping_complete(device, recomp::GameInput::Y_AXIS_POS, values.analog_up);
assign_mapping_complete(device, recomp::GameInput::TOGGLE_MENU, values.toggle_menu);
assign_mapping_complete(device, recomp::GameInput::ACCEPT_MENU, values.accept_menu);
assign_mapping_complete(device, recomp::GameInput::APPLY_MENU, values.apply_menu);
};
void banjo::reset_input_bindings() {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
void banjo::reset_cont_input_bindings() {
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
void banjo::reset_kb_input_bindings() {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
}
void banjo::reset_single_input_binding(recomp::InputDevice device, recomp::GameInput input) {
assign_mapping_complete(
device,
input,
recomp::get_default_mapping_for_input(
device == recomp::InputDevice::Keyboard ?
recomp::default_n64_keyboard_mappings :
recomp::default_n64_controller_mappings,
input
)
);
}
void reset_graphics_options() {
ultramodern::renderer::GraphicsConfig new_config{};
new_config.res_option = res_default;
new_config.wm_option = wm_default();
new_config.hr_option = hr_default;
new_config.ds_option = ds_default;
new_config.ar_option = ar_default;
new_config.msaa_option = msaa_default;
new_config.rr_option = rr_default;
new_config.hpfb_option = hpfb_default;
new_config.rr_manual_value = rr_manual_default;
new_config.developer_mode = developer_mode_default;
ultramodern::renderer::set_graphics_config(new_config);
}
bool save_graphics_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
ultramodern::to_json(config_json, ultramodern::renderer::get_graphics_config());
return save_json_with_backups(path, config_json);
}
bool load_graphics_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
return false;
}
ultramodern::renderer::GraphicsConfig new_config{};
ultramodern::from_json(config_json, new_config);
ultramodern::renderer::set_graphics_config(new_config);
return true;
}
void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::InputDevice device) {
const std::string& input_name = recomp::get_input_enum_name(input);
nlohmann::json& out_array = out[input_name];
out_array = nlohmann::json::array();
for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) {
out_array[binding_index] = recomp::get_input_binding(input, binding_index, device);
}
};
bool save_controls_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
config_json["keyboard"] = {};
config_json["controller"] = {};
for (size_t i = 0; i < recomp::get_num_inputs(); i++) {
recomp::GameInput cur_input = static_cast<recomp::GameInput>(i);
add_input_bindings(config_json["keyboard"], cur_input, recomp::InputDevice::Keyboard);
add_input_bindings(config_json["controller"], cur_input, recomp::InputDevice::Controller);
}
return save_json_with_backups(path, config_json);
}
bool load_input_device_from_json(const nlohmann::json& config_json, recomp::InputDevice device, const std::string& key) {
// Check if the json object for the given key exists.
auto find_it = config_json.find(key);
if (find_it == config_json.end()) {
return false;
}
const nlohmann::json& mappings_json = *find_it;
for (size_t i = 0; i < recomp::get_num_inputs(); i++) {
recomp::GameInput cur_input = static_cast<recomp::GameInput>(i);
const std::string& input_name = recomp::get_input_enum_name(cur_input);
// Check if the json object for the given input exists and that it's an array.
auto find_input_it = mappings_json.find(input_name);
if (find_input_it == mappings_json.end() || !find_input_it->is_array()) {
assign_mapping(
device,
cur_input,
recomp::get_default_mapping_for_input(
device == recomp::InputDevice::Keyboard ?
recomp::default_n64_keyboard_mappings :
recomp::default_n64_controller_mappings,
cur_input
)
);
continue;
}
const nlohmann::json& input_json = *find_input_it;
// Deserialize all the bindings from the json array (up to the max number of bindings per input).
for (size_t binding_index = 0; binding_index < std::min(recomp::bindings_per_input, input_json.size()); binding_index++) {
recomp::InputField cur_field{};
recomp::from_json(input_json[binding_index], cur_field);
recomp::set_input_binding(cur_input, binding_index, device, cur_field);
}
}
return true;
}
bool load_controls_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
return false;
}
if (!load_input_device_from_json(config_json, recomp::InputDevice::Keyboard, "keyboard")) {
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
}
if (!load_input_device_from_json(config_json, recomp::InputDevice::Controller, "controller")) {
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings);
}
return true;
}
bool save_sound_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
config_json["main_volume"] = banjo::get_main_volume();
config_json["bgm_volume"] = banjo::get_bgm_volume();
return save_json_with_backups(path, config_json);
}
bool load_sound_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
return false;
}
banjo::reset_sound_settings();
call_if_key_exists(banjo::set_main_volume, config_json, "main_volume");
call_if_key_exists(banjo::set_bgm_volume, config_json, "bgm_volume");
return true;
}
void banjo::load_config() {
detect_steam_deck();
std::filesystem::path recomp_dir = banjo::get_app_folder_path();
std::filesystem::path general_path = recomp_dir / general_filename;
std::filesystem::path graphics_path = recomp_dir / graphics_filename;
std::filesystem::path controls_path = recomp_dir / controls_filename;
std::filesystem::path sound_path = recomp_dir / sound_filename;
if (!recomp_dir.empty()) {
std::filesystem::create_directories(recomp_dir);
}
// TODO error handling for failing to save config files after resetting them.
if (!load_general_config(general_path)) {
// Set the general settings from an empty json to use defaults.
set_general_settings_from_json({});
save_general_config(general_path);
}
if (!load_graphics_config(graphics_path)) {
reset_graphics_options();
save_graphics_config(graphics_path);
}
if (!load_controls_config(controls_path)) {
banjo::reset_input_bindings();
save_controls_config(controls_path);
}
if (!load_sound_config(sound_path)) {
banjo::reset_sound_settings();
save_sound_config(sound_path);
}
}
void banjo::save_config() {
std::filesystem::path recomp_dir = banjo::get_app_folder_path();
if (recomp_dir.empty()) {
return;
}
std::filesystem::create_directories(recomp_dir);
// TODO error handling for failing to save config files.
save_general_config(recomp_dir / general_filename);
save_graphics_config(recomp_dir / graphics_filename);
save_controls_config(recomp_dir / controls_filename);
save_sound_config(recomp_dir / sound_filename);
}
+116
View File
@@ -0,0 +1,116 @@
#include <array>
#include "librecomp/helpers.hpp"
#include "recomp_input.h"
#include "ultramodern/ultramodern.hpp"
// Arrays that hold the mappings for every input for keyboard and controller respectively.
using input_mapping = std::array<recomp::InputField, recomp::bindings_per_input>;
using input_mapping_array = std::array<input_mapping, static_cast<size_t>(recomp::GameInput::COUNT)>;
static input_mapping_array keyboard_input_mappings{};
static input_mapping_array controller_input_mappings{};
// Make the button value array, which maps a button index to its bit field.
#define DEFINE_INPUT(name, value, readable) uint16_t(value##u),
static const std::array n64_button_values = {
DEFINE_N64_BUTTON_INPUTS()
};
#undef DEFINE_INPUT
// Make the input name array.
#define DEFINE_INPUT(name, value, readable) readable,
static const std::vector<std::string> input_names = {
DEFINE_ALL_INPUTS()
};
#undef DEFINE_INPUT
// Make the input enum name array.
#define DEFINE_INPUT(name, value, readable) #name,
static const std::vector<std::string> input_enum_names = {
DEFINE_ALL_INPUTS()
};
#undef DEFINE_INPUT
size_t recomp::get_num_inputs() {
return (size_t)GameInput::COUNT;
}
const std::string& recomp::get_input_name(GameInput input) {
return input_names.at(static_cast<size_t>(input));
}
const std::string& recomp::get_input_enum_name(GameInput input) {
return input_enum_names.at(static_cast<size_t>(input));
}
recomp::GameInput recomp::get_input_from_enum_name(const std::string_view enum_name) {
auto find_it = std::find(input_enum_names.begin(), input_enum_names.end(), enum_name);
if (find_it == input_enum_names.end()) {
return recomp::GameInput::COUNT;
}
return static_cast<recomp::GameInput>(find_it - input_enum_names.begin());
}
// Due to an RmlUi limitation this can't be const. Ideally it would return a const reference or even just a straight up copy.
recomp::InputField& recomp::get_input_binding(GameInput input, size_t binding_index, recomp::InputDevice device) {
input_mapping_array& device_mappings = (device == recomp::InputDevice::Controller) ? controller_input_mappings : keyboard_input_mappings;
input_mapping& cur_input_mapping = device_mappings.at(static_cast<size_t>(input));
if (binding_index < cur_input_mapping.size()) {
return cur_input_mapping[binding_index];
}
else {
static recomp::InputField dummy_field = {};
return dummy_field;
}
}
void recomp::set_input_binding(recomp::GameInput input, size_t binding_index, recomp::InputDevice device, recomp::InputField value) {
input_mapping_array& device_mappings = (device == recomp::InputDevice::Controller) ? controller_input_mappings : keyboard_input_mappings;
input_mapping& cur_input_mapping = device_mappings.at(static_cast<size_t>(input));
if (binding_index < cur_input_mapping.size()) {
cur_input_mapping[binding_index] = value;
}
}
bool recomp::get_n64_input(int controller_num, uint16_t* buttons_out, float* x_out, float* y_out) {
uint16_t cur_buttons = 0;
float cur_x = 0.0f;
float cur_y = 0.0f;
if (controller_num != 0) {
return false;
}
if (!recomp::game_input_disabled()) {
for (size_t i = 0; i < n64_button_values.size(); i++) {
size_t input_index = (size_t)GameInput::N64_BUTTON_START + i;
cur_buttons |= recomp::get_input_digital(keyboard_input_mappings[input_index]) ? n64_button_values[i] : 0;
cur_buttons |= recomp::get_input_digital(controller_input_mappings[input_index]) ? n64_button_values[i] : 0;
}
float joystick_deadzone = recomp::get_joystick_deadzone() / 100.0f;
float joystick_x = recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::X_AXIS_POS])
- recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::X_AXIS_NEG]);
float joystick_y = recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::Y_AXIS_POS])
- recomp::get_input_analog(controller_input_mappings[(size_t)GameInput::Y_AXIS_NEG]);
recomp::apply_joystick_deadzone(joystick_x, joystick_y, &joystick_x, &joystick_y);
cur_x = recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::X_AXIS_POS])
- recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::X_AXIS_NEG]) + joystick_x;
cur_y = recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::Y_AXIS_POS])
- recomp::get_input_analog(keyboard_input_mappings[(size_t)GameInput::Y_AXIS_NEG]) + joystick_y;
}
*buttons_out = cur_buttons;
*x_out = std::clamp(cur_x, -1.0f, 1.0f);
*y_out = std::clamp(cur_y, -1.0f, 1.0f);
return true;
}
+4
View File
@@ -0,0 +1,4 @@
#include <atomic>
#include "banjo_debug.h"
#include "librecomp/helpers.hpp"
#include "../patches/input.h"
+870
View File
@@ -0,0 +1,870 @@
#include <atomic>
#include <mutex>
#include "ultramodern/ultramodern.hpp"
#include "recomp.h"
#include "recomp_input.h"
#include "banjo_config.h"
#include "recomp_ui.h"
#include "SDL.h"
#include "promptfont.h"
#include "GamepadMotion.hpp"
constexpr float axis_threshold = 0.5f;
struct ControllerState {
SDL_GameController* controller;
std::array<float, 3> latest_accelerometer;
GamepadMotion motion;
uint32_t prev_gyro_timestamp;
ControllerState() : controller{}, latest_accelerometer{}, motion{}, prev_gyro_timestamp{} {
motion.Reset();
motion.SetCalibrationMode(GamepadMotionHelpers::CalibrationMode::Stillness | GamepadMotionHelpers::CalibrationMode::SensorFusion);
};
};
static struct {
const Uint8* keys = nullptr;
SDL_Keymod keymod = SDL_Keymod::KMOD_NONE;
int numkeys = 0;
std::atomic_int32_t mouse_wheel_pos = 0;
std::mutex cur_controllers_mutex;
std::vector<SDL_GameController*> cur_controllers{};
std::unordered_map<SDL_JoystickID, ControllerState> controller_states;
std::array<float, 2> rotation_delta{};
std::array<float, 2> mouse_delta{};
std::mutex pending_input_mutex;
std::array<float, 2> pending_rotation_delta{};
std::array<float, 2> pending_mouse_delta{};
float cur_rumble;
bool rumble_active;
} InputState;
std::atomic<recomp::InputDevice> scanning_device = recomp::InputDevice::COUNT;
std::atomic<recomp::InputField> scanned_input;
enum class InputType {
None = 0, // Using zero for None ensures that default initialized InputFields are unbound.
Keyboard,
Mouse,
ControllerDigital,
ControllerAnalog // Axis input_id values are the SDL value + 1
};
void set_scanned_input(recomp::InputField value) {
scanning_device.store(recomp::InputDevice::COUNT);
scanned_input.store(value);
}
recomp::InputField recomp::get_scanned_input() {
recomp::InputField ret = scanned_input.load();
scanned_input.store({});
return ret;
}
void recomp::start_scanning_input(recomp::InputDevice device) {
scanned_input.store({});
scanning_device.store(device);
}
void recomp::stop_scanning_input() {
scanning_device.store(recomp::InputDevice::COUNT);
}
void queue_if_enabled(SDL_Event* event) {
if (!recomp::all_input_disabled()) {
recompui::queue_event(*event);
}
}
static std::atomic_bool cursor_enabled = true;
void recompui::set_cursor_visible(bool visible) {
cursor_enabled.store(visible);
}
bool should_override_keystate(SDL_Scancode key, SDL_Keymod mod) {
// Override Enter when Alt is held.
if (key == SDL_Scancode::SDL_SCANCODE_RETURN) {
if (mod & SDL_Keymod::KMOD_ALT) {
return true;
}
}
return false;
}
bool sdl_event_filter(void* userdata, SDL_Event* event) {
switch (event->type) {
case SDL_EventType::SDL_KEYDOWN:
{
SDL_KeyboardEvent* keyevent = &event->key;
// Skip repeated events when not in the menu
if (!recompui::is_context_taking_input() &&
event->key.repeat) {
break;
}
if ((keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_RETURN && (keyevent->keysym.mod & SDL_Keymod::KMOD_ALT)) ||
keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_F11
) {
recompui::toggle_fullscreen();
}
if (scanning_device != recomp::InputDevice::COUNT) {
if (keyevent->keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
recomp::cancel_scanning_input();
} else if (scanning_device == recomp::InputDevice::Keyboard) {
set_scanned_input({(uint32_t)InputType::Keyboard, keyevent->keysym.scancode});
}
} else {
if (!should_override_keystate(keyevent->keysym.scancode, static_cast<SDL_Keymod>(keyevent->keysym.mod))) {
queue_if_enabled(event);
}
}
}
break;
case SDL_EventType::SDL_CONTROLLERDEVICEADDED:
{
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
SDL_GameController* controller = SDL_GameControllerOpen(controller_event->which);
printf("Controller added: %d\n", controller_event->which);
if (controller != nullptr) {
printf(" Instance ID: %d\n", SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)));
ControllerState& state = InputState.controller_states[SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))];
state.controller = controller;
if (SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_GYRO) && SDL_GameControllerHasSensor(controller, SDL_SensorType::SDL_SENSOR_ACCEL)) {
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_GYRO, SDL_TRUE);
SDL_GameControllerSetSensorEnabled(controller, SDL_SensorType::SDL_SENSOR_ACCEL, SDL_TRUE);
}
}
}
break;
case SDL_EventType::SDL_CONTROLLERDEVICEREMOVED:
{
SDL_ControllerDeviceEvent* controller_event = &event->cdevice;
printf("Controller removed: %d\n", controller_event->which);
InputState.controller_states.erase(controller_event->which);
}
break;
case SDL_EventType::SDL_QUIT: {
if (!ultramodern::is_game_started()) {
ultramodern::quit();
return true;
}
recompui::ContextId config_context_id = recompui::get_config_context_id();
if (!recompui::is_context_shown(config_context_id)) {
recompui::show_context(config_context_id, "");
}
banjo::open_quit_game_prompt();
recompui::activate_mouse();
break;
}
case SDL_EventType::SDL_MOUSEWHEEL:
{
SDL_MouseWheelEvent* wheel_event = &event->wheel;
InputState.mouse_wheel_pos.fetch_add(wheel_event->y * (wheel_event->direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1));
}
queue_if_enabled(event);
break;
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
if (scanning_device != recomp::InputDevice::COUNT) {
auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
// note - magic number: 0 is InputType::None
if ((menuToggleBinding0.input_type != 0 && event->cbutton.button == menuToggleBinding0.input_id) ||
(menuToggleBinding1.input_type != 0 && event->cbutton.button == menuToggleBinding1.input_id)) {
recomp::cancel_scanning_input();
} else if (scanning_device == recomp::InputDevice::Controller) {
SDL_ControllerButtonEvent* button_event = &event->cbutton;
auto scanned_input_index = recomp::get_scanned_input_index();
if ((scanned_input_index == static_cast<int>(recomp::GameInput::TOGGLE_MENU) ||
scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) && (
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT ||
button_event->button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
break;
}
set_scanned_input({(uint32_t)InputType::ControllerDigital, button_event->button});
}
} else {
queue_if_enabled(event);
}
break;
case SDL_EventType::SDL_CONTROLLERAXISMOTION:
if (scanning_device == recomp::InputDevice::Controller) {
auto scanned_input_index = recomp::get_scanned_input_index();
if (scanned_input_index == static_cast<int>(recomp::GameInput::TOGGLE_MENU) ||
scanned_input_index == static_cast<int>(recomp::GameInput::ACCEPT_MENU) ||
scanned_input_index == static_cast<int>(recomp::GameInput::APPLY_MENU)) {
break;
}
SDL_ControllerAxisEvent* axis_event = &event->caxis;
float axis_value = axis_event->value * (1/32768.0f);
if (axis_value > axis_threshold) {
SDL_Event set_stick_return_event;
set_stick_return_event.type = SDL_USEREVENT;
set_stick_return_event.user.code = axis_event->axis;
set_stick_return_event.user.data1 = nullptr;
set_stick_return_event.user.data2 = nullptr;
recompui::queue_event(set_stick_return_event);
set_scanned_input({(uint32_t)InputType::ControllerAnalog, axis_event->axis + 1});
}
else if (axis_value < -axis_threshold) {
SDL_Event set_stick_return_event;
set_stick_return_event.type = SDL_USEREVENT;
set_stick_return_event.user.code = axis_event->axis;
set_stick_return_event.user.data1 = nullptr;
set_stick_return_event.user.data2 = nullptr;
recompui::queue_event(set_stick_return_event);
set_scanned_input({(uint32_t)InputType::ControllerAnalog, -axis_event->axis - 1});
}
} else {
queue_if_enabled(event);
}
break;
case SDL_EventType::SDL_CONTROLLERSENSORUPDATE:
if (event->csensor.sensor == SDL_SensorType::SDL_SENSOR_ACCEL) {
// Convert acceleration to g's.
float x = event->csensor.data[0] / SDL_STANDARD_GRAVITY;
float y = event->csensor.data[1] / SDL_STANDARD_GRAVITY;
float z = event->csensor.data[2] / SDL_STANDARD_GRAVITY;
ControllerState& state = InputState.controller_states[event->csensor.which];
state.latest_accelerometer[0] = x;
state.latest_accelerometer[1] = y;
state.latest_accelerometer[2] = z;
}
else if (event->csensor.sensor == SDL_SensorType::SDL_SENSOR_GYRO) {
// constexpr float gyro_threshold = 0.05f;
// Convert rotational velocity to degrees per second.
constexpr float rad_to_deg = 180.0f / M_PI;
float x = event->csensor.data[0] * rad_to_deg;
float y = event->csensor.data[1] * rad_to_deg;
float z = event->csensor.data[2] * rad_to_deg;
ControllerState& state = InputState.controller_states[event->csensor.which];
uint64_t cur_timestamp = event->csensor.timestamp;
uint32_t delta_ms = cur_timestamp - state.prev_gyro_timestamp;
state.motion.ProcessMotion(x, y, z, state.latest_accelerometer[0], state.latest_accelerometer[1], state.latest_accelerometer[2], delta_ms * 0.001f);
state.prev_gyro_timestamp = cur_timestamp;
float rot_x = 0.0f;
float rot_y = 0.0f;
state.motion.GetPlayerSpaceGyro(rot_x, rot_y);
{
std::lock_guard lock{ InputState.pending_input_mutex };
InputState.pending_rotation_delta[0] += rot_x;
InputState.pending_rotation_delta[1] += rot_y;
}
}
break;
case SDL_EventType::SDL_MOUSEMOTION:
if (!recomp::game_input_disabled()) {
SDL_MouseMotionEvent* motion_event = &event->motion;
std::lock_guard lock{ InputState.pending_input_mutex };
InputState.pending_mouse_delta[0] += motion_event->xrel;
InputState.pending_mouse_delta[1] += motion_event->yrel;
}
default:
queue_if_enabled(event);
break;
}
return false;
}
void recomp::handle_events() {
SDL_Event cur_event;
static bool exited = false;
while (SDL_PollEvent(&cur_event) && !exited) {
exited = sdl_event_filter(nullptr, &cur_event);
// Lock the cursor if all three conditions are true: mouse aiming is enabled, game input is not disabled, and the game has been started.
bool cursor_locked = (recomp::get_mouse_sensitivity() != 0) && !recomp::game_input_disabled() && ultramodern::is_game_started();
// Hide the cursor based on its enable state, but override visibility to false if the cursor is locked.
bool cursor_visible = cursor_enabled;
if (cursor_locked) {
cursor_visible = false;
}
SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE);
SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE);
}
}
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B;
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X;
constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y;
const recomp::DefaultN64Mappings recomp::default_n64_keyboard_mappings = {
.a = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_SPACE}
},
.b = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_LSHIFT}
},
.l = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_E}
},
.r = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_R}
},
.z = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_Q}
},
.start = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RETURN}
},
.c_left = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_LEFT}
},
.c_right = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RIGHT}
},
.c_up = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_UP}
},
.c_down = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_DOWN}
},
.dpad_left = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_J}
},
.dpad_right = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_L}
},
.dpad_up = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_I}
},
.dpad_down = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_K}
},
.analog_left = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_A}
},
.analog_right = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_D}
},
.analog_up = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_W}
},
.analog_down = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_S}
},
.toggle_menu = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_ESCAPE}
},
.accept_menu = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_RETURN}
},
.apply_menu = {
{.input_type = (uint32_t)InputType::Keyboard, .input_id = SDL_SCANCODE_F}
}
};
const recomp::DefaultN64Mappings recomp::default_n64_controller_mappings = {
.a = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_SOUTH},
},
.b = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_WEST},
},
.l = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
},
.r = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_TRIGGERRIGHT + 1},
},
.z = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_TRIGGERLEFT + 1},
},
.start = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_START},
},
.c_left = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_RIGHTX + 1)},
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_NORTH},
},
.c_right = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_RIGHTX + 1},
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_EAST},
},
.c_up = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_RIGHTY + 1)},
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_RIGHTSTICK},
},
.c_down = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_RIGHTY + 1},
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
},
.dpad_left = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_LEFT},
},
.dpad_right = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_RIGHT},
},
.dpad_up = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_UP},
},
.dpad_down = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_DPAD_DOWN},
},
.analog_left = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_LEFTX + 1)},
},
.analog_right = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_LEFTX + 1},
},
.analog_up = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = -(SDL_CONTROLLER_AXIS_LEFTY + 1)},
},
.analog_down = {
{.input_type = (uint32_t)InputType::ControllerAnalog, .input_id = SDL_CONTROLLER_AXIS_LEFTY + 1},
},
.toggle_menu = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_BACK},
},
.accept_menu = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_SOUTH},
},
.apply_menu = {
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_WEST},
{.input_type = (uint32_t)InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_START}
}
};
void recomp::poll_inputs() {
InputState.keys = SDL_GetKeyboardState(&InputState.numkeys);
InputState.keymod = SDL_GetModState();
{
std::lock_guard lock{ InputState.cur_controllers_mutex };
InputState.cur_controllers.clear();
for (const auto& [id, state] : InputState.controller_states) {
(void)id; // Avoid unused variable warning.
SDL_GameController* controller = state.controller;
if (controller != nullptr) {
InputState.cur_controllers.push_back(controller);
}
}
}
// Read the deltas while resetting them to zero.
{
std::lock_guard lock{ InputState.pending_input_mutex };
InputState.rotation_delta = InputState.pending_rotation_delta;
InputState.pending_rotation_delta = { 0.0f, 0.0f };
InputState.mouse_delta = InputState.pending_mouse_delta;
InputState.pending_mouse_delta = { 0.0f, 0.0f };
}
}
void recomp::set_rumble(int controller_num, bool on) {
if (controller_num == 0) {
InputState.rumble_active = on;
}
}
ultramodern::input::connected_device_info_t recomp::get_connected_device_info(int controller_num) {
switch (controller_num) {
case 0:
return ultramodern::input::connected_device_info_t {
.connected_device = ultramodern::input::Device::Controller,
.connected_pak = ultramodern::input::Pak::RumblePak,
};
}
return ultramodern::input::connected_device_info_t {
.connected_device = ultramodern::input::Device::None,
.connected_pak = ultramodern::input::Pak::None,
};
}
static float smoothstep(float from, float to, float amount) {
amount = (amount * amount) * (3.0f - 2.0f * amount);
return std::lerp(from, to, amount);
}
// Update rumble to attempt to mimic the way n64 rumble ramps up and falls off
void recomp::update_rumble() {
// Note: values are not accurate! just approximations based on feel
if (InputState.rumble_active) {
InputState.cur_rumble += 0.17f;
if (InputState.cur_rumble > 1) InputState.cur_rumble = 1;
} else {
InputState.cur_rumble *= 0.92f;
InputState.cur_rumble -= 0.01f;
if (InputState.cur_rumble < 0) InputState.cur_rumble = 0;
}
float smooth_rumble = smoothstep(0, 1, InputState.cur_rumble);
uint16_t rumble_strength = smooth_rumble * (recomp::get_rumble_strength() * 0xFFFF / 100);
uint32_t duration = 1000000; // Dummy duration value that lasts long enough to matter as the game will reset rumble on its own.
{
std::lock_guard lock{ InputState.cur_controllers_mutex };
for (const auto& controller : InputState.cur_controllers) {
SDL_GameControllerRumble(controller, 0, rumble_strength, duration);
}
}
}
bool controller_button_state(int32_t input_id) {
if (input_id >= 0 && input_id < SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MAX) {
SDL_GameControllerButton button = (SDL_GameControllerButton)input_id;
bool ret = false;
{
std::lock_guard lock{ InputState.cur_controllers_mutex };
for (const auto& controller : InputState.cur_controllers) {
ret |= SDL_GameControllerGetButton(controller, button);
}
}
return ret;
}
return false;
}
static std::atomic_bool right_analog_suppressed = false;
float controller_axis_state(int32_t input_id, bool allow_suppression) {
if (abs(input_id) - 1 < SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_MAX) {
SDL_GameControllerAxis axis = (SDL_GameControllerAxis)(abs(input_id) - 1);
bool negative_range = input_id < 0;
float ret = 0.0f;
{
std::lock_guard lock{ InputState.cur_controllers_mutex };
for (const auto& controller : InputState.cur_controllers) {
float cur_val = SDL_GameControllerGetAxis(controller, axis) * (1/32768.0f);
if (negative_range) {
cur_val = -cur_val;
}
// Check if this input is a right analog axis and suppress it accordingly.
if (allow_suppression && right_analog_suppressed.load() &&
(axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX || axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY)) {
cur_val = 0;
}
ret += std::clamp(cur_val, 0.0f, 1.0f);
}
}
return std::clamp(ret, 0.0f, 1.0f);
}
return false;
}
float recomp::get_input_analog(const recomp::InputField& field) {
switch ((InputType)field.input_type) {
case InputType::Keyboard:
if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) {
if (should_override_keystate(static_cast<SDL_Scancode>(field.input_id), InputState.keymod)) {
return 0.0f;
}
return InputState.keys[field.input_id] ? 1.0f : 0.0f;
}
return 0.0f;
case InputType::ControllerDigital:
return controller_button_state(field.input_id) ? 1.0f : 0.0f;
case InputType::ControllerAnalog:
return controller_axis_state(field.input_id, true);
case InputType::Mouse:
// TODO mouse support
return 0.0f;
case InputType::None:
return false;
}
}
float recomp::get_input_analog(const std::span<const recomp::InputField> fields) {
float ret = 0.0f;
for (const auto& field : fields) {
ret += get_input_analog(field);
}
return std::clamp(ret, 0.0f, 1.0f);
}
bool recomp::get_input_digital(const recomp::InputField& field) {
switch ((InputType)field.input_type) {
case InputType::Keyboard:
if (InputState.keys && field.input_id >= 0 && field.input_id < InputState.numkeys) {
if (should_override_keystate(static_cast<SDL_Scancode>(field.input_id), InputState.keymod)) {
return false;
}
return InputState.keys[field.input_id] != 0;
}
return false;
case InputType::ControllerDigital:
return controller_button_state(field.input_id);
case InputType::ControllerAnalog:
// TODO adjustable threshold
return controller_axis_state(field.input_id, true) >= axis_threshold;
case InputType::Mouse:
// TODO mouse support
return false;
case InputType::None:
return false;
}
}
bool recomp::get_input_digital(const std::span<const recomp::InputField> fields) {
bool ret = 0;
for (const auto& field : fields) {
ret |= get_input_digital(field);
}
return ret;
}
void recomp::get_gyro_deltas(float* x, float* y) {
std::array<float, 2> cur_rotation_delta = InputState.rotation_delta;
float sensitivity = (float)recomp::get_gyro_sensitivity() / 100.0f;
*x = cur_rotation_delta[0] * sensitivity;
*y = cur_rotation_delta[1] * sensitivity;
}
void recomp::get_mouse_deltas(float* x, float* y) {
std::array<float, 2> cur_mouse_delta = InputState.mouse_delta;
float sensitivity = (float)recomp::get_mouse_sensitivity() / 100.0f;
*x = cur_mouse_delta[0] * sensitivity;
*y = cur_mouse_delta[1] * sensitivity;
}
void recomp::apply_joystick_deadzone(float x_in, float y_in, float* x_out, float* y_out) {
float joystick_deadzone = (float)recomp::get_joystick_deadzone() / 100.0f;
if(fabsf(x_in) < joystick_deadzone) {
x_in = 0.0f;
}
else {
if(x_in > 0.0f) {
x_in -= joystick_deadzone;
}
else {
x_in += joystick_deadzone;
}
x_in /= (1.0f - joystick_deadzone);
}
if(fabsf(y_in) < joystick_deadzone) {
y_in = 0.0f;
}
else {
if(y_in > 0.0f) {
y_in -= joystick_deadzone;
}
else {
y_in += joystick_deadzone;
}
y_in /= (1.0f - joystick_deadzone);
}
*x_out = x_in;
*y_out = y_in;
}
void recomp::get_right_analog(float* x, float* y) {
float x_val =
controller_axis_state((SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false) -
controller_axis_state(-(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX + 1), false);
float y_val =
controller_axis_state((SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false) -
controller_axis_state(-(SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY + 1), false);
recomp::apply_joystick_deadzone(x_val, y_val, x, y);
}
void recomp::set_right_analog_suppressed(bool suppressed) {
right_analog_suppressed.store(suppressed);
}
bool recomp::game_input_disabled() {
// Disable input if any menu that blocks input is open.
return recompui::is_context_taking_input();
}
bool recomp::all_input_disabled() {
// Disable all input if an input is being polled.
return scanning_device != recomp::InputDevice::COUNT;
}
std::string controller_button_to_string(SDL_GameControllerButton button) {
switch (button) {
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_A:
return PF_GAMEPAD_A;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_B:
return PF_GAMEPAD_B;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_X:
return PF_GAMEPAD_X;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_Y:
return PF_GAMEPAD_Y;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_BACK:
return PF_XBOX_VIEW;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_GUIDE:
return PF_GAMEPAD_HOME;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_START:
return PF_XBOX_MENU;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSTICK:
return PF_ANALOG_L_CLICK;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSTICK:
return PF_ANALOG_R_CLICK;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
return PF_XBOX_LEFT_SHOULDER;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
return PF_XBOX_RIGHT_SHOULDER;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP:
return PF_DPAD_UP;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN:
return PF_DPAD_DOWN;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT:
return PF_DPAD_LEFT;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
return PF_DPAD_RIGHT;
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
// return "";
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD:
return PF_SONY_TOUCHPAD;
default:
return "Button " + std::to_string(button);
}
}
std::unordered_map<SDL_Scancode, std::string> scancode_codepoints {
{SDL_SCANCODE_LEFT, PF_KEYBOARD_LEFT},
// NOTE: UP and RIGHT are swapped with promptfont.
{SDL_SCANCODE_UP, PF_KEYBOARD_RIGHT},
{SDL_SCANCODE_RIGHT, PF_KEYBOARD_UP},
{SDL_SCANCODE_DOWN, PF_KEYBOARD_DOWN},
{SDL_SCANCODE_A, PF_KEYBOARD_A},
{SDL_SCANCODE_B, PF_KEYBOARD_B},
{SDL_SCANCODE_C, PF_KEYBOARD_C},
{SDL_SCANCODE_D, PF_KEYBOARD_D},
{SDL_SCANCODE_E, PF_KEYBOARD_E},
{SDL_SCANCODE_F, PF_KEYBOARD_F},
{SDL_SCANCODE_G, PF_KEYBOARD_G},
{SDL_SCANCODE_H, PF_KEYBOARD_H},
{SDL_SCANCODE_I, PF_KEYBOARD_I},
{SDL_SCANCODE_J, PF_KEYBOARD_J},
{SDL_SCANCODE_K, PF_KEYBOARD_K},
{SDL_SCANCODE_L, PF_KEYBOARD_L},
{SDL_SCANCODE_M, PF_KEYBOARD_M},
{SDL_SCANCODE_N, PF_KEYBOARD_N},
{SDL_SCANCODE_O, PF_KEYBOARD_O},
{SDL_SCANCODE_P, PF_KEYBOARD_P},
{SDL_SCANCODE_Q, PF_KEYBOARD_Q},
{SDL_SCANCODE_R, PF_KEYBOARD_R},
{SDL_SCANCODE_S, PF_KEYBOARD_S},
{SDL_SCANCODE_T, PF_KEYBOARD_T},
{SDL_SCANCODE_U, PF_KEYBOARD_U},
{SDL_SCANCODE_V, PF_KEYBOARD_V},
{SDL_SCANCODE_W, PF_KEYBOARD_W},
{SDL_SCANCODE_X, PF_KEYBOARD_X},
{SDL_SCANCODE_Y, PF_KEYBOARD_Y},
{SDL_SCANCODE_Z, PF_KEYBOARD_Z},
{SDL_SCANCODE_0, PF_KEYBOARD_0},
{SDL_SCANCODE_1, PF_KEYBOARD_1},
{SDL_SCANCODE_2, PF_KEYBOARD_2},
{SDL_SCANCODE_3, PF_KEYBOARD_3},
{SDL_SCANCODE_4, PF_KEYBOARD_4},
{SDL_SCANCODE_5, PF_KEYBOARD_5},
{SDL_SCANCODE_6, PF_KEYBOARD_6},
{SDL_SCANCODE_7, PF_KEYBOARD_7},
{SDL_SCANCODE_8, PF_KEYBOARD_8},
{SDL_SCANCODE_9, PF_KEYBOARD_9},
{SDL_SCANCODE_ESCAPE, PF_KEYBOARD_ESCAPE},
{SDL_SCANCODE_F1, PF_KEYBOARD_F1},
{SDL_SCANCODE_F2, PF_KEYBOARD_F2},
{SDL_SCANCODE_F3, PF_KEYBOARD_F3},
{SDL_SCANCODE_F4, PF_KEYBOARD_F4},
{SDL_SCANCODE_F5, PF_KEYBOARD_F5},
{SDL_SCANCODE_F6, PF_KEYBOARD_F6},
{SDL_SCANCODE_F7, PF_KEYBOARD_F7},
{SDL_SCANCODE_F8, PF_KEYBOARD_F8},
{SDL_SCANCODE_F9, PF_KEYBOARD_F9},
{SDL_SCANCODE_F10, PF_KEYBOARD_F10},
{SDL_SCANCODE_F11, PF_KEYBOARD_F11},
{SDL_SCANCODE_F12, PF_KEYBOARD_F12},
{SDL_SCANCODE_PRINTSCREEN, PF_KEYBOARD_PRINT_SCREEN},
{SDL_SCANCODE_SCROLLLOCK, PF_KEYBOARD_SCROLL_LOCK},
{SDL_SCANCODE_PAUSE, PF_KEYBOARD_PAUSE},
{SDL_SCANCODE_INSERT, PF_KEYBOARD_INSERT},
{SDL_SCANCODE_HOME, PF_KEYBOARD_HOME},
{SDL_SCANCODE_PAGEUP, PF_KEYBOARD_PAGE_UP},
{SDL_SCANCODE_DELETE, PF_KEYBOARD_DELETE},
{SDL_SCANCODE_END, PF_KEYBOARD_END},
{SDL_SCANCODE_PAGEDOWN, PF_KEYBOARD_PAGE_DOWN},
{SDL_SCANCODE_SPACE, PF_KEYBOARD_SPACE},
{SDL_SCANCODE_BACKSPACE, PF_KEYBOARD_BACKSPACE},
{SDL_SCANCODE_TAB, PF_KEYBOARD_TAB},
{SDL_SCANCODE_RETURN, PF_KEYBOARD_ENTER},
{SDL_SCANCODE_CAPSLOCK, PF_KEYBOARD_CAPS},
{SDL_SCANCODE_NUMLOCKCLEAR, PF_KEYBOARD_NUM_LOCK},
{SDL_SCANCODE_LSHIFT, "L" PF_KEYBOARD_SHIFT},
{SDL_SCANCODE_RSHIFT, "R" PF_KEYBOARD_SHIFT},
};
std::string keyboard_input_to_string(SDL_Scancode key) {
if (scancode_codepoints.find(key) != scancode_codepoints.end()) {
return scancode_codepoints[key];
}
return std::to_string(key);
}
std::string controller_axis_to_string(int axis) {
bool positive = axis > 0;
SDL_GameControllerAxis actual_axis = SDL_GameControllerAxis(abs(axis) - 1);
switch (actual_axis) {
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX:
return positive ? "\u21C0" : "\u21BC";
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY:
return positive ? "\u21C2" : "\u21BE";
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTX:
return positive ? "\u21C1" : "\u21BD";
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_RIGHTY:
return positive ? "\u21C3" : "\u21BF";
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERLEFT:
return positive ? "\u2196" : "\u21DC";
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
return positive ? "\u2197" : "\u21DD";
default:
return "Axis " + std::to_string(actual_axis) + (positive ? '+' : '-');
}
}
std::string recomp::InputField::to_string() const {
switch ((InputType)input_type) {
case InputType::None:
return "";
case InputType::ControllerDigital:
return controller_button_to_string((SDL_GameControllerButton)input_id);
case InputType::ControllerAnalog:
return controller_axis_to_string(input_id);
case InputType::Keyboard:
return keyboard_input_to_string((SDL_Scancode)input_id);
default:
return std::to_string(input_type) + "," + std::to_string(input_id);
}
}
+241
View File
@@ -0,0 +1,241 @@
#include <cmath>
#include "recomp.h"
#include "librecomp/overlays.hpp"
#include "librecomp/addresses.hpp"
#include "banjo_config.h"
#include "recomp_input.h"
#include "recomp_ui.h"
#include "banjo_render.h"
#include "banjo_sound.h"
#include "librecomp/helpers.hpp"
#include "../patches/input.h"
#include "../patches/graphics.h"
#include "../patches/sound.h"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
extern "C" void recomp_update_inputs(uint8_t* rdram, recomp_context* ctx) {
recomp::poll_inputs();
}
extern "C" void recomp_puts(uint8_t* rdram, recomp_context* ctx) {
PTR(char) cur_str = _arg<0, PTR(char)>(rdram, ctx);
u32 length = _arg<1, u32>(rdram, ctx);
for (u32 i = 0; i < length; i++) {
fputc(MEM_B(i, (gpr)cur_str), stdout);
}
}
extern "C" void recomp_exit(uint8_t* rdram, recomp_context* ctx) {
ultramodern::quit();
}
extern "C" void recomp_get_gyro_deltas(uint8_t* rdram, recomp_context* ctx) {
float* x_out = _arg<0, float*>(rdram, ctx);
float* y_out = _arg<1, float*>(rdram, ctx);
recomp::get_gyro_deltas(x_out, y_out);
}
extern "C" void recomp_get_mouse_deltas(uint8_t* rdram, recomp_context* ctx) {
float* x_out = _arg<0, float*>(rdram, ctx);
float* y_out = _arg<1, float*>(rdram, ctx);
recomp::get_mouse_deltas(x_out, y_out);
}
extern "C" void recomp_powf(uint8_t* rdram, recomp_context* ctx) {
float a = _arg<0, float>(rdram, ctx);
float b = ctx->f14.fl; //_arg<1, float>(rdram, ctx);
_return(ctx, std::pow(a, b));
}
extern "C" void recomp_get_target_framerate(uint8_t* rdram, recomp_context* ctx) {
int frame_divisor = _arg<0, u32>(rdram, ctx);
_return(ctx, ultramodern::get_target_framerate(60 / frame_divisor));
}
extern "C" void recomp_get_window_resolution(uint8_t* rdram, recomp_context* ctx) {
int width, height;
recompui::get_window_size(width, height);
gpr width_out = _arg<0, PTR(u32)>(rdram, ctx);
gpr height_out = _arg<1, PTR(u32)>(rdram, ctx);
MEM_W(0, width_out) = (u32)width;
MEM_W(0, height_out) = (u32)height;
}
extern "C" void recomp_get_target_aspect_ratio(uint8_t* rdram, recomp_context* ctx) {
ultramodern::renderer::GraphicsConfig graphics_config = ultramodern::renderer::get_graphics_config();
float original = _arg<0, float>(rdram, ctx);
int width, height;
recompui::get_window_size(width, height);
switch (graphics_config.ar_option) {
case ultramodern::renderer::AspectRatio::Original:
default:
_return(ctx, original);
return;
case ultramodern::renderer::AspectRatio::Expand:
_return(ctx, std::max(static_cast<float>(width) / height, original));
return;
}
}
extern "C" void recomp_get_bgm_volume(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, banjo::get_bgm_volume() / 100.0f);
}
extern "C" void recomp_time_us(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, static_cast<u32>(std::chrono::duration_cast<std::chrono::microseconds>(ultramodern::time_since_start()).count()));
}
extern "C" void recomp_load_overlays(uint8_t * rdram, recomp_context * ctx) {
u32 rom = _arg<0, u32>(rdram, ctx);
PTR(void) ram = _arg<1, PTR(void)>(rdram, ctx);
u32 size = _arg<2, u32>(rdram, ctx);
load_overlays(rom, ram, size);
}
extern "C" void recomp_high_precision_fb_enabled(uint8_t * rdram, recomp_context * ctx) {
_return(ctx, static_cast<s32>(banjo::renderer::RT64HighPrecisionFBEnabled()));
}
extern "C" void recomp_get_resolution_scale(uint8_t* rdram, recomp_context* ctx) {
_return(ctx, ultramodern::get_resolution_scale());
}
extern "C" void recomp_get_inverted_axes(uint8_t* rdram, recomp_context* ctx) {
s32* x_out = _arg<0, s32*>(rdram, ctx);
s32* y_out = _arg<1, s32*>(rdram, ctx);
banjo::CameraInvertMode mode = banjo::get_camera_invert_mode();
*x_out = (mode == banjo::CameraInvertMode::InvertX || mode == banjo::CameraInvertMode::InvertBoth);
*y_out = (mode == banjo::CameraInvertMode::InvertY || mode == banjo::CameraInvertMode::InvertBoth);
}
extern "C" void recomp_get_analog_inverted_axes(uint8_t* rdram, recomp_context* ctx) {
s32* x_out = _arg<0, s32*>(rdram, ctx);
s32* y_out = _arg<1, s32*>(rdram, ctx);
banjo::CameraInvertMode mode = banjo::get_analog_camera_invert_mode();
*x_out = (mode == banjo::CameraInvertMode::InvertX || mode == banjo::CameraInvertMode::InvertBoth);
*y_out = (mode == banjo::CameraInvertMode::InvertY || mode == banjo::CameraInvertMode::InvertBoth);
}
extern "C" void recomp_get_analog_cam_enabled(uint8_t* rdram, recomp_context* ctx) {
_return<s32>(ctx, banjo::get_analog_cam_mode() == banjo::AnalogCamMode::On);
}
extern "C" void recomp_get_camera_inputs(uint8_t* rdram, recomp_context* ctx) {
float* x_out = _arg<0, float*>(rdram, ctx);
float* y_out = _arg<1, float*>(rdram, ctx);
// TODO expose this in the menu
constexpr float radial_deadzone = 0.05f;
float x, y;
recomp::get_right_analog(&x, &y);
float magnitude = sqrtf(x * x + y * y);
if (magnitude < radial_deadzone) {
*x_out = 0.0f;
*y_out = 0.0f;
}
else {
float x_normalized = x / magnitude;
float y_normalized = y / magnitude;
*x_out = x_normalized * ((magnitude - radial_deadzone) / (1 - radial_deadzone));
*y_out = y_normalized * ((magnitude - radial_deadzone) / (1 - radial_deadzone));
}
}
extern "C" void recomp_set_right_analog_suppressed(uint8_t* rdram, recomp_context* ctx) {
s32 suppressed = _arg<0, s32>(rdram, ctx);
recomp::set_right_analog_suppressed(suppressed);
}
// Function with typo in decomp
extern "C" void osWriteBackDCacheAll(uint8_t* rdram, recomp_context* ctx) {}
extern "C" void boot_osPiRawStartDma(uint8_t* rdram, recomp_context* ctx) {
uint32_t direction = ctx->r4;
uint32_t device_address = ctx->r5;
gpr rdram_address = ctx->r6;
uint32_t size = ctx->r7;
assert(direction == 0); // Only reads
// Complete the DMA synchronously (the game immediately waits until it's done anyways)
recomp::do_rom_read(rdram, rdram_address, device_address + recomp::rom_base, size);
}
constexpr uint32_t k1_to_phys(uint32_t addr) {
return addr & 0x1FFFFFFF;
}
extern "C" void osPiReadIo_recomp(RDRAM_ARG recomp_context * ctx) {
uint32_t devAddr = recomp::rom_base | ctx->r4;
gpr dramAddr = ctx->r5;
uint32_t physical_addr = k1_to_phys(devAddr);
if (physical_addr > recomp::rom_base) {
// cart rom
recomp::do_rom_pio(PASS_RDRAM dramAddr, physical_addr);
} else {
// sram
assert(false && "SRAM ReadIo unimplemented");
}
ctx->r2 = 0;
}
extern "C" void boot___osInitialize_common(uint8_t* rdram, recomp_context* ctx) {}
extern "C" void boot_osPiGetStatus(uint8_t* rdram, recomp_context* ctx) {
// PI not busy
ctx->r2 = 0;
}
extern "C" void osPfsInit_recomp(uint8_t * rdram, recomp_context* ctx) {
ctx->r2 = 11; // PFS_ERR_DEVICE
}
extern "C" void __ll_lshift_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a << b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
extern "C" void __ull_rshift_recomp(uint8_t * rdram, recomp_context * ctx) {
uint64_t a = (ctx->r4 << 32) | ((ctx->r5 << 0) & 0xFFFFFFFFu);
uint64_t b = (ctx->r6 << 32) | ((ctx->r7 << 0) & 0xFFFFFFFFu);
uint64_t ret = a >> b;
ctx->r2 = (int32_t)(ret >> 32);
ctx->r3 = (int32_t)(ret >> 0);
}
// u32 rom_addr, void *ram_addr, u32 size
extern "C" void recomp_load_overlays_by_rom(uint8_t* rdram, recomp_context* ctx) {
u32 rom_addr = _arg<0, u32>(rdram, ctx);
PTR(void) ram_addr = _arg<1, PTR(void)>(rdram, ctx);
u32 size = _arg<2, u32>(rdram, ctx);
load_overlays(rom_addr, ram_addr, size);
}
+123
View File
@@ -0,0 +1,123 @@
#include <cassert>
#include <cstring>
#include <fstream>
#include "librecomp/game.hpp"
#include "banjo_game.h"
#ifdef _MSC_VER
inline uint32_t byteswap(uint32_t val) {
return _byteswap_ulong(val);
}
#else
constexpr uint32_t byteswap(uint32_t val) {
return __builtin_bswap32(val);
}
#endif
// Produces a decompressed BK rom. This is only needed because the game has compressed code.
// For other recomps using this repo as an example, you can omit the decompression routine and
// set the corresponding fields in the GameEntry if the game doesn't have compressed code,
// even if it does have compressed data.
std::vector<uint8_t> banjo::decompress_bk(std::span<const uint8_t> compressed_rom) {
return {};
// Sanity check the rom size and header. These should already be correct from the runtime's check,
// but it should prevent this file from accidentally being copied to another recomp.
// if (compressed_rom.size() != 0x2000000) {
// assert(false);
// return {};
// }
// if (compressed_rom[0x3B] != 'N' || compressed_rom[0x3C] != 'Z' || compressed_rom[0x3D] != 'S' || compressed_rom[0x3E] != 'E') {
// assert(false);
// return {};
// }
// struct DmaDataEntry {
// uint32_t vrom_start;
// uint32_t vrom_end;
// uint32_t rom_start;
// uint32_t rom_end;
// void bswap() {
// vrom_start = byteswap(vrom_start);
// vrom_end = byteswap(vrom_end);
// rom_start = byteswap(rom_start);
// rom_end = byteswap(rom_end);
// }
// };
// DmaDataEntry cur_entry{};
// size_t cur_entry_index = 0;
// constexpr size_t dma_data_rom_addr = 0x1A500;
// std::vector<uint8_t> ret{};
// ret.resize(0x2F00000);
// size_t content_end = 0;
// do {
// // Read the entry from the compressed rom.
// size_t cur_entry_rom_address = dma_data_rom_addr + (cur_entry_index++) * sizeof(DmaDataEntry);
// memcpy(&cur_entry, compressed_rom.data() + cur_entry_rom_address, sizeof(DmaDataEntry));
// // Swap the entry to native endianness after reading from the big endian data.
// cur_entry.bswap();
// // Rom end being 0 means the data is already uncompressed, so copy it as-is to vrom start.
// size_t entry_decompressed_size = cur_entry.vrom_end - cur_entry.vrom_start;
// if (cur_entry.rom_end == 0) {
// memcpy(ret.data() + cur_entry.vrom_start, compressed_rom.data() + cur_entry.rom_start, entry_decompressed_size);
// // Edit the entry to account for it being in a new location now.
// cur_entry.rom_start = cur_entry.vrom_start;
// }
// // Otherwise, decompress the input data into the output data.
// else {
// if (cur_entry.rom_end != cur_entry.rom_start) {
// // Validate the presence of the yaz0 header.
// if (compressed_rom[cur_entry.rom_start + 0] != 'Y' ||
// compressed_rom[cur_entry.rom_start + 1] != 'a' ||
// compressed_rom[cur_entry.rom_start + 2] != 'z' ||
// compressed_rom[cur_entry.rom_start + 3] != '0')
// {
// assert(false);
// return {};
// }
// // Skip the yaz0 header.
// size_t compressed_data_rom_start = cur_entry.rom_start + 0x10;
// size_t entry_compressed_size = cur_entry.rom_end - compressed_data_rom_start;
// std::span input_span = std::span{ compressed_rom }.subspan(compressed_data_rom_start, entry_compressed_size);
// std::span output_span = std::span{ ret }.subspan(cur_entry.vrom_start, entry_decompressed_size);
// yaz0_decompress(input_span, output_span);
// // Edit the entry to account for it being decompressed now.
// cur_entry.rom_start = cur_entry.vrom_start;
// cur_entry.rom_end = 0;
// }
// }
// if (entry_decompressed_size != 0) {
// if (cur_entry.vrom_end > content_end) {
// content_end = cur_entry.vrom_end;
// }
// }
// // Swap the entry back to big endian for writing.
// cur_entry.bswap();
// // Write the modified entry to the decompressed rom.
// memcpy(ret.data() + cur_entry_rom_address, &cur_entry, sizeof(DmaDataEntry));
// } while (cur_entry.vrom_end != 0);
// // Align the start of padding to the closest 0x1000 (matches decomp rom decompression behavior).
// content_end = (content_end + 0x1000 - 1) & -0x1000;
// // Write 0xFF as the padding.
// std::fill(ret.begin() + content_end, ret.end(), 0xFF);
}
void banjo::bk_on_init(uint8_t* rdram, recomp_context* ctx) {
MEM_W(0, (int32_t)0x80000310) = 6103;
recomp::do_rom_read(rdram, (int32_t)0x80000000, 0x100004C0, 0x2A4);
}
+663
View File
@@ -0,0 +1,663 @@
#include <cstdio>
#include <cassert>
#include <unordered_map>
#include <vector>
#include <array>
#include <filesystem>
#include <numeric>
#include <stdexcept>
#include <cinttypes>
#include "nfd.h"
#include "ultramodern/ultra64.h"
#include "ultramodern/ultramodern.hpp"
#define SDL_MAIN_HANDLED
#ifdef _WIN32
#include "SDL.h"
#else
#include "SDL2/SDL.h"
#include "SDL2/SDL_syswm.h"
#endif
#include "recomp_ui.h"
#include "recomp_input.h"
#include "banjo_config.h"
#include "banjo_sound.h"
#include "banjo_render.h"
#include "banjo_game.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
#include "librecomp/mods.hpp"
#include "librecomp/helpers.hpp"
#include "../../patches/graphics.h"
#include "../../patches/input.h"
#include "../../patches/sound.h"
#include "../../patches/misc_funcs.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include "SDL_syswm.h"
#endif
#include "../../lib/rt64/src/contrib/stb/stb_image.h"
const std::string version_string = "0.0.1";
template<typename... Ts>
void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
((void)fprintf(stderr, str, args), ...);
assert(false);
std::quick_exit(EXIT_FAILURE);
}
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2");
SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) > 0) {
exit_error("Failed to initialize SDL2: %s\n", SDL_GetError());
}
fprintf(stdout, "SDL Video Driver: %s\n", SDL_GetCurrentVideoDriver());
return {};
}
#if defined(__gnu_linux__)
#include "icon_bytes.h"
bool SetImageAsIcon(const char* filename, SDL_Window* window)
{
// Read data
int width, height, bytesPerPixel;
void* data = stbi_load_from_memory(reinterpret_cast<const uint8_t*>(icon_bytes), sizeof(icon_bytes), &width, &height, &bytesPerPixel, 4);
// Calculate pitch
int pitch;
pitch = width * 4;
pitch = (pitch + 3) & ~3;
// Setup relevance bitmask
int Rmask, Gmask, Bmask, Amask;
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
Rmask = 0x000000FF;
Gmask = 0x0000FF00;
Bmask = 0x00FF0000;
Amask = 0xFF000000;
#else
Rmask = 0xFF000000;
Gmask = 0x00FF0000;
Bmask = 0x0000FF00;
Amask = 0x000000FF;
#endif
SDL_Surface* surface = nullptr;
if (data != nullptr) {
surface = SDL_CreateRGBSurfaceFrom(data, width, height, 32, pitch, Rmask, Gmask,
Bmask, Amask);
}
if (surface == nullptr) {
if (data != nullptr) {
stbi_image_free(data);
}
return false;
} else {
SDL_SetWindowIcon(window,surface);
SDL_FreeSurface(surface);
stbi_image_free(data);
return true;
}
}
#endif
SDL_Window* window;
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
uint32_t flags = SDL_WINDOW_RESIZABLE;
#if defined(RT64_SDL_WINDOW_VULKAN)
flags |= SDL_WINDOW_VULKAN;
#endif
window = SDL_CreateWindow("Banjo: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, flags);
#if defined(__linux__)
SetImageAsIcon("icons/512.png",window);
if (ultramodern::renderer::get_graphics_config().wm_option == ultramodern::renderer::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux
SDL_SetWindowFullscreen(window,SDL_WINDOW_FULLSCREEN_DESKTOP);
} else {
SDL_SetWindowFullscreen(window,0);
}
#endif
if (window == nullptr) {
exit_error("Failed to create window: %s\n", SDL_GetError());
}
SDL_SysWMinfo wmInfo;
SDL_VERSION(&wmInfo.version);
SDL_GetWindowWMInfo(window, &wmInfo);
#if defined(_WIN32)
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
#elif defined(__linux__) || defined(__ANDROID__)
return ultramodern::renderer::WindowHandle{ window };
#else
static_assert(false && "Unimplemented");
#endif
}
void update_gfx(void*) {
recomp::handle_events();
}
static SDL_AudioCVT audio_convert;
static SDL_AudioDeviceID audio_device = 0;
// Samples per channel per second.
static uint32_t sample_rate = 48000;
static uint32_t output_sample_rate = 48000;
// Channel count.
constexpr uint32_t input_channels = 2;
static uint32_t output_channels = 2;
// Terminology: a frame is a collection of samples for each channel. e.g. 2 input samples is one input frame. This is unrelated to graphical frames.
// Number of frames to duplicate for fixing interpolation at the start and end of a chunk.
constexpr uint32_t duplicated_input_frames = 4;
// The number of output frames to skip for playback (to avoid playing duplicate inputs twice).
static uint32_t discarded_output_frames;
constexpr uint32_t bytes_per_frame = input_channels * sizeof(float);
void queue_samples(int16_t* audio_data, size_t sample_count) {
// Buffer for holding the output of swapping the audio channels. This is reused across
// calls to reduce runtime allocations.
static std::vector<float> swap_buffer;
static std::array<float, duplicated_input_frames * input_channels> duplicated_sample_buffer;
// Make sure the swap buffer is large enough to hold the audio data, including any extra space needed for resampling.
size_t resampled_sample_count = sample_count + duplicated_input_frames * input_channels;
size_t max_sample_count = std::max(resampled_sample_count, resampled_sample_count * audio_convert.len_mult);
if (max_sample_count > swap_buffer.size()) {
swap_buffer.resize(max_sample_count);
}
// Copy the duplicated frames from last chunk into this chunk
for (size_t i = 0; i < duplicated_input_frames * input_channels; i++) {
swap_buffer[i] = duplicated_sample_buffer[i];
}
// Convert the audio from 16-bit values to floats and swap the audio channels into the
// swap buffer to correct for the address xor caused by endianness handling.
float cur_main_volume = banjo::get_main_volume() / 100.0f; // Get the current main volume, normalized to 0.0-1.0.
for (size_t i = 0; i < sample_count; i += input_channels) {
swap_buffer[i + 0 + duplicated_input_frames * input_channels] = audio_data[i + 1] * (0.5f / 32768.0f) * cur_main_volume;
swap_buffer[i + 1 + duplicated_input_frames * input_channels] = audio_data[i + 0] * (0.5f / 32768.0f) * cur_main_volume;
}
// TODO handle cases where a chunk is smaller than the duplicated frame count.
assert(sample_count > duplicated_input_frames * input_channels);
// Copy the last converted samples into the duplicated sample buffer to reuse in resampling the next queued chunk.
for (size_t i = 0; i < duplicated_input_frames * input_channels; i++) {
duplicated_sample_buffer[i] = swap_buffer[i + sample_count];
}
audio_convert.buf = reinterpret_cast<Uint8*>(swap_buffer.data());
audio_convert.len = (sample_count + duplicated_input_frames * input_channels) * sizeof(swap_buffer[0]);
int ret = SDL_ConvertAudio(&audio_convert);
if (ret < 0) {
printf("Error using SDL audio converter: %s\n", SDL_GetError());
throw std::runtime_error("Error using SDL audio converter");
}
uint64_t cur_queued_microseconds = uint64_t(SDL_GetQueuedAudioSize(audio_device)) / bytes_per_frame * 1000000 / sample_rate;
uint32_t num_bytes_to_queue = audio_convert.len_cvt - output_channels * discarded_output_frames * sizeof(swap_buffer[0]);
float* samples_to_queue = swap_buffer.data() + output_channels * discarded_output_frames / 2;
// Prevent audio latency from building up by skipping samples in incoming audio when too many samples are already queued.
// Skip samples based on how many microseconds of samples are queued already.
uint32_t skip_factor = cur_queued_microseconds / 100000;
if (skip_factor != 0) {
uint32_t skip_ratio = 1 << skip_factor;
num_bytes_to_queue /= skip_ratio;
for (size_t i = 0; i < num_bytes_to_queue / (output_channels * sizeof(swap_buffer[0])); i++) {
samples_to_queue[2 * i + 0] = samples_to_queue[2 * skip_ratio * i + 0];
samples_to_queue[2 * i + 1] = samples_to_queue[2 * skip_ratio * i + 1];
}
}
// Queue the swapped audio data.
// Offset the data start by only half the discarded frame count as the other half of the discarded frames are at the end of the buffer.
SDL_QueueAudio(audio_device, samples_to_queue, num_bytes_to_queue);
}
size_t get_frames_remaining() {
constexpr float buffer_offset_frames = 1.0f;
// Get the number of remaining buffered audio bytes.
uint64_t buffered_byte_count = SDL_GetQueuedAudioSize(audio_device);
// Scale the byte count based on the ratio of sample rates and channel counts.
buffered_byte_count = buffered_byte_count * 2 * sample_rate / output_sample_rate / output_channels;
// Adjust the reported count to be some number of refreshes in the future, which helps ensure that
// there are enough samples even if the audio thread experiences a small amount of lag. This prevents
// audio popping on games that use the buffered audio byte count to determine how many samples
// to generate.
uint32_t frames_per_vi = (sample_rate / 60);
if (buffered_byte_count > (buffer_offset_frames * bytes_per_frame * frames_per_vi)) {
buffered_byte_count -= (buffer_offset_frames * bytes_per_frame * frames_per_vi);
}
else {
buffered_byte_count = 0;
}
// Convert from byte count to sample count.
return static_cast<uint32_t>(buffered_byte_count / bytes_per_frame);
}
void update_audio_converter() {
int ret = SDL_BuildAudioCVT(&audio_convert, AUDIO_F32, input_channels, sample_rate, AUDIO_F32, output_channels, output_sample_rate);
if (ret < 0) {
printf("Error creating SDL audio converter: %s\n", SDL_GetError());
throw std::runtime_error("Error creating SDL audio converter");
}
// Calculate the number of samples to discard based on the sample rate ratio and the duplicate frame count.
discarded_output_frames = duplicated_input_frames * output_sample_rate / sample_rate;
}
void set_frequency(uint32_t freq) {
sample_rate = freq;
update_audio_converter();
}
void reset_audio(uint32_t output_freq) {
SDL_AudioSpec spec_desired{
.freq = (int)output_freq,
.format = AUDIO_F32,
.channels = (Uint8)output_channels,
.silence = 0, // calculated
.samples = 0x100, // Fairly small sample count to reduce the latency of internal buffering
.padding = 0, // unused
.size = 0, // calculated
.callback = nullptr,
.userdata = nullptr
};
audio_device = SDL_OpenAudioDevice(nullptr, false, &spec_desired, nullptr, 0);
if (audio_device == 0) {
exit_error("SDL error opening audio device: %s\n", SDL_GetError());
}
SDL_PauseAudioDevice(audio_device, 0);
output_sample_rate = output_freq;
update_audio_converter();
}
extern RspUcodeFunc n_aspMain;
RspUcodeFunc* get_rsp_microcode(const OSTask* task) {
switch (task->t.type) {
case M_AUDTASK:
return n_aspMain;
default:
fprintf(stderr, "Unknown task: %" PRIu32 "\n", task->t.type);
return nullptr;
}
}
extern "C" void recomp_entrypoint(uint8_t * rdram, recomp_context * ctx);
gpr get_entrypoint_address();
// array of supported GameEntry objects
std::vector<recomp::GameEntry> supported_games = {
{
.rom_hash = 0x1B67585D56E07F8CULL,
.internal_name = "Banjo-Kazooie",
.game_id = u8"bk.n64.us.1.0",
.mod_game_id = "bk",
.save_type = recomp::SaveType::Eep4k,
.is_enabled = false,
.decompression_routine = banjo::decompress_bk,
.has_compressed_code = true,
.entrypoint_address = get_entrypoint_address(),
.entrypoint = recomp_entrypoint,
.on_init_callback = banjo::bk_on_init,
},
};
// TODO: move somewhere else
namespace banjo {
std::string get_game_thread_name(const OSThread* t) {
std::string name = "[Game] ";
switch (t->id) {
case 0:
switch (t->priority) {
case 150:
name += "PIMGR";
break;
case 80:
name += "VIMGR";
break;
default:
name += std::to_string(t->id);
break;
}
break;
case 1:
name += "INIT";
break;
case 2:
name += "DEFRAG";
break;
case 4:
name += "AUDIO";
break;
case 5:
name += "RESET";
break;
case 6:
name += "MAIN";
break;
case 7:
name += "CONT";
break;
case 8:
name += "RUMBLE";
break;
default:
name += std::to_string(t->id);
break;
}
return name;
}
}
#ifdef _WIN32
struct PreloadContext {
HANDLE handle;
HANDLE mapping_handle;
SIZE_T size;
PVOID view;
};
bool preload_executable(PreloadContext& context) {
wchar_t module_name[MAX_PATH];
GetModuleFileNameW(NULL, module_name, MAX_PATH);
context.handle = CreateFileW(module_name, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (context.handle == INVALID_HANDLE_VALUE) {
fprintf(stderr, "Failed to load executable into memory!");
context = {};
return false;
}
LARGE_INTEGER module_size;
if (!GetFileSizeEx(context.handle, &module_size)) {
fprintf(stderr, "Failed to get size of executable!");
CloseHandle(context.handle);
context = {};
return false;
}
context.size = module_size.QuadPart;
context.mapping_handle = CreateFileMappingW(context.handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (context.mapping_handle == nullptr) {
fprintf(stderr, "Failed to create file mapping of executable!");
CloseHandle(context.handle);
context = {};
return EXIT_FAILURE;
}
context.view = MapViewOfFile(context.mapping_handle, FILE_MAP_READ, 0, 0, 0);
if (context.view == nullptr) {
fprintf(stderr, "Failed to map view of of executable!");
CloseHandle(context.mapping_handle);
CloseHandle(context.handle);
context = {};
return false;
}
DWORD pid = GetCurrentProcessId();
HANDLE process_handle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_QUERY_INFORMATION, FALSE, pid);
if (process_handle == nullptr) {
fprintf(stderr, "Failed to open own process!");
CloseHandle(context.mapping_handle);
CloseHandle(context.handle);
context = {};
return false;
}
SIZE_T minimum_set_size, maximum_set_size;
if (!GetProcessWorkingSetSize(process_handle, &minimum_set_size, &maximum_set_size)) {
fprintf(stderr, "Failed to get working set size!");
CloseHandle(context.mapping_handle);
CloseHandle(context.handle);
context = {};
return false;
}
if (!SetProcessWorkingSetSize(process_handle, minimum_set_size + context.size, maximum_set_size + context.size)) {
fprintf(stderr, "Failed to set working set size!");
CloseHandle(context.mapping_handle);
CloseHandle(context.handle);
context = {};
return false;
}
if (VirtualLock(context.view, context.size) == 0) {
fprintf(stderr, "Failed to lock view of executable! (Error: %08lx)\n", GetLastError());
CloseHandle(context.mapping_handle);
CloseHandle(context.handle);
context = {};
return false;
}
return true;
}
void release_preload(PreloadContext& context) {
VirtualUnlock(context.view, context.size);
CloseHandle(context.mapping_handle);
CloseHandle(context.handle);
context = {};
}
#else
struct PreloadContext {
};
// TODO implement on other platforms
bool preload_executable(PreloadContext& context) {
return false;
}
void release_preload(PreloadContext& context) {
}
#endif
void enable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
banjo::renderer::enable_texture_pack(mod);
}
void disable_texture_pack(recomp::mods::ModContext& context, const recomp::mods::ModHandle& mod) {
(void)context;
banjo::renderer::disable_texture_pack(mod);
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
int main(int argc, char** argv) {
(void)argc;
(void)argv;
recomp::Version project_version{};
if (!recomp::Version::from_string(version_string, project_version)) {
ultramodern::error_handling::message_box(("Invalid version string: " + version_string).c_str());
return EXIT_FAILURE;
}
// Map this executable into memory and lock it, which should keep it in physical memory. This ensures
// that there are no stutters from the OS having to load new pages of the executable whenever a new code page is run.
PreloadContext preload_context;
bool preloaded = preload_executable(preload_context);
if (!preloaded) {
fprintf(stderr, "Failed to preload executable!\n");
}
#ifdef _WIN32
// Set up console output to accept UTF-8 on windows
SetConsoleOutputCP(CP_UTF8);
// Initialize native file dialogs.
NFD_Init();
// Change to a font that supports Japanese characters
CONSOLE_FONT_INFOEX cfi;
cfi.cbSize = sizeof cfi;
cfi.nFont = 0;
cfi.dwFontSize.X = 0;
cfi.dwFontSize.Y = 16;
cfi.FontFamily = FF_DONTCARE;
cfi.FontWeight = FW_NORMAL;
wcscpy_s(cfi.FaceName, L"NSimSun");
SetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi);
#endif
#ifdef _WIN32
// Force wasapi on Windows, as there seems to be some issue with sample queueing with directsound currently.
SDL_setenv("SDL_AUDIODRIVER", "wasapi", true);
#endif
//printf("Current dir: %ls\n", std::filesystem::current_path().c_str());
// Initialize SDL audio and set the output frequency.
SDL_InitSubSystem(SDL_INIT_AUDIO);
reset_audio(48000);
// Source controller mappings file
if (SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt") < 0) {
fprintf(stderr, "Failed to load controller mappings: %s\n", SDL_GetError());
}
recomp::register_config_path(banjo::get_app_folder_path());
// Register supported games and patches
for (const auto& game : supported_games) {
recomp::register_game(game);
}
REGISTER_FUNC(recomp_get_window_resolution);
REGISTER_FUNC(recomp_get_target_aspect_ratio);
REGISTER_FUNC(recomp_get_target_framerate);
REGISTER_FUNC(recomp_get_analog_cam_enabled);
REGISTER_FUNC(recomp_get_camera_inputs);
REGISTER_FUNC(recomp_get_bgm_volume);
REGISTER_FUNC(recomp_get_gyro_deltas);
REGISTER_FUNC(recomp_get_mouse_deltas);
REGISTER_FUNC(recomp_get_inverted_axes);
REGISTER_FUNC(recomp_get_analog_inverted_axes);
recompui::register_ui_exports();
banjo::register_bk_overlays();
banjo::register_bk_patches();
banjo::load_config();
recomp::rsp::callbacks_t rsp_callbacks{
.get_rsp_microcode = get_rsp_microcode,
};
ultramodern::renderer::callbacks_t renderer_callbacks{
.create_render_context = banjo::renderer::create_render_context,
};
ultramodern::gfx_callbacks_t gfx_callbacks{
.create_gfx = create_gfx,
.create_window = create_window,
.update_gfx = update_gfx,
};
ultramodern::audio_callbacks_t audio_callbacks{
.queue_samples = queue_samples,
.get_frames_remaining = get_frames_remaining,
.set_frequency = set_frequency,
};
ultramodern::input::callbacks_t input_callbacks{
.poll_input = recomp::poll_inputs,
.get_input = recomp::get_n64_input,
.set_rumble = recomp::set_rumble,
.get_connected_device_info = recomp::get_connected_device_info,
};
ultramodern::events::callbacks_t thread_callbacks{
.vi_callback = recomp::update_rumble,
.gfx_init_callback = recompui::update_supported_options,
};
ultramodern::error_handling::callbacks_t error_handling_callbacks{
.message_box = recompui::message_box,
};
ultramodern::threads::callbacks_t threads_callbacks{
.get_game_thread_name = banjo::get_game_thread_name,
};
// Register the texture pack content type with rt64.json as its content file.
recomp::mods::ModContentType texture_pack_content_type{
.content_filename = "rt64.json",
.allow_runtime_toggle = true,
.on_enabled = enable_texture_pack,
.on_disabled = disable_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::start(
project_version,
{},
rsp_callbacks,
renderer_callbacks,
audio_callbacks,
input_callbacks,
gfx_callbacks,
thread_callbacks,
error_handling_callbacks,
threads_callbacks
);
NFD_Quit();
if (preloaded) {
release_preload(preload_context);
}
return EXIT_SUCCESS;
}
+19
View File
@@ -0,0 +1,19 @@
#include "ovl_patches.hpp"
#include "../../RecompiledFuncs/recomp_overlays.inl"
#include "librecomp/overlays.hpp"
void banjo::register_bk_overlays() {
recomp::overlays::overlay_section_table_data_t sections {
.code_sections = section_table,
.num_code_sections = ARRLEN(section_table),
.total_num_sections = num_sections,
};
recomp::overlays::overlays_by_index_t overlays {
.table = overlay_sections_by_index,
.len = ARRLEN(overlay_sections_by_index),
};
recomp::overlays::register_overlays(sections, overlays);
}
+13
View File
@@ -0,0 +1,13 @@
#include "ovl_patches.hpp"
#include "../../RecompiledPatches/patches_bin.h"
#include "../../RecompiledPatches/recomp_overlays.inl"
#include "librecomp/overlays.hpp"
#include "librecomp/game.hpp"
void banjo::register_bk_patches() {
recomp::overlays::register_patches(bk_patches_bin, sizeof(bk_patches_bin), section_table, ARRLEN(section_table));
recomp::overlays::register_base_exports(export_table);
recomp::overlays::register_base_events(event_names);
recomp::overlays::register_manual_patch_symbols(manual_patch_symbols);
}
+419
View File
@@ -0,0 +1,419 @@
#include <memory>
#include <cstring>
#include <variant>
#define HLSL_CPU
#include "hle/rt64_application.h"
#include "rt64_render_hooks.h"
#include "ultramodern/ultramodern.hpp"
#include "ultramodern/config.hpp"
#include "banjo_render.h"
#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;
static uint8_t DMEM[0x1000];
static uint8_t IMEM[0x1000];
struct TexturePackEnableAction {
std::filesystem::path path;
};
struct TexturePackDisableAction {
std::filesystem::path path;
};
using TexturePackAction = std::variant<TexturePackEnableAction, TexturePackDisableAction>;
static moodycamel::ConcurrentQueue<TexturePackAction> texture_pack_action_queue;
unsigned int MI_INTR_REG = 0;
unsigned int DPC_START_REG = 0;
unsigned int DPC_END_REG = 0;
unsigned int DPC_CURRENT_REG = 0;
unsigned int DPC_STATUS_REG = 0;
unsigned int DPC_CLOCK_REG = 0;
unsigned int DPC_BUFBUSY_REG = 0;
unsigned int DPC_PIPEBUSY_REG = 0;
unsigned int DPC_TMEM_REG = 0;
unsigned int VI_STATUS_REG = 0;
unsigned int VI_ORIGIN_REG = 0;
unsigned int VI_WIDTH_REG = 0;
unsigned int VI_INTR_REG = 0;
unsigned int VI_V_CURRENT_LINE_REG = 0;
unsigned int VI_TIMING_REG = 0;
unsigned int VI_V_SYNC_REG = 0;
unsigned int VI_H_SYNC_REG = 0;
unsigned int VI_LEAP_REG = 0;
unsigned int VI_H_START_REG = 0;
unsigned int VI_V_START_REG = 0;
unsigned int VI_V_BURST_REG = 0;
unsigned int VI_X_SCALE_REG = 0;
unsigned int VI_Y_SCALE_REG = 0;
void dummy_check_interrupts() {}
RT64::UserConfiguration::Antialiasing compute_max_supported_aa(RT64::RenderSampleCounts bits) {
if (bits & RT64::RenderSampleCount::Bits::COUNT_2) {
if (bits & RT64::RenderSampleCount::Bits::COUNT_4) {
if (bits & RT64::RenderSampleCount::Bits::COUNT_8) {
return RT64::UserConfiguration::Antialiasing::MSAA8X;
}
return RT64::UserConfiguration::Antialiasing::MSAA4X;
}
return RT64::UserConfiguration::Antialiasing::MSAA2X;
};
return RT64::UserConfiguration::Antialiasing::None;
}
RT64::UserConfiguration::AspectRatio to_rt64(ultramodern::renderer::AspectRatio option) {
switch (option) {
case ultramodern::renderer::AspectRatio::Original:
return RT64::UserConfiguration::AspectRatio::Original;
case ultramodern::renderer::AspectRatio::Expand:
return RT64::UserConfiguration::AspectRatio::Expand;
case ultramodern::renderer::AspectRatio::Manual:
return RT64::UserConfiguration::AspectRatio::Manual;
case ultramodern::renderer::AspectRatio::OptionCount:
return RT64::UserConfiguration::AspectRatio::OptionCount;
}
}
RT64::UserConfiguration::Antialiasing to_rt64(ultramodern::renderer::Antialiasing option) {
switch (option) {
case ultramodern::renderer::Antialiasing::None:
return RT64::UserConfiguration::Antialiasing::None;
case ultramodern::renderer::Antialiasing::MSAA2X:
return RT64::UserConfiguration::Antialiasing::MSAA2X;
case ultramodern::renderer::Antialiasing::MSAA4X:
return RT64::UserConfiguration::Antialiasing::MSAA4X;
case ultramodern::renderer::Antialiasing::MSAA8X:
return RT64::UserConfiguration::Antialiasing::MSAA8X;
case ultramodern::renderer::Antialiasing::OptionCount:
return RT64::UserConfiguration::Antialiasing::OptionCount;
}
}
RT64::UserConfiguration::RefreshRate to_rt64(ultramodern::renderer::RefreshRate option) {
switch (option) {
case ultramodern::renderer::RefreshRate::Original:
return RT64::UserConfiguration::RefreshRate::Original;
case ultramodern::renderer::RefreshRate::Display:
return RT64::UserConfiguration::RefreshRate::Display;
case ultramodern::renderer::RefreshRate::Manual:
return RT64::UserConfiguration::RefreshRate::Manual;
case ultramodern::renderer::RefreshRate::OptionCount:
return RT64::UserConfiguration::RefreshRate::OptionCount;
}
}
RT64::UserConfiguration::InternalColorFormat to_rt64(ultramodern::renderer::HighPrecisionFramebuffer option) {
switch (option) {
case ultramodern::renderer::HighPrecisionFramebuffer::Off:
return RT64::UserConfiguration::InternalColorFormat::Standard;
case ultramodern::renderer::HighPrecisionFramebuffer::On:
return RT64::UserConfiguration::InternalColorFormat::High;
case ultramodern::renderer::HighPrecisionFramebuffer::Auto:
return RT64::UserConfiguration::InternalColorFormat::Automatic;
case ultramodern::renderer::HighPrecisionFramebuffer::OptionCount:
return RT64::UserConfiguration::InternalColorFormat::OptionCount;
}
}
void set_application_user_config(RT64::Application* application, const ultramodern::renderer::GraphicsConfig& config) {
switch (config.res_option) {
default:
case ultramodern::renderer::Resolution::Auto:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::WindowIntegerScale;
application->userConfig.downsampleMultiplier = 1;
break;
case ultramodern::renderer::Resolution::Original:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::Manual;
application->userConfig.resolutionMultiplier = std::max(config.ds_option, 1);
application->userConfig.downsampleMultiplier = std::max(config.ds_option, 1);
break;
case ultramodern::renderer::Resolution::Original2x:
application->userConfig.resolution = RT64::UserConfiguration::Resolution::Manual;
application->userConfig.resolutionMultiplier = 2.0 * std::max(config.ds_option, 1);
application->userConfig.downsampleMultiplier = std::max(config.ds_option, 1);
break;
}
switch (config.hr_option) {
default:
case ultramodern::renderer::HUDRatioMode::Original:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Original;
break;
case ultramodern::renderer::HUDRatioMode::Clamp16x9:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Manual;
application->userConfig.extAspectTarget = 16.0/9.0;
break;
case ultramodern::renderer::HUDRatioMode::Full:
application->userConfig.extAspectRatio = RT64::UserConfiguration::AspectRatio::Expand;
break;
}
application->userConfig.aspectRatio = to_rt64(config.ar_option);
application->userConfig.antialiasing = to_rt64(config.msaa_option);
application->userConfig.refreshRate = to_rt64(config.rr_option);
application->userConfig.refreshRateTarget = config.rr_manual_value;
application->userConfig.internalColorFormat = to_rt64(config.hpfb_option);
}
ultramodern::renderer::SetupResult map_setup_result(RT64::Application::SetupResult rt64_result) {
switch (rt64_result) {
case RT64::Application::SetupResult::Success:
return ultramodern::renderer::SetupResult::Success;
case RT64::Application::SetupResult::DynamicLibrariesNotFound:
return ultramodern::renderer::SetupResult::DynamicLibrariesNotFound;
case RT64::Application::SetupResult::InvalidGraphicsAPI:
return ultramodern::renderer::SetupResult::InvalidGraphicsAPI;
case RT64::Application::SetupResult::GraphicsAPINotFound:
return ultramodern::renderer::SetupResult::GraphicsAPINotFound;
case RT64::Application::SetupResult::GraphicsDeviceNotFound:
return ultramodern::renderer::SetupResult::GraphicsDeviceNotFound;
}
fprintf(stderr, "Unhandled `RT64::Application::SetupResult` ?\n");
assert(false);
std::exit(EXIT_FAILURE);
}
banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool debug) {
static unsigned char dummy_rom_header[0x40];
recompui::set_render_hooks();
// Set up the RT64 application core fields.
RT64::Application::Core appCore{};
#if defined(_WIN32)
appCore.window = window_handle.window;
#elif defined(__linux__) || defined(__ANDROID__)
appCore.window = window_handle;
#elif defined(__APPLE__)
appCore.window.window = window_handle.window;
appCore.window.view = window_handle.view;
#endif
appCore.checkInterrupts = dummy_check_interrupts;
appCore.HEADER = dummy_rom_header;
appCore.RDRAM = rdram;
appCore.DMEM = DMEM;
appCore.IMEM = IMEM;
appCore.MI_INTR_REG = &MI_INTR_REG;
appCore.DPC_START_REG = &DPC_START_REG;
appCore.DPC_END_REG = &DPC_END_REG;
appCore.DPC_CURRENT_REG = &DPC_CURRENT_REG;
appCore.DPC_STATUS_REG = &DPC_STATUS_REG;
appCore.DPC_CLOCK_REG = &DPC_CLOCK_REG;
appCore.DPC_BUFBUSY_REG = &DPC_BUFBUSY_REG;
appCore.DPC_PIPEBUSY_REG = &DPC_PIPEBUSY_REG;
appCore.DPC_TMEM_REG = &DPC_TMEM_REG;
appCore.VI_STATUS_REG = &VI_STATUS_REG;
appCore.VI_ORIGIN_REG = &VI_ORIGIN_REG;
appCore.VI_WIDTH_REG = &VI_WIDTH_REG;
appCore.VI_INTR_REG = &VI_INTR_REG;
appCore.VI_V_CURRENT_LINE_REG = &VI_V_CURRENT_LINE_REG;
appCore.VI_TIMING_REG = &VI_TIMING_REG;
appCore.VI_V_SYNC_REG = &VI_V_SYNC_REG;
appCore.VI_H_SYNC_REG = &VI_H_SYNC_REG;
appCore.VI_LEAP_REG = &VI_LEAP_REG;
appCore.VI_H_START_REG = &VI_H_START_REG;
appCore.VI_V_START_REG = &VI_V_START_REG;
appCore.VI_V_BURST_REG = &VI_V_BURST_REG;
appCore.VI_X_SCALE_REG = &VI_X_SCALE_REG;
appCore.VI_Y_SCALE_REG = &VI_Y_SCALE_REG;
// Set up the RT64 application configuration fields.
RT64::ApplicationConfiguration appConfig;
appConfig.useConfigurationFile = false;
// Create the RT64 application.
app = std::make_unique<RT64::Application>(appCore, appConfig);
// Set initial user config settings based on the current settings.
auto& cur_config = ultramodern::renderer::get_graphics_config();
set_application_user_config(app.get(), cur_config);
app->userConfig.developerMode = debug;
// Force gbi depth branches to prevent LODs from kicking in.
app->enhancementConfig.f3dex.forceBranch = true;
// Scale LODs based on the output resolution.
app->enhancementConfig.textureLOD.scale = true;
// Pick an API if the user has set an override.
switch (cur_config.api_option) {
case ultramodern::renderer::GraphicsApi::D3D12:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::D3D12;
break;
case ultramodern::renderer::GraphicsApi::Vulkan:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Vulkan;
break;
default:
case ultramodern::renderer::GraphicsApi::Auto:
// Don't override if auto is selected.
break;
}
// Set up the RT64 application.
uint32_t thread_id = 0;
#ifdef _WIN32
thread_id = window_handle.thread_id;
#endif
setup_result = map_setup_result(app->setup(thread_id));
if (setup_result != ultramodern::renderer::SetupResult::Success) {
app = nullptr;
return;
}
// Set the application's fullscreen state.
app->setFullScreen(cur_config.wm_option == ultramodern::renderer::WindowMode::Fullscreen);
// Check if the selected device actually supports MSAA sample positions and MSAA for for the formats that will be used
// and downgrade the configuration accordingly.
if (app->device->getCapabilities().sampleLocations) {
RT64::RenderSampleCounts color_sample_counts = app->device->getSampleCountsSupported(RT64::RenderFormat::R8G8B8A8_UNORM);
RT64::RenderSampleCounts depth_sample_counts = app->device->getSampleCountsSupported(RT64::RenderFormat::D32_FLOAT);
RT64::RenderSampleCounts common_sample_counts = color_sample_counts & depth_sample_counts;
device_max_msaa = compute_max_supported_aa(common_sample_counts);
sample_positions_supported = true;
}
else {
device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
sample_positions_supported = false;
}
high_precision_fb_enabled = app->shaderLibrary->usesHDR;
}
banjo::renderer::RT64Context::~RT64Context() = default;
void banjo::renderer::RT64Context::send_dl(const OSTask* task) {
bool packs_disabled = false;
TexturePackAction cur_action;
while (texture_pack_action_queue.try_dequeue(cur_action)) {
std::visit(overloaded{
[&](TexturePackDisableAction& to_disable) {
enabled_texture_packs.erase(to_disable.path);
packs_disabled = true;
},
[&](TexturePackEnableAction& to_enable) {
enabled_texture_packs.insert(to_enable.path);
// Load the pack now if no packs have been disabled.
if (!packs_disabled) {
app->textureCache->loadReplacementDirectory(to_enable.path);
}
}
}, cur_action);
}
// If any packs were disabled, unload all packs and load all the active ones.
if (packs_disabled) {
app->textureCache->clearReplacementDirectories();
for (const std::filesystem::path& cur_pack_path : enabled_texture_packs) {
app->textureCache->loadReplacementDirectory(cur_pack_path);
}
}
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);
}
void banjo::renderer::RT64Context::update_screen(uint32_t vi_origin) {
VI_ORIGIN_REG = vi_origin;
app->updateScreen();
}
void banjo::renderer::RT64Context::shutdown() {
if (app != nullptr) {
app->end();
}
}
bool banjo::renderer::RT64Context::update_config(const ultramodern::renderer::GraphicsConfig& old_config, const ultramodern::renderer::GraphicsConfig& new_config) {
if (old_config == new_config) {
return false;
}
if (new_config.wm_option != old_config.wm_option) {
app->setFullScreen(new_config.wm_option == ultramodern::renderer::WindowMode::Fullscreen);
}
set_application_user_config(app.get(), new_config);
app->updateUserConfig(true);
if (new_config.msaa_option != old_config.msaa_option) {
app->updateMultisampling();
}
return true;
}
void banjo::renderer::RT64Context::enable_instant_present() {
// Enable the present early presentation mode for minimal latency.
app->enhancementConfig.presentation.mode = RT64::EnhancementConfiguration::Presentation::Mode::PresentEarly;
app->updateEnhancementConfig();
}
uint32_t banjo::renderer::RT64Context::get_display_framerate() const {
return app->presentQueue->ext.sharedResources->swapChainRate;
}
float banjo::renderer::RT64Context::get_resolution_scale() const {
constexpr int ReferenceHeight = 240;
switch (app->userConfig.resolution) {
case RT64::UserConfiguration::Resolution::WindowIntegerScale:
if (app->sharedQueueResources->swapChainHeight > 0) {
return std::max(float((app->sharedQueueResources->swapChainHeight + ReferenceHeight - 1) / ReferenceHeight), 1.0f);
}
else {
return 1.0f;
}
case RT64::UserConfiguration::Resolution::Manual:
return float(app->userConfig.resolutionMultiplier);
case RT64::UserConfiguration::Resolution::Original:
default:
return 1.0f;
}
}
RT64::UserConfiguration::Antialiasing banjo::renderer::RT64MaxMSAA() {
return device_max_msaa;
}
std::unique_ptr<ultramodern::renderer::RendererContext> banjo::renderer::create_render_context(uint8_t* rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode) {
return std::make_unique<banjo::renderer::RT64Context>(rdram, window_handle, developer_mode);
}
bool banjo::renderer::RT64SamplePositionsSupported() {
return sample_positions_supported;
}
bool banjo::renderer::RT64HighPrecisionFBEnabled() {
return high_precision_fb_enabled;
}
void banjo::renderer::enable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_root_path});
}
void banjo::renderer::disable_texture_pack(const recomp::mods::ModHandle& mod) {
texture_pack_action_queue.enqueue(TexturePackDisableAction{mod.manifest.mod_root_path});
}
+565
View File
@@ -0,0 +1,565 @@
#include <mutex>
#include <string>
#include <unordered_map>
#include "slot_map.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;
std::vector<Element*> loose_elements;
std::unordered_set<ResourceId> to_update;
Context(Rml::ElementDocument* document) : document(document), root_element(document) {}
};
} // namespace recompui
using context_slotmap = dod::slot_map32<recompui::Context>;
static struct {
std::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;
} 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,
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::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;
}
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();
ContextId ret = create_context_impl(doc);
Element* root = ret.get_root_element();
// Mark the root element as not being a shim, as that's only needed for elements that were parented to Rml ones manually.
root->shim = false;
// TODO move these defaults elsewhere. Copied from the existing rcss.
ret.open();
root->set_width(100.0f, Unit::Percent);
root->set_height(100.0f, Unit::Percent);
root->set_display(Display::Flex);
root->set_opacity(1.0f);
root->set_font_family("LatoLatin");
root->set_font_style(FontStyle::Normal);
root->set_font_weight(400);
float sz = 16.0f;
float spacing = 0.0f;
float sz_add = sz + 4;
root->set_font_size(sz_add, Unit::Dp);
root->set_letter_spacing(sz_add * spacing, Unit::Dp);
root->set_line_height(sz_add, Unit::Dp);
ret.close();
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;
}
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);
}
}
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())->process_event(update_event);
}
}
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) {
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::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;
}
+63
View File
@@ -0,0 +1,63 @@
#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();
void open();
void close();
void process_updates();
static constexpr ContextId null() { return ContextId{ .slot_id = uint32_t(-1) }; }
// TODO
bool takes_input() { return true; }
};
ContextId create_context(const std::filesystem::path& path);
ContextId create_context(Rml::ElementDocument* document);
ContextId create_context();
void destroy_context(ContextId id);
ContextId get_current_context();
ContextId get_context_from_document(Rml::ElementDocument* document);
void destroy_all_contexts();
void register_ui_exports();
} // namespace recompui
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
namespace recompui {
class Style;
class Element;
struct ResourceId {
uint32_t slot_id;
bool operator==(const ResourceId& rhs) const = default;
const Style* operator*() const;
Style* operator*();
const Style* operator->() const { return *(*this); }
Style* operator->() { return *(*this); }
const Element* as_element() const;
Element* as_element();
static constexpr ResourceId null() { return ResourceId{ uint32_t(-1) }; }
};
} // namespace recompui
+92
View File
@@ -0,0 +1,92 @@
#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), "button") {
this->style = style;
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 });
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 });
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 });
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(&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);
break;
case EventType::Enable:
set_style_enabled(disabled_state, !std::get<EventEnable>(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);
}
};
+27
View File
@@ -0,0 +1,27 @@
#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 disabled_style;
Style hover_disabled_style;
std::list<std::function<void()>> pressed_callbacks;
// Element overrides.
virtual void process_event(const Event &e) override;
public:
Button(Element *parent, const std::string &text, ButtonStyle style);
void add_pressed_callback(std::function<void()> callback);
};
} // namespace recompui
+46
View File
@@ -0,0 +1,46 @@
#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)) {
if (draggable) {
set_drag(Drag::Drag);
}
}
void Clickable::process_event(const Event &e) {
switch (e.type) {
case EventType::Click: {
const EventClick &click = std::get<EventClick>(e.variant);
for (const auto &function : pressed_callbacks) {
function(click.x, click.y);
}
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::Drag: {
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);
}
};
+20
View File
@@ -0,0 +1,20 @@
#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;
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
+14
View File
@@ -0,0 +1,14 @@
#include "ui_container.h"
#include <cassert>
namespace recompui {
Container::Container(Element *parent, FlexDirection direction, JustifyContent justify_content) : Element(parent) {
set_display(Display::Flex);
set_flex(1.0f, 1.0f);
set_flex_direction(direction);
set_justify_content(justify_content);
}
};
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Container : public Element {
public:
Container(Element* parent, FlexDirection direction, JustifyContent justify_content);
};
} // namespace recompui
+312
View File
@@ -0,0 +1,312 @@
#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) {
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();
}
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);
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::Mousedown, 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);
}
}
void Element::apply_style(Style *style) {
for (auto it : style->property_map) {
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)) {
process_event(Event::enable_event(!attribute_state));
}
for (auto &child : children) {
child->propagate_disabled(attribute_state);
}
}
}
void Element::ProcessEvent(Rml::Event &event) {
ContextId context = ContextId::null();
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
if (doc != nullptr) {
context = get_context_from_document(doc);
}
// TODO disallow null contexts once the entire UI system has been migrated.
if (context != ContextId::null()) {
context.open();
}
// Events that are processed during any phase.
switch (event.GetId()) {
case Rml::EventId::Mousedown:
process_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f)));
break;
case Rml::EventId::Drag:
process_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:
process_event(Event::hover_event(true));
break;
case Rml::EventId::Mouseout:
process_event(Event::hover_event(false));
break;
case Rml::EventId::Focus:
process_event(Event::focus_event(true));
break;
case Rml::EventId::Blur:
process_event(Event::focus_event(false));
break;
case Rml::EventId::Dragstart:
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start));
break;
case Rml::EventId::Dragend:
process_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End));
break;
case Rml::EventId::Change: {
if (events_enabled & Events(EventType::Text)) {
Rml::Variant *value_variant = base->GetAttribute("value");
if (value_variant != nullptr) {
process_event(Event::text_event(value_variant->Get<std::string>()));
}
}
break;
}
default:
break;
}
}
if (context != ContextId::null()) {
context.close();
}
}
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::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();
}
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;
}
void Element::set_text(std::string_view text) {
base->SetInnerRML(std::string(text));
}
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();
}
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();
}
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);
}
}
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include "ui_style.h"
#include "../core/ui_context.h"
#include <unordered_set>
namespace recompui {
class ContextId;
class Element : public Style, public Rml::EventListener {
friend ContextId create_context(const std::filesystem::path& path);
friend ContextId create_context();
friend class ContextId; // To allow ContextId to call the process_event method directly.
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<Element *> children;
bool shim = false;
bool enabled = true;
bool disabled_attribute = false;
bool disabled_from_parent = false;
void add_child(Element *child);
void register_event_listeners(uint32_t events_enabled);
void apply_style(Style *style);
void apply_styles();
void propagate_disabled(bool disabled);
// 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);
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");
virtual ~Element();
void clear_children();
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);
void set_src(std::string_view src);
void set_style_enabled(std::string_view style_name, bool enabled);
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 queue_update();
};
} // namespace recompui
+11
View File
@@ -0,0 +1,11 @@
#include "ui_image.h"
#include <cassert>
namespace recompui {
Image::Image(Element *parent, std::string_view src) : Element(parent, 0, "img") {
set_src(src);
}
};
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class Image : public Element {
public:
Image(Element *parent, std::string_view src);
};
} // namespace recompui
+43
View File
@@ -0,0 +1,43 @@
#include "ui_label.h"
#include <cassert>
namespace recompui {
Label::Label(Element *parent, LabelStyle label_style) : Element(parent) {
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);
}
};
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class LabelStyle {
Annotation,
Small,
Normal,
Large
};
class Label : public Element {
public:
Label(Element *parent, LabelStyle label_style);
Label(Element *parent, const std::string &text, LabelStyle label_style);
};
} // namespace recompui
+106
View File
@@ -0,0 +1,106 @@
#include "ui_radio.h"
namespace recompui {
// RadioOption
RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "label") {
this->index = index;
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, 255 });
set_border_bottom_width(0.0f);
set_color(Color{ 255, 255, 255, 153 });
set_padding_bottom(8.0f);
set_text_transform(TextTransform::Uppercase);
hover_style.set_color(Color{ 255, 255, 255, 204 });
checked_style.set_color(Color{ 255, 255, 255, 255 });
checked_style.set_border_bottom_width(1.0f);
add_style(&hover_style, { hover_state });
add_style(&checked_style, { checked_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;
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);
}
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart) {
set_gap(24.0f);
set_flex_grow(0.0f);
}
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(std::bind(&Radio::option_selected, this, std::placeholders::_1));
options.emplace_back(option);
// The first option was added, select it.
if (options.size() == 1) {
set_index_internal(0, true, false);
}
}
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);
}
};
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "ui_container.h"
#include "ui_label.h"
namespace recompui {
class RadioOption : public Element {
private:
Style hover_style;
Style checked_style;
std::function<void(uint32_t)> pressed_callback = nullptr;
uint32_t index = 0;
protected:
virtual void process_event(const Event &e) override;
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);
};
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);
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);
};
} // namespace recompui
+27
View File
@@ -0,0 +1,27 @@
#include "ui_scroll_container.h"
#include <cassert>
namespace recompui {
ScrollContainer::ScrollContainer(Element *parent, ScrollDirection direction) : Element(parent) {
set_flex(1.0f, 1.0f, 100.0f);
set_width(100.0f, Unit::Percent);
set_height(100.0f, Unit::Percent);
switch (direction) {
case ScrollDirection::Horizontal:
set_max_width(100.0f, Unit::Percent);
set_overflow_x(Overflow::Auto);
break;
case ScrollDirection::Vertical:
set_max_height(100.0f, Unit::Percent);
set_overflow_y(Overflow::Auto);
break;
default:
assert(false && "Unknown scroll direction.");
break;
}
}
};
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "ui_element.h"
namespace recompui {
enum class ScrollDirection {
Horizontal,
Vertical
};
class ScrollContainer : public Element {
public:
ScrollContainer(Element *parent, ScrollDirection direction);
};
} // namespace recompui
+145
View File
@@ -0,0 +1,145 @@
#include "ui_slider.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(slider_width_dp * ratio);
}
void Slider::update_label_text() {
char text_buffer[32];
int precision = type == SliderType::Double ? 1 : 0;
auto result = std::to_chars(text_buffer, text_buffer + sizeof(text_buffer) - 1, value, std::chars_format::fixed, precision);
if (result.ec == std::errc()) {
if (type == SliderType::Percent) {
*result.ptr = '%';
result.ptr++;
}
value_label->set_text(std::string(text_buffer, result.ptr));
}
}
Slider::Slider(Element *parent, SliderType type) : Element(parent) {
this->type = type;
set_display(Display::Flex);
set_flex(1.0f, 1.0f, 100.0f, Unit::Percent);
set_flex_direction(FlexDirection::Row);
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<Element>(this);
slider_element->set_width(slider_width_dp);
{
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(std::bind(&Slider::bar_clicked, this, std::placeholders::_1, std::placeholders::_2));
bar_element->add_dragged_callback(std::bind(&Slider::bar_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
circle_element = context.create_element<Clickable>(slider_element, true);
circle_element->set_position(Position::Relative);
circle_element->set_width(16.0f);
circle_element->set_height(16.0f);
circle_element->set_margin_top(-8.0f);
circle_element->set_margin_right(-8.0f);
circle_element->set_margin_left(-8.0f);
circle_element->set_background_color(Color{ 204, 204, 204, 255 });
circle_element->set_border_radius(8.0f);
circle_element->add_dragged_callback(std::bind(&Slider::circle_dragged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
circle_element->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);
}
} // namespace recompui
+50
View File
@@ -0,0 +1,50 @@
#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;
Element *slider_element = nullptr;
Clickable *bar_element = nullptr;
Clickable *circle_element = nullptr;
double value = 50.0;
double min_value = 0.0;
double max_value = 100.0;
double step_value = 0.0;
float slider_width_dp = 300.0;
std::vector<std::function<void(double)>> value_changed_callbacks;
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();
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);
};
} // namespace recompui
+549
View File
@@ -0,0 +1,549 @@
#include "ui_style.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;
}
}
void Style::set_property(Rml::PropertyId property_id, const Rml::Property &property) {
property_map[property_id] = property;
}
Style::Style() {
}
Style::~Style() {
}
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:
assert(false && "Unimplemented.");
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;
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));
}
} // namespace recompui
+100
View File
@@ -0,0 +1,100 @@
#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_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 bool is_element() { return false; }
ResourceId get_resource_id() { return resource_id; }
};
} // namespace recompui
+45
View File
@@ -0,0 +1,45 @@
#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) : Element(parent, Events(EventType::Text), "input") {
set_min_width(60.0f);
set_max_width(400.0f);
set_border_color(Color{ 242, 242, 242, 255 });
set_border_bottom_width(1.0f);
set_padding_bottom(6.0f);
}
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);
}
};
+20
View File
@@ -0,0 +1,20 @@
#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;
public:
TextInput(Element *parent);
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
+140
View File
@@ -0,0 +1,140 @@
#include "ui_toggle.h"
#include <cassert>
#include <ultramodern/ultramodern.hpp>
namespace recompui {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
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 });
checked_hover_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_hover_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(&checked_hover_style, { checked_state, hover_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;
set_style_enabled(hover_state, hover_active);
floater->set_style_enabled(hover_state, hover_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);
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);
}
};
+35
View File
@@ -0,0 +1,35 @@
#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 checked_hover_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;
public:
Toggle(Element *parent);
void set_checked(bool checked);
bool is_checked() const;
void add_checked_callback(std::function<void(bool)> callback);
};
} // namespace recompui
+232
View File
@@ -0,0 +1,232 @@
#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 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
};
enum class EventType {
None,
Click,
Focus,
Hover,
Enable,
Drag,
Text,
Update,
Count
};
enum class DragPhase {
None,
Start,
Move,
End
};
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;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, 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;
}
};
enum class Display {
None,
Block,
Inline,
InlineBlock,
FlowRoot,
Flex,
InlineFlex,
Table,
InlineTable,
TableRow,
TableRowGroup,
TableColumn,
TableColumnGroup,
TableCell
};
enum class Position {
Absolute,
Relative
};
enum class JustifyContent {
FlexStart,
FlexEnd,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly
};
enum class FlexDirection {
Row,
Column
};
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
+780
View File
@@ -0,0 +1,780 @@
#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"
using namespace recompui;
constexpr ResourceId root_element_id{ 0xFFFFFFFE };
// Helpers
ContextId get_context(uint8_t* rdram, recomp_context* ctx) {
uint32_t context_id = _arg<0, uint32_t>(rdram, ctx);
return ContextId{ .slot_id = context_id };
}
template <int arg_index>
std::string arg_string(uint8_t* rdram, recomp_context* ctx) {
PTR(char) str = _arg<arg_index, PTR(char)>(rdram, ctx);
// Get the length of the byteswapped string.
size_t len = 0;
while (MEM_B(str, len) != 0x00) {
len++;
}
std::string ret{};
ret.reserve(len + 1);
for (size_t i = 0; i < len; i++) {
ret += (char)MEM_B(str, i);
}
return ret;
}
template <int arg_index>
ResourceId arg_resource_id(uint8_t* rdram, recomp_context* ctx) {
uint32_t slot_id = _arg<arg_index, uint32_t>(rdram, ctx);
return ResourceId{ .slot_id = slot_id };
}
template <int arg_index>
Element* arg_element(uint8_t* rdram, recomp_context* ctx, ContextId ui_context) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
return ui_context.get_root_element();
}
return resource.as_element();
}
template <int arg_index>
Style* arg_style(uint8_t* rdram, recomp_context* ctx) {
ResourceId resource = arg_resource_id<arg_index>(rdram, ctx);
if (resource == ResourceId::null()) {
return nullptr;
}
else if (resource == root_element_id) {
ContextId ui_context = recompui::get_current_context();
return ui_context.get_root_element();
}
return *resource;
}
template <int arg_index>
Color arg_color(uint8_t* rdram, recomp_context* ctx) {
PTR(u8) color_arg = _arg<arg_index, PTR(u8)>(rdram, ctx);
Color ret{};
ret.r = MEM_B(0, color_arg);
ret.g = MEM_B(1, color_arg);
ret.b = MEM_B(2, color_arg);
ret.a = MEM_B(3, color_arg);
return ret;
}
void return_resource(recomp_context* ctx, ResourceId resource) {
_return<uint32_t>(ctx, resource.slot_id);
}
// Contexts
void recompui_create_context(uint8_t* rdram, recomp_context* ctx) {
(void)rdram;
ContextId ui_context = create_context();
_return<uint32_t>(ctx, ui_context.slot_id);
}
void recompui_open_context(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
ui_context.open();
}
void recompui_close_context(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
ui_context.close();
}
void recompui_context_root(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
(void)ui_context;
return_resource(ctx, root_element_id);
}
void recompui_show_context(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
recompui::show_context(ui_context, "");
}
void recompui_hide_context(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
recompui::hide_context(ui_context);
}
// Resources
void recompui_create_style(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Style* ret = ui_context.create_style();
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_element(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
Element* ret = ui_context.create_element<Element>(parent);
return_resource(ctx, ret->get_resource_id());
}
void recompui_create_button(uint8_t* rdram, recomp_context* ctx) {
ContextId ui_context = get_context(rdram, ctx);
Element* parent = arg_element<1>(rdram, ctx, ui_context);
std::string text = arg_string<2>(rdram, ctx);
uint32_t style = _arg<3, uint32_t>(rdram, ctx);
Button* ret = ui_context.create_element<Button>(parent, text, static_cast<ButtonStyle>(style));
return_resource(ctx, ret->get_resource_id());
}
// Position and Layout
void recompui_set_position(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t position = _arg<1, uint32_t>(rdram, ctx);
resource->set_position(static_cast<Position>(position));
}
void recompui_set_left(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float left = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_left(left, static_cast<Unit>(unit));
}
void recompui_set_top(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float top = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_top(top, static_cast<Unit>(unit));
}
void recompui_set_right(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float right = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_right(right, static_cast<Unit>(unit));
}
void recompui_set_bottom(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float bottom = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_bottom(bottom, static_cast<Unit>(unit));
}
// Sizing
void recompui_set_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_width(width, static_cast<Unit>(unit));
}
void recompui_set_width_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_width_auto();
}
void recompui_set_height(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float height = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_height(height, static_cast<Unit>(unit));
}
void recompui_set_height_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_height_auto();
}
void recompui_set_min_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_min_width(width, static_cast<Unit>(unit));
}
void recompui_set_min_height(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float height = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_min_height(height, static_cast<Unit>(unit));
}
void recompui_set_max_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_max_width(width, static_cast<Unit>(unit));
}
void recompui_set_max_height(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float height = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_max_height(height, static_cast<Unit>(unit));
}
// Padding
void recompui_set_padding(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float padding = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_padding(padding, static_cast<Unit>(unit));
}
void recompui_set_padding_left(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float padding = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_padding_left(padding, static_cast<Unit>(unit));
}
void recompui_set_padding_top(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float padding = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_padding_top(padding, static_cast<Unit>(unit));
}
void recompui_set_padding_right(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float padding = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_padding_right(padding, static_cast<Unit>(unit));
}
void recompui_set_padding_bottom(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float padding = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_padding_bottom(padding, static_cast<Unit>(unit));
}
// Margins
void recompui_set_margin(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float margin = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_margin(margin, static_cast<Unit>(unit));
}
void recompui_set_margin_left(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float margin = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_margin_left(margin, static_cast<Unit>(unit));
}
void recompui_set_margin_top(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float margin = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_margin_top(margin, static_cast<Unit>(unit));
}
void recompui_set_margin_right(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float margin = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_margin_right(margin, static_cast<Unit>(unit));
}
void recompui_set_margin_bottom(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float margin = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_margin_bottom(margin, static_cast<Unit>(unit));
}
void recompui_set_margin_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_margin_auto();
}
void recompui_set_margin_left_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_margin_left_auto();
}
void recompui_set_margin_top_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_margin_top_auto();
}
void recompui_set_margin_right_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_margin_right_auto();
}
void recompui_set_margin_bottom_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_margin_bottom_auto();
}
// Borders
void recompui_set_border_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_width(width, static_cast<Unit>(unit));
}
void recompui_set_border_left_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_left_width(width, static_cast<Unit>(unit));
}
void recompui_set_border_top_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_top_width(width, static_cast<Unit>(unit));
}
void recompui_set_border_right_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_right_width(width, static_cast<Unit>(unit));
}
void recompui_set_border_bottom_width(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float width = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_bottom_width(width, static_cast<Unit>(unit));
}
void recompui_set_border_radius(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float radius = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_radius(radius, static_cast<Unit>(unit));
}
void recompui_set_border_top_left_radius(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float radius = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_top_left_radius(radius, static_cast<Unit>(unit));
}
void recompui_set_border_top_right_radius(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float radius = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_top_right_radius(radius, static_cast<Unit>(unit));
}
void recompui_set_border_bottom_left_radius(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float radius = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_bottom_left_radius(radius, static_cast<Unit>(unit));
}
void recompui_set_border_bottom_right_radius(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float radius = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_border_bottom_right_radius(radius, static_cast<Unit>(unit));
}
// Colors
void recompui_set_background_color(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
Color color = arg_color<1>(rdram, ctx);
resource->set_background_color(color);
}
void recompui_set_border_color(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
Color color = arg_color<1>(rdram, ctx);
resource->set_border_color(color);
}
void recompui_set_border_left_color(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
Color color = arg_color<1>(rdram, ctx);
resource->set_border_left_color(color);
}
void recompui_set_border_top_color(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
Color color = arg_color<1>(rdram, ctx);
resource->set_border_top_color(color);
}
void recompui_set_border_right_color(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
Color color = arg_color<1>(rdram, ctx);
resource->set_border_right_color(color);
}
void recompui_set_border_bottom_color(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
Color color = arg_color<1>(rdram, ctx);
resource->set_border_bottom_color(color);
}
void recompui_set_color(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
Color color = arg_color<1>(rdram, ctx);
resource->set_color(color);
}
// Cursor and Display
void recompui_set_cursor(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t cursor = _arg<1, uint32_t>(rdram, ctx);
resource->set_cursor(static_cast<Cursor>(cursor));
}
void recompui_set_opacity(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float opacity = _arg_float_a1(rdram, ctx);
resource->set_opacity(opacity);
}
void recompui_set_display(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t display = _arg<1, uint32_t>(rdram, ctx);
resource->set_display(static_cast<Display>(display));
}
// Flexbox
void recompui_set_justify_content(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t justify_content = _arg<1, uint32_t>(rdram, ctx);
resource->set_justify_content(static_cast<JustifyContent>(justify_content));
}
void recompui_set_flex_grow(uint8_t* rdram, recomp_context* ctx) { // float grow
Style* resource = arg_style<0>(rdram, ctx);
float grow = _arg_float_a1(rdram, ctx);
resource->set_flex_grow(grow);
}
void recompui_set_flex_shrink(uint8_t* rdram, recomp_context* ctx) { // float shrink
Style* resource = arg_style<0>(rdram, ctx);
float shrink = _arg_float_a1(rdram, ctx);
resource->set_flex_shrink(shrink);
}
void recompui_set_flex_basis_auto(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
resource->set_flex_basis_auto();
}
void recompui_set_flex_basis(uint8_t* rdram, recomp_context* ctx) { // float basis, Unit unit = Unit::Percent
Style* resource = arg_style<0>(rdram, ctx);
float basis = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_flex_basis(basis, static_cast<Unit>(unit));
}
void recompui_set_flex_direction(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t direction = _arg<1, uint32_t>(rdram, ctx);
resource->set_flex_direction(static_cast<FlexDirection>(direction));
}
void recompui_set_align_items(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t align_items = _arg<1, uint32_t>(rdram, ctx);
resource->set_align_items(static_cast<AlignItems>(align_items));
}
// Overflow
void recompui_set_overflow(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t overflow = _arg<1, uint32_t>(rdram, ctx);
resource->set_overflow(static_cast<Overflow>(overflow));
}
void recompui_set_overflow_x(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t overflow = _arg<1, uint32_t>(rdram, ctx);
resource->set_overflow_x(static_cast<Overflow>(overflow));
}
void recompui_set_overflow_y(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t overflow = _arg<1, uint32_t>(rdram, ctx);
resource->set_overflow_y(static_cast<Overflow>(overflow));
}
// Text and Fonts
void recompui_set_font_size(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float size = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_font_size(size, static_cast<Unit>(unit));
}
void recompui_set_letter_spacing(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float spacing = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_letter_spacing(spacing, static_cast<Unit>(unit));
}
void recompui_set_line_height(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float height = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_line_height(height, static_cast<Unit>(unit));
}
void recompui_set_font_style(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t style = _arg<1, uint32_t>(rdram, ctx);
resource->set_font_style(static_cast<FontStyle>(style));
}
void recompui_set_font_weight(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
int32_t weight = _arg<1, int32_t>(rdram, ctx);
resource->set_font_weight(weight);
}
void recompui_set_text_align(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t text_align = _arg<1, uint32_t>(rdram, ctx);
resource->set_text_align(static_cast<TextAlign>(text_align));
}
// Gaps
void recompui_set_gap(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float size = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_gap(size, static_cast<Unit>(unit));
}
void recompui_set_row_gap(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float size = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_row_gap(size, static_cast<Unit>(unit));
}
void recompui_set_column_gap(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
float size = _arg_float_a1(rdram, ctx);
uint32_t unit = _arg<2, uint32_t>(rdram, ctx);
resource->set_column_gap(size, static_cast<Unit>(unit));
}
// Drag and Focus
void recompui_set_drag(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t drag = _arg<1, uint32_t>(rdram, ctx);
resource->set_drag(static_cast<Drag>(drag));
}
void recompui_set_tab_index(uint8_t* rdram, recomp_context* ctx) {
Style* resource = arg_style<0>(rdram, ctx);
uint32_t tab_index = _arg<1, uint32_t>(rdram, ctx);
resource->set_tab_index(static_cast<TabIndex>(tab_index));
}
#define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name)
void recompui::register_ui_exports() {
REGISTER_FUNC(recompui_create_context);
REGISTER_FUNC(recompui_open_context);
REGISTER_FUNC(recompui_close_context);
REGISTER_FUNC(recompui_context_root);
REGISTER_FUNC(recompui_show_context);
REGISTER_FUNC(recompui_hide_context);
REGISTER_FUNC(recompui_create_style);
REGISTER_FUNC(recompui_create_element);
REGISTER_FUNC(recompui_create_button);
REGISTER_FUNC(recompui_set_position);
REGISTER_FUNC(recompui_set_left);
REGISTER_FUNC(recompui_set_top);
REGISTER_FUNC(recompui_set_right);
REGISTER_FUNC(recompui_set_bottom);
REGISTER_FUNC(recompui_set_width);
REGISTER_FUNC(recompui_set_width_auto);
REGISTER_FUNC(recompui_set_height);
REGISTER_FUNC(recompui_set_height_auto);
REGISTER_FUNC(recompui_set_min_width);
REGISTER_FUNC(recompui_set_min_height);
REGISTER_FUNC(recompui_set_max_width);
REGISTER_FUNC(recompui_set_max_height);
REGISTER_FUNC(recompui_set_padding);
REGISTER_FUNC(recompui_set_padding_left);
REGISTER_FUNC(recompui_set_padding_top);
REGISTER_FUNC(recompui_set_padding_right);
REGISTER_FUNC(recompui_set_padding_bottom);
REGISTER_FUNC(recompui_set_margin);
REGISTER_FUNC(recompui_set_margin_left);
REGISTER_FUNC(recompui_set_margin_top);
REGISTER_FUNC(recompui_set_margin_right);
REGISTER_FUNC(recompui_set_margin_bottom);
REGISTER_FUNC(recompui_set_margin_auto);
REGISTER_FUNC(recompui_set_margin_left_auto);
REGISTER_FUNC(recompui_set_margin_top_auto);
REGISTER_FUNC(recompui_set_margin_right_auto);
REGISTER_FUNC(recompui_set_margin_bottom_auto);
REGISTER_FUNC(recompui_set_border_width);
REGISTER_FUNC(recompui_set_border_left_width);
REGISTER_FUNC(recompui_set_border_top_width);
REGISTER_FUNC(recompui_set_border_right_width);
REGISTER_FUNC(recompui_set_border_bottom_width);
REGISTER_FUNC(recompui_set_border_radius);
REGISTER_FUNC(recompui_set_border_top_left_radius);
REGISTER_FUNC(recompui_set_border_top_right_radius);
REGISTER_FUNC(recompui_set_border_bottom_left_radius);
REGISTER_FUNC(recompui_set_border_bottom_right_radius);
REGISTER_FUNC(recompui_set_background_color);
REGISTER_FUNC(recompui_set_border_color);
REGISTER_FUNC(recompui_set_border_left_color);
REGISTER_FUNC(recompui_set_border_top_color);
REGISTER_FUNC(recompui_set_border_right_color);
REGISTER_FUNC(recompui_set_border_bottom_color);
REGISTER_FUNC(recompui_set_color);
REGISTER_FUNC(recompui_set_cursor);
REGISTER_FUNC(recompui_set_opacity);
REGISTER_FUNC(recompui_set_display);
REGISTER_FUNC(recompui_set_justify_content);
REGISTER_FUNC(recompui_set_flex_grow);
REGISTER_FUNC(recompui_set_flex_shrink);
REGISTER_FUNC(recompui_set_flex_basis_auto);
REGISTER_FUNC(recompui_set_flex_basis);
REGISTER_FUNC(recompui_set_flex_direction);
REGISTER_FUNC(recompui_set_align_items);
REGISTER_FUNC(recompui_set_overflow);
REGISTER_FUNC(recompui_set_overflow_x);
REGISTER_FUNC(recompui_set_overflow_y);
REGISTER_FUNC(recompui_set_font_size);
REGISTER_FUNC(recompui_set_letter_spacing);
REGISTER_FUNC(recompui_set_line_height);
REGISTER_FUNC(recompui_set_font_style);
REGISTER_FUNC(recompui_set_font_weight);
REGISTER_FUNC(recompui_set_text_align);
REGISTER_FUNC(recompui_set_gap);
REGISTER_FUNC(recompui_set_row_gap);
REGISTER_FUNC(recompui_set_column_gap);
REGISTER_FUNC(recompui_set_drag);
REGISTER_FUNC(recompui_set_tab_index);
}
+172
View File
@@ -0,0 +1,172 @@
#include "RmlUi/Core.h"
#include "RmlUi/../../Source/Core/PropertyParserColour.h"
#include "recomp_ui.h"
#include <string.h>
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);
}
PropertyParserColorHack::~PropertyParserColorHack() {}
bool PropertyParserColorHack::ParseValue(Rml::Property& property, const Rml::String& value, const Rml::ParameterMap& /*parameters*/) const {
if (value.empty())
return false;
Rml::Colourb colour;
// 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;
// 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;
}
// 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);
size_t find = value.find('(');
if (find == Rml::String::npos)
return false;
size_t begin_values = find + 1;
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;
values.push_back("255");
}
// 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());
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;
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;
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));
}
ColourMap PropertyParserColorHack::html_colours{};
}
+914
View File
@@ -0,0 +1,914 @@
#include "recomp_ui.h"
#include "recomp_input.h"
#include "banjo_sound.h"
#include "banjo_config.h"
#include "banjo_debug.h"
#include "banjo_render.h"
#include "promptfont.h"
#include "ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
#include "core/ui_context.h"
ultramodern::renderer::GraphicsConfig new_options;
Rml::DataModelHandle nav_help_model_handle;
Rml::DataModelHandle general_model_handle;
Rml::DataModelHandle controls_model_handle;
Rml::DataModelHandle graphics_model_handle;
Rml::DataModelHandle sound_options_model_handle;
// True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
bool configuring_controller = false;
template <typename T>
void get_option(const T& input, Rml::Variant& output) {
std::string value = "";
to_json(value, input);
if (value.empty()) {
throw std::runtime_error("Invalid value :" + std::to_string(int(input)));
}
output = value;
}
template <typename T>
void set_option(T& output, const Rml::Variant& input) {
T value = T::OptionCount;
from_json(input.Get<std::string>(), value);
if (value == T::OptionCount) {
throw std::runtime_error("Invalid value :" + input.Get<std::string>());
}
output = value;
}
template <typename T>
void bind_option(Rml::DataModelConstructor& constructor, const std::string& name, T* option) {
constructor.BindFunc(name,
[option](Rml::Variant& out) { get_option(*option, out); },
[option](const Rml::Variant& in) {
set_option(*option, in);
graphics_model_handle.DirtyVariable("options_changed");
graphics_model_handle.DirtyVariable("ds_info");
}
);
};
template <typename T>
void bind_atomic(Rml::DataModelConstructor& constructor, Rml::DataModelHandle handle, const char* name, std::atomic<T>* atomic_val) {
constructor.BindFunc(name,
[atomic_val](Rml::Variant& out) {
out = atomic_val->load();
},
[atomic_val, handle, name](const Rml::Variant& in) mutable {
atomic_val->store(in.Get<T>());
handle.DirtyVariable(name);
}
);
}
static int scanned_binding_index = -1;
static int scanned_input_index = -1;
static int focused_input_index = -1;
static int focused_config_option_index = -1;
static bool msaa2x_supported = false;
static bool msaa4x_supported = false;
static bool msaa8x_supported = false;
static bool sample_positions_supported = false;
static bool cont_active = true;
static recomp::InputDevice cur_device = recomp::InputDevice::Controller;
int recomp::get_scanned_input_index() {
return scanned_input_index;
}
void recomp::finish_scanning_input(recomp::InputField scanned_field) {
recomp::set_input_binding(static_cast<recomp::GameInput>(scanned_input_index), scanned_binding_index, cur_device, scanned_field);
scanned_input_index = -1;
scanned_binding_index = -1;
controls_model_handle.DirtyVariable("inputs");
controls_model_handle.DirtyVariable("active_binding_input");
controls_model_handle.DirtyVariable("active_binding_slot");
nav_help_model_handle.DirtyVariable("nav_help__accept");
nav_help_model_handle.DirtyVariable("nav_help__exit");
graphics_model_handle.DirtyVariable("gfx_help__apply");
}
void recomp::cancel_scanning_input() {
recomp::stop_scanning_input();
scanned_input_index = -1;
scanned_binding_index = -1;
controls_model_handle.DirtyVariable("inputs");
controls_model_handle.DirtyVariable("active_binding_input");
controls_model_handle.DirtyVariable("active_binding_slot");
nav_help_model_handle.DirtyVariable("nav_help__accept");
nav_help_model_handle.DirtyVariable("nav_help__exit");
graphics_model_handle.DirtyVariable("gfx_help__apply");
}
void recomp::config_menu_set_cont_or_kb(bool cont_interacted) {
if (cont_active != cont_interacted) {
cont_active = cont_interacted;
if (nav_help_model_handle) {
nav_help_model_handle.DirtyVariable("nav_help__navigate");
nav_help_model_handle.DirtyVariable("nav_help__accept");
nav_help_model_handle.DirtyVariable("nav_help__exit");
}
if (graphics_model_handle) {
graphics_model_handle.DirtyVariable("gfx_help__apply");
}
}
}
void close_config_menu_impl() {
banjo::save_config();
recompui::ContextId config_context = recompui::get_config_context_id();
recompui::ContextId sub_menu_context = recompui::get_config_sub_menu_context_id();
if (recompui::is_context_shown(sub_menu_context)) {
recompui::hide_context(sub_menu_context);
}
else {
recompui::hide_context(config_context);
}
if (!ultramodern::is_game_started()) {
recompui::show_context(recompui::get_launcher_context_id(), "");
}
}
// TODO: Remove once RT64 gets native fullscreen support on Linux
#if defined(__linux__)
extern SDL_Window* window;
#endif
void apply_graphics_config(void) {
ultramodern::renderer::set_graphics_config(new_options);
#if defined(__linux__) // TODO: Remove once RT64 gets native fullscreen support on Linux
if (new_options.wm_option == ultramodern::renderer::WindowMode::Fullscreen) {
SDL_SetWindowFullscreen(window,SDL_WINDOW_FULLSCREEN_DESKTOP);
} else {
SDL_SetWindowFullscreen(window,0);
}
#endif
}
void close_config_menu() {
if (ultramodern::renderer::get_graphics_config() != new_options) {
recompui::open_prompt(
"Graphics options have changed",
"Would you like to apply or discard the changes?",
"Apply",
"Discard",
[]() {
apply_graphics_config();
graphics_model_handle.DirtyAllVariables();
close_config_menu_impl();
},
[]() {
new_options = ultramodern::renderer::get_graphics_config();
graphics_model_handle.DirtyAllVariables();
close_config_menu_impl();
},
recompui::ButtonVariant::Success,
recompui::ButtonVariant::Error,
true,
"config__close-menu-button"
);
return;
}
close_config_menu_impl();
}
void banjo::open_quit_game_prompt() {
recompui::open_prompt(
"Are you sure you want to quit?",
"Any progress since your last save will be lost.",
"Quit",
"Cancel",
[]() {
ultramodern::quit();
},
[]() {},
recompui::ButtonVariant::Error,
recompui::ButtonVariant::Tertiary,
true,
"config__quit-game-button"
);
}
// These defaults values don't matter, as the config file handling overrides them.
struct ControlOptionsContext {
int rumble_strength; // 0 to 100
int gyro_sensitivity; // 0 to 100
int mouse_sensitivity; // 0 to 100
int joystick_deadzone; // 0 to 100
recomp::BackgroundInputMode background_input_mode;
banjo::CameraInvertMode camera_invert_mode;
banjo::AnalogCamMode analog_cam_mode;
banjo::CameraInvertMode analog_camera_invert_mode;
};
ControlOptionsContext control_options_context;
int recomp::get_rumble_strength() {
return control_options_context.rumble_strength;
}
void recomp::set_rumble_strength(int strength) {
control_options_context.rumble_strength = strength;
if (general_model_handle) {
general_model_handle.DirtyVariable("rumble_strength");
}
}
int recomp::get_gyro_sensitivity() {
return control_options_context.gyro_sensitivity;
}
int recomp::get_mouse_sensitivity() {
return control_options_context.mouse_sensitivity;
}
int recomp::get_joystick_deadzone() {
return control_options_context.joystick_deadzone;
}
void recomp::set_gyro_sensitivity(int sensitivity) {
control_options_context.gyro_sensitivity = sensitivity;
if (general_model_handle) {
general_model_handle.DirtyVariable("gyro_sensitivity");
}
}
void recomp::set_mouse_sensitivity(int sensitivity) {
control_options_context.mouse_sensitivity = sensitivity;
if (general_model_handle) {
general_model_handle.DirtyVariable("mouse_sensitivity");
}
}
void recomp::set_joystick_deadzone(int deadzone) {
control_options_context.joystick_deadzone = deadzone;
if (general_model_handle) {
general_model_handle.DirtyVariable("joystick_deadzone");
}
}
recomp::BackgroundInputMode recomp::get_background_input_mode() {
return control_options_context.background_input_mode;
}
void recomp::set_background_input_mode(recomp::BackgroundInputMode mode) {
control_options_context.background_input_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("background_input_mode");
}
SDL_SetHint(
SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
mode == recomp::BackgroundInputMode::On
? "1"
: "0"
);
}
banjo::CameraInvertMode banjo::get_camera_invert_mode() {
return control_options_context.camera_invert_mode;
}
void banjo::set_camera_invert_mode(banjo::CameraInvertMode mode) {
control_options_context.camera_invert_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("camera_invert_mode");
}
}
banjo::AnalogCamMode banjo::get_analog_cam_mode() {
return control_options_context.analog_cam_mode;
}
void banjo::set_analog_cam_mode(banjo::AnalogCamMode mode) {
control_options_context.analog_cam_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("analog_cam_mode");
}
}
banjo::CameraInvertMode banjo::get_analog_camera_invert_mode() {
return control_options_context.analog_camera_invert_mode;
}
void banjo::set_analog_camera_invert_mode(banjo::CameraInvertMode mode) {
control_options_context.analog_camera_invert_mode = mode;
if (general_model_handle) {
general_model_handle.DirtyVariable("analog_camera_invert_mode");
}
}
struct SoundOptionsContext {
std::atomic<int> main_volume; // Option to control the volume of all sound
std::atomic<int> bgm_volume;
std::atomic<int> low_health_beeps_enabled; // RmlUi doesn't seem to like "true"/"false" strings for setting variants so an int is used here instead.
void reset() {
bgm_volume = 100;
main_volume = 100;
low_health_beeps_enabled = (int)true;
}
SoundOptionsContext() {
reset();
}
};
SoundOptionsContext sound_options_context;
void banjo::reset_sound_settings() {
sound_options_context.reset();
if (sound_options_model_handle) {
sound_options_model_handle.DirtyAllVariables();
}
}
void banjo::set_main_volume(int volume) {
sound_options_context.main_volume.store(volume);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("main_volume");
}
}
int banjo::get_main_volume() {
return sound_options_context.main_volume.load();
}
void banjo::set_bgm_volume(int volume) {
sound_options_context.bgm_volume.store(volume);
if (sound_options_model_handle) {
sound_options_model_handle.DirtyVariable("bgm_volume");
}
}
int banjo::get_bgm_volume() {
return sound_options_context.bgm_volume.load();
}
struct DebugContext {
Rml::DataModelHandle model_handle;
bool debug_enabled = false;
DebugContext() {
}
};
DebugContext debug_context;
recompui::ContextId config_context;
recompui::ContextId recompui::get_config_context_id() {
return config_context;
}
class ConfigMenu : public recompui::MenuController {
public:
ConfigMenu() {
}
~ConfigMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
config_context = recompui::create_context("assets/config_menu.rml");
Rml::ElementDocument* ret = config_context.get_document();
return ret;
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "apply_options",
[](const std::string& param, Rml::Event& event) {
graphics_model_handle.DirtyVariable("options_changed");
apply_graphics_config();
});
recompui::register_event(listener, "config_keydown",
[](const std::string& param, Rml::Event& event) {
if (!recompui::is_prompt_open() && event.GetId() == Rml::EventId::Keydown) {
auto key = event.GetParameter<Rml::Input::KeyIdentifier>("key_identifier", Rml::Input::KeyIdentifier::KI_UNKNOWN);
switch (key) {
case Rml::Input::KeyIdentifier::KI_ESCAPE:
close_config_menu();
break;
case Rml::Input::KeyIdentifier::KI_F:
graphics_model_handle.DirtyVariable("options_changed");
apply_graphics_config();
break;
}
}
});
// This needs to be separate from `close_config_menu` so it ensures that the event is only on the target
recompui::register_event(listener, "close_config_menu_backdrop",
[](const std::string& param, Rml::Event& event) {
if (event.GetPhase() == Rml::EventPhase::Target) {
close_config_menu();
}
});
recompui::register_event(listener, "close_config_menu",
[](const std::string& param, Rml::Event& event) {
close_config_menu();
});
recompui::register_event(listener, "open_quit_game_prompt",
[](const std::string& param, Rml::Event& event) {
banjo::open_quit_game_prompt();
});
recompui::register_event(listener, "toggle_input_device",
[](const std::string& param, Rml::Event& event) {
cur_device = cur_device == recomp::InputDevice::Controller
? recomp::InputDevice::Keyboard
: recomp::InputDevice::Controller;
controls_model_handle.DirtyVariable("input_device_is_keyboard");
controls_model_handle.DirtyVariable("inputs");
});
}
void bind_config_list_events(Rml::DataModelConstructor &constructor) {
constructor.BindEventCallback("set_cur_config_index",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
int option_index = inputs.at(0).Get<size_t>();
// watch for mouseout being overzealous during event bubbling, only clear if the event's attached element matches the current
if (option_index == -1 && event.GetType() == "mouseout" && event.GetCurrentElement() != event.GetTargetElement()) {
return;
}
focused_config_option_index = option_index;
model_handle.DirtyVariable("cur_config_index");
});
constructor.Bind("cur_config_index", &focused_config_option_index);
}
void make_graphics_bindings(Rml::Context* context) {
Rml::DataModelConstructor constructor = context->CreateDataModel("graphics_model");
if (!constructor) {
throw std::runtime_error("Failed to make RmlUi data model for the graphics config menu");
}
ultramodern::sleep_milliseconds(50);
new_options = ultramodern::renderer::get_graphics_config();
bind_config_list_events(constructor);
constructor.BindFunc("res_option",
[](Rml::Variant& out) { get_option(new_options.res_option, out); },
[](const Rml::Variant& in) {
set_option(new_options.res_option, in);
graphics_model_handle.DirtyVariable("options_changed");
graphics_model_handle.DirtyVariable("ds_info");
graphics_model_handle.DirtyVariable("ds_option");
}
);
bind_option(constructor, "wm_option", &new_options.wm_option);
bind_option(constructor, "ar_option", &new_options.ar_option);
bind_option(constructor, "hr_option", &new_options.hr_option);
bind_option(constructor, "msaa_option", &new_options.msaa_option);
bind_option(constructor, "rr_option", &new_options.rr_option);
constructor.BindFunc("rr_manual_value",
[](Rml::Variant& out) {
out = new_options.rr_manual_value;
},
[](const Rml::Variant& in) {
new_options.rr_manual_value = in.Get<int>();
graphics_model_handle.DirtyVariable("options_changed");
});
constructor.BindFunc("ds_option",
[](Rml::Variant& out) {
if (new_options.res_option == ultramodern::renderer::Resolution::Auto) {
out = 1;
} else {
out = new_options.ds_option;
}
},
[](const Rml::Variant& in) {
new_options.ds_option = in.Get<int>();
graphics_model_handle.DirtyVariable("options_changed");
graphics_model_handle.DirtyVariable("ds_info");
});
constructor.BindFunc("display_refresh_rate",
[](Rml::Variant& out) {
out = ultramodern::get_display_refresh_rate();
});
constructor.BindFunc("options_changed",
[](Rml::Variant& out) {
out = (ultramodern::renderer::get_graphics_config() != new_options);
});
constructor.BindFunc("ds_info",
[](Rml::Variant& out) {
switch (new_options.res_option) {
default:
case ultramodern::renderer::Resolution::Auto:
out = "Downsampling is not available at auto resolution";
return;
case ultramodern::renderer::Resolution::Original:
if (new_options.ds_option == 2) {
out = "Rendered in 480p and scaled to 240p";
} else if (new_options.ds_option == 4) {
out = "Rendered in 960p and scaled to 240p";
}
return;
case ultramodern::renderer::Resolution::Original2x:
if (new_options.ds_option == 2) {
out = "Rendered in 960p and scaled to 480p";
} else if (new_options.ds_option == 4) {
out = "Rendered in 4K and scaled to 480p";
}
return;
}
out = "";
});
constructor.BindFunc("gfx_help__apply", [](Rml::Variant& out) {
if (cont_active) {
out = \
(recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller).to_string() != "" ?
" " + recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller).to_string() :
""
) + \
(recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller).to_string() != "" ?
" " + recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller).to_string() :
""
);
} else {
out = " " PF_KEYBOARD_F;
}
});
constructor.Bind("msaa2x_supported", &msaa2x_supported);
constructor.Bind("msaa4x_supported", &msaa4x_supported);
constructor.Bind("msaa8x_supported", &msaa8x_supported);
constructor.Bind("sample_positions_supported", &sample_positions_supported);
graphics_model_handle = constructor.GetModelHandle();
}
void make_controls_bindings(Rml::Context* context) {
Rml::DataModelConstructor constructor = context->CreateDataModel("controls_model");
if (!constructor) {
throw std::runtime_error("Failed to make RmlUi data model for the controls config menu");
}
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } );
constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } );
constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) {
return Rml::Variant{recomp::get_input_name(static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>()))};
});
constructor.RegisterTransformFunc("get_input_enum_name", [](const Rml::VariantList& inputs) {
return Rml::Variant{recomp::get_input_enum_name(static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>()))};
});
constructor.BindEventCallback("set_input_binding",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
scanned_input_index = inputs.at(0).Get<size_t>();
scanned_binding_index = inputs.at(1).Get<size_t>();
recomp::start_scanning_input(cur_device);
model_handle.DirtyVariable("active_binding_input");
model_handle.DirtyVariable("active_binding_slot");
});
constructor.BindEventCallback("reset_input_bindings_to_defaults",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
if (cur_device == recomp::InputDevice::Controller) {
banjo::reset_cont_input_bindings();
} else {
banjo::reset_kb_input_bindings();
}
model_handle.DirtyAllVariables();
nav_help_model_handle.DirtyVariable("nav_help__accept");
nav_help_model_handle.DirtyVariable("nav_help__exit");
graphics_model_handle.DirtyVariable("gfx_help__apply");
});
constructor.BindEventCallback("clear_input_bindings",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
recomp::GameInput input = static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>());
for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) {
recomp::set_input_binding(input, binding_index, cur_device, recomp::InputField{});
}
model_handle.DirtyVariable("inputs");
graphics_model_handle.DirtyVariable("gfx_help__apply");
});
constructor.BindEventCallback("reset_single_input_binding_to_default",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
recomp::GameInput input = static_cast<recomp::GameInput>(inputs.at(0).Get<size_t>());
banjo::reset_single_input_binding(cur_device, input);
model_handle.DirtyVariable("inputs");
nav_help_model_handle.DirtyVariable("nav_help__accept");
nav_help_model_handle.DirtyVariable("nav_help__exit");
});
constructor.BindEventCallback("set_input_row_focus",
[](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) {
int input_index = inputs.at(0).Get<size_t>();
// watch for mouseout being overzealous during event bubbling, only clear if the event's attached element matches the current
if (input_index == -1 && event.GetType() == "mouseout" && event.GetCurrentElement() != event.GetTargetElement()) {
return;
}
focused_input_index = input_index;
model_handle.DirtyVariable("cur_input_row");
});
// Rml variable definition for an individual InputField.
struct InputFieldVariableDefinition : public Rml::VariableDefinition {
InputFieldVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Scalar) {}
virtual bool Get(void* ptr, Rml::Variant& variant) override { variant = reinterpret_cast<recomp::InputField*>(ptr)->to_string(); return true; }
virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; }
};
// Static instance of the InputField variable definition to have a pointer to return to RmlUi.
static InputFieldVariableDefinition input_field_definition_instance{};
// Rml variable definition for an array of InputField values (e.g. all the bindings for a single input).
struct BindingContainerVariableDefinition : public Rml::VariableDefinition {
BindingContainerVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Array) {}
virtual bool Get(void* ptr, Rml::Variant& variant) override { return false; }
virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; }
virtual int Size(void* ptr) override { return recomp::bindings_per_input; }
virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override {
recomp::GameInput input = static_cast<recomp::GameInput>((uintptr_t)ptr);
return Rml::DataVariable{&input_field_definition_instance, &recomp::get_input_binding(input, address.index, cur_device)};
}
};
// Static instance of the InputField array variable definition to have a fixed pointer to return to RmlUi.
static BindingContainerVariableDefinition binding_container_var_instance{};
// Rml variable definition for an array of an array of InputField values (e.g. all the bindings for all inputs).
struct BindingArrayContainerVariableDefinition : public Rml::VariableDefinition {
BindingArrayContainerVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Array) {}
virtual bool Get(void* ptr, Rml::Variant& variant) override { return false; }
virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; }
virtual int Size(void* ptr) override { return recomp::get_num_inputs(); }
virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override {
// Encode the input index as the pointer to avoid needing to do any allocations.
return Rml::DataVariable(&binding_container_var_instance, (void*)(uintptr_t)address.index);
}
};
// Static instance of the BindingArrayContainerVariableDefinition variable definition to have a fixed pointer to return to RmlUi.
static BindingArrayContainerVariableDefinition binding_array_var_instance{};
struct InputContainerVariableDefinition : public Rml::VariableDefinition {
InputContainerVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Struct) {}
virtual bool Get(void* ptr, Rml::Variant& variant) override { return true; }
virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; }
virtual int Size(void* ptr) override { return recomp::get_num_inputs(); }
virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override {
if (address.name == "array") {
return Rml::DataVariable(&binding_array_var_instance, nullptr);
}
else {
recomp::GameInput input = recomp::get_input_from_enum_name(address.name);
if (input != recomp::GameInput::COUNT) {
return Rml::DataVariable(&binding_container_var_instance, (void*)(uintptr_t)input);
}
}
return Rml::DataVariable{};
}
};
// Dummy type to associate with the variable definition.
struct InputContainer {};
constructor.RegisterCustomDataVariableDefinition<InputContainer>(Rml::MakeUnique<InputContainerVariableDefinition>());
// Dummy instance of the dummy type to bind to the variable.
static InputContainer dummy_container;
constructor.Bind("inputs", &dummy_container);
constructor.BindFunc("cur_input_row", [](Rml::Variant& out) {
if (focused_input_index == -1) {
out = "NONE";
}
else {
out = recomp::get_input_enum_name(static_cast<recomp::GameInput>(focused_input_index));
}
});
constructor.BindFunc("active_binding_input", [](Rml::Variant& out) {
if (scanned_input_index == -1) {
out = "NONE";
}
else {
out = recomp::get_input_enum_name(static_cast<recomp::GameInput>(scanned_input_index));
}
});
constructor.Bind<int>("active_binding_slot", &scanned_binding_index);
controls_model_handle = constructor.GetModelHandle();
}
void make_nav_help_bindings(Rml::Context* context) {
Rml::DataModelConstructor constructor = context->CreateDataModel("nav_help_model");
if (!constructor) {
throw std::runtime_error("Failed to make RmlUi data model for nav help");
}
constructor.BindFunc("nav_help__navigate", [](Rml::Variant& out) {
if (cont_active) {
out = PF_DPAD;
} else {
out = PF_KEYBOARD_ARROWS PF_KEYBOARD_TAB;
}
});
constructor.BindFunc("nav_help__accept", [](Rml::Variant& out) {
if (cont_active) {
out = \
recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller).to_string() + \
recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller).to_string();
} else {
out = PF_KEYBOARD_ENTER;
}
});
constructor.BindFunc("nav_help__exit", [](Rml::Variant& out) {
if (cont_active) {
out = \
recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller).to_string() + \
recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller).to_string();
} else {
out = PF_KEYBOARD_ESCAPE;
}
});
nav_help_model_handle = constructor.GetModelHandle();
}
void make_general_bindings(Rml::Context* context) {
Rml::DataModelConstructor constructor = context->CreateDataModel("general_model");
if (!constructor) {
throw std::runtime_error("Failed to make RmlUi data model for the control options menu");
}
bind_config_list_events(constructor);
constructor.Bind("rumble_strength", &control_options_context.rumble_strength);
constructor.Bind("gyro_sensitivity", &control_options_context.gyro_sensitivity);
constructor.Bind("mouse_sensitivity", &control_options_context.mouse_sensitivity);
constructor.Bind("joystick_deadzone", &control_options_context.joystick_deadzone);
bind_option(constructor, "background_input_mode", &control_options_context.background_input_mode);
bind_option(constructor, "camera_invert_mode", &control_options_context.camera_invert_mode);
bind_option(constructor, "analog_cam_mode", &control_options_context.analog_cam_mode);
bind_option(constructor, "analog_camera_invert_mode", &control_options_context.analog_camera_invert_mode);
general_model_handle = constructor.GetModelHandle();
}
void make_sound_options_bindings(Rml::Context* context) {
Rml::DataModelConstructor constructor = context->CreateDataModel("sound_options_model");
if (!constructor) {
throw std::runtime_error("Failed to make RmlUi data model for the sound options menu");
}
bind_config_list_events(constructor);
sound_options_model_handle = constructor.GetModelHandle();
bind_atomic(constructor, sound_options_model_handle, "main_volume", &sound_options_context.main_volume);
bind_atomic(constructor, sound_options_model_handle, "bgm_volume", &sound_options_context.bgm_volume);
bind_atomic(constructor, sound_options_model_handle, "low_health_beeps_enabled", &sound_options_context.low_health_beeps_enabled);
}
void make_debug_bindings(Rml::Context* context) {
Rml::DataModelConstructor constructor = context->CreateDataModel("debug_model");
if (!constructor) {
throw std::runtime_error("Failed to make RmlUi data model for the debug menu");
}
bind_config_list_events(constructor);
// Bind the debug mode enabled flag.
constructor.Bind("debug_enabled", &debug_context.debug_enabled);
debug_context.model_handle = constructor.GetModelHandle();
}
void make_bindings(Rml::Context* context) override {
// initially set cont state for ui help
//recomp::config_menu_set_cont_or_kb(recompui::get_cont_active());
make_nav_help_bindings(context);
make_general_bindings(context);
make_controls_bindings(context);
make_graphics_bindings(context);
make_sound_options_bindings(context);
make_debug_bindings(context);
}
};
std::unique_ptr<recompui::MenuController> recompui::create_config_menu() {
return std::make_unique<ConfigMenu>();
}
bool banjo::get_debug_mode_enabled() {
return debug_context.debug_enabled;
}
void banjo::set_debug_mode_enabled(bool enabled) {
debug_context.debug_enabled = enabled;
if (debug_context.model_handle) {
debug_context.model_handle.DirtyVariable("debug_enabled");
}
}
void recompui::update_supported_options() {
msaa2x_supported = banjo::renderer::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA2X;
msaa4x_supported = banjo::renderer::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA4X;
msaa8x_supported = banjo::renderer::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA8X;
sample_positions_supported = banjo::renderer::RT64SamplePositionsSupported();
new_options = ultramodern::renderer::get_graphics_config();
graphics_model_handle.DirtyAllVariables();
}
void recompui::toggle_fullscreen() {
new_options.wm_option = (new_options.wm_option == ultramodern::renderer::WindowMode::Windowed) ? ultramodern::renderer::WindowMode::Fullscreen : ultramodern::renderer::WindowMode::Windowed;
apply_graphics_config();
graphics_model_handle.DirtyVariable("wm_option");
}
void recompui::open_prompt(
const std::string& headerText,
const std::string& contentText,
const std::string& confirmLabelText,
const std::string& cancelLabelText,
std::function<void()> confirmCb,
std::function<void()> cancelCb,
ButtonVariant _confirmVariant,
ButtonVariant _cancelVariant,
bool _focusOnCancel,
const std::string& _returnElementId
) {
printf("Prompt opened\n %s (%s): %s %s\n", contentText.c_str(), headerText.c_str(), confirmLabelText.c_str(), cancelLabelText.c_str());
printf(" Autoselected %s\n", confirmLabelText.c_str());
confirmCb();
}
bool recompui::is_prompt_open() {
return false;
}
void recompui::set_config_tab(ConfigTab tab) {
ContextId config_context = recompui::get_config_context_id();
Rml::ElementDocument* doc = config_context.get_document();
assert(doc != nullptr);
Rml::Element* tabset_el = doc->GetElementById("config_tabset");
assert(tabset_el != nullptr);
Rml::ElementTabSet* tabset = rmlui_dynamic_cast<Rml::ElementTabSet*>(tabset_el);
assert(tabset != nullptr);
int tab_index = 0;
switch (tab) {
case ConfigTab::General:
tab_index = 0;
break;
case ConfigTab::Controls:
tab_index = 1;
break;
case ConfigTab::Graphics:
tab_index = 2;
break;
case ConfigTab::Sound:
tab_index = 3;
break;
case ConfigTab::Mods:
tab_index = 4;
break;
case ConfigTab::Debug:
tab_index = 5;
break;
default:
assert(false);
return;
}
tabset->SetActiveTab(tab_index);
}
+236
View File
@@ -0,0 +1,236 @@
#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_id(std::string_view id) {
this->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(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_min_value(min_value);
slider->set_max_value(max_value);
slider->set_step_value(step_value);
slider->set_value(value);
slider->add_value_changed_callback(std::bind(&ConfigOptionSlider::slider_value_changed, this, std::placeholders::_1));
}
// ConfigOptionTextInput
void ConfigOptionTextInput::text_changed(const std::string &text) {
callback(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_text(value);
text_input->add_text_changed_callback(std::bind(&ConfigOptionTextInput::text_changed, this, std::placeholders::_1));
}
// ConfigOptionRadio
void ConfigOptionRadio::index_changed(uint32_t index) {
callback(id, index);
}
ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) : ConfigOptionElement(parent) {
this->callback = callback;
radio = get_current_context().create_element<Radio>(this);
radio->add_index_changed_callback(std::bind(&ConfigOptionRadio::index_changed, this, std::placeholders::_1));
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, "");
}
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_left(12.0f);
header_container->set_gap(24.0f);
{
back_button = context.create_element<Button>(header_container, "Back", ButtonStyle::Secondary);
back_button->add_pressed_callback(std::bind(&ConfigSubMenu::back_button_pressed, this));
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);
}
}
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_id(id);
option->set_name(name);
option->set_description(description);
option->set_hover_callback(std::bind(&ConfigSubMenu::option_hovered, this, std::placeholders::_1, std::placeholders::_2));
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;
}
}
+102
View File
@@ -0,0 +1,102 @@
#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 id;
std::string name;
std::string description;
std::function<void(ConfigOptionElement *, bool)> hover_callback = nullptr;
virtual void process_event(const Event &e) override;
public:
ConfigOptionElement(Element *parent);
virtual ~ConfigOptionElement();
void set_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;
};
class ConfigOptionSlider : public ConfigOptionElement {
protected:
Slider *slider = nullptr;
std::function<void(const std::string &, double)> callback;
void slider_value_changed(double v);
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);
};
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);
public:
ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
};
class ConfigOptionRadio : public ConfigOptionElement {
protected:
Radio *radio = nullptr;
std::function<void(const std::string &, uint32_t)> callback;
void index_changed(uint32_t index);
public:
ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
};
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);
public:
ConfigSubMenu(Element *parent);
virtual ~ConfigSubMenu();
void enter(std::string_view title);
void clear_options();
void add_slider_option(std::string_view id, std::string_view name, std::string_view description, double value, double min, double max, double step, bool percent, std::function<void(const std::string &, double)> callback);
void add_text_option(std::string_view id, std::string_view name, std::string_view description, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
void add_radio_option(std::string_view id, std::string_view name, std::string_view description, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
};
class ElementConfigSubMenu : public Rml::Element {
public:
ElementConfigSubMenu(const Rml::String &tag);
virtual ~ElementConfigSubMenu();
void set_display(bool display);
ConfigSubMenu *get_config_sub_menu_element() const;
private:
ConfigSubMenu *config_sub_menu;
};
}
#endif
+42
View File
@@ -0,0 +1,42 @@
#include "ui_elements.h"
struct RecompCustomElement {
Rml::String tag;
std::unique_ptr<Rml::ElementInstancer> instancer;
};
#define CUSTOM_ELEMENT(s, e) { s, std::make_unique< Rml::ElementInstancerGeneric< e > >() }
static RecompCustomElement custom_elements[] = {
CUSTOM_ELEMENT("recomp-mod-menu", recompui::ElementModMenu),
CUSTOM_ELEMENT("recomp-config-sub-menu", recompui::ElementConfigSubMenu),
};
void recompui::register_custom_elements() {
for (auto& element_config : custom_elements) {
Rml::Factory::RegisterElementInstancer(element_config.tag, element_config.instancer.get());
}
}
Rml::ElementInstancer* recompui::get_custom_element_instancer(std::string tag) {
for (auto& element_config : custom_elements) {
if (tag == element_config.tag) {
return element_config.instancer.get();
}
}
return nullptr;
}
Rml::ElementPtr recompui::create_custom_element(Rml::Element* parent, std::string tag) {
auto instancer = recompui::get_custom_element_instancer(tag);
const Rml::XMLAttributes attributes = {};
if (Rml::ElementPtr element = instancer->InstanceElement(parent, tag, attributes))
{
element->SetInstancer(instancer);
element->SetAttributes(attributes);
return element;
}
return nullptr;
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef RECOMPUI_ELEMENTS_H
#define RECOMPUI_ELEMENTS_H
#include "recomp_ui.h"
#include "RmlUi/Core/Element.h"
#include "ui_mod_menu.h"
#include "ui_config_sub_menu.h"
namespace recompui {
void register_custom_elements();
Rml::ElementInstancer* get_custom_element_instancer(std::string tag);
}
#endif
+134
View File
@@ -0,0 +1,134 @@
#include "recomp_ui.h"
#include "banjo_config.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
#include "nfd.h"
#include <filesystem>
static std::string version_string;
Rml::DataModelHandle model_handle;
bool bk_rom_valid = false;
extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
if (result == NFD_OKAY) {
std::filesystem::path path{native_path};
NFD_FreePathN(native_path);
native_path = nullptr;
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
bk_rom_valid = true;
model_handle.DirtyVariable("bk_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
}
recompui::ContextId launcher_context;
recompui::ContextId recompui::get_launcher_context_id() {
return launcher_context;
}
class LauncherMenu : public recompui::MenuController {
public:
LauncherMenu() {
bk_rom_valid = recomp::is_rom_valid(supported_games[0].game_id);
}
~LauncherMenu() override {
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
launcher_context = recompui::create_context("assets/launcher.rml");
Rml::ElementDocument* ret = launcher_context.get_document();
return ret;
}
void 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) {
bk_rom_valid = true;
model_handle.DirtyVariable("bk_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_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_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");
constructor.Bind("bk_rom_valid", &bk_rom_valid);
version_string = recomp::get_project_version().to_string();
constructor.Bind("version_number", &version_string);
model_handle = constructor.GetModelHandle();
}
};
std::unique_ptr<recompui::MenuController> recompui::create_launcher_menu() {
return std::make_unique<LauncherMenu>();
}
+121
View File
@@ -0,0 +1,121 @@
#include "ui_mod_details_panel.h"
#include "librecomp/mods.hpp"
namespace recompui {
ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
set_flex(1.0f, 1.0f, 200.0f);
set_height(100.0f, Unit::Percent);
set_display(Display::Flex);
set_flex_direction(FlexDirection::Column);
set_border_bottom_right_radius(16.0f);
set_background_color(Color{ 190, 184, 219, 25 });
ContextId context = get_current_context();
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 });
{
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<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
body_container->set_flex(0.0f, 0.0f);
body_container->set_text_align(TextAlign::Left);
body_container->set_padding(16.0f);
body_container->set_gap(16.0f);
{
description_label = context.create_element<Label>(body_container, LabelStyle::Normal);
authors_label = context.create_element<Label>(body_container, LabelStyle::Normal);
}
spacer_element = context.create_element<Element>(this);
spacer_element->set_flex(1.0f, 0.0f);
buttons_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceAround);
buttons_container->set_flex(0.0f, 0.0f);
buttons_container->set_padding(16.0f);
buttons_container->set_justify_content(JustifyContent::SpaceBetween);
{
enable_container = context.create_element<Container>(buttons_container, FlexDirection::Row, JustifyContent::FlexStart);
enable_container->set_align_items(AlignItems::Center);
enable_container->set_gap(16.0f);
{
enable_toggle = context.create_element<Toggle>(enable_container);
enable_toggle->add_checked_callback(std::bind(&ModDetailsPanel::enable_toggle_checked, this, std::placeholders::_1));
enable_label = context.create_element<Label>(enable_container, "A currently enabled mod requires this mod", LabelStyle::Annotation);
}
configure_button = context.create_element<Button>(buttons_container, "Configure", recompui::ButtonStyle::Secondary);
configure_button->add_pressed_callback(std::bind(&ModDetailsPanel::configure_button_pressed, this));
}
}
ModDetailsPanel::~ModDetailsPanel() {
}
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 = "<i>Authors</i>:";
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);
}
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::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
+45
View File
@@ -0,0 +1,45 @@
#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"
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);
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;
Container *body_container = nullptr;
Label *description_label = nullptr;
Label *authors_label = nullptr;
Element *spacer_element = nullptr;
Container *buttons_container = nullptr;
Container *enable_container = nullptr;
Toggle *enable_toggle = nullptr;
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
+572
View File
@@ -0,0 +1,572 @@
#include "ui_mod_menu.h"
#include "recomp_ui.h"
#include "librecomp/mods.hpp"
#include <string>
#ifdef WIN32
#include <shellapi.h>
#endif
// TODO:
// - Set up navigation.
// - Add hover and active state for mod entries.
namespace recompui {
static std::string generate_thumbnail_src_for_mod(const std::string &mod_id) {
return "?/mods/" + mod_id + "/thumb";
}
static bool is_mod_enabled_or_auto(const std::string &mod_id) {
return recomp::mods::is_mod_enabled(mod_id) || recomp::mods::is_mod_auto_enabled(mod_id);
}
// ModEntryView
ModEntryView::ModEntryView(Element *parent) : Element(parent) {
ContextId context = get_current_context();
set_display(Display::Flex);
set_flex_direction(FlexDirection::Row);
set_width(100.0f, Unit::Percent);
set_height_auto();
set_padding_top(4.0f);
set_padding_right(8.0f);
set_padding_bottom(4.0f);
set_padding_left(8.0f);
set_border_width(1.1f);
set_border_color(Color{ 242, 242, 242, 12 });
set_background_color(Color{ 242, 242, 242, 12 });
set_cursor(Cursor::Pointer);
checked_style.set_border_color(Color{ 242, 242, 242, 160 });
hover_style.set_border_color(Color{ 242, 242, 242, 64 });
checked_hover_style.set_border_color(Color{ 242, 242, 242, 204 });
{
thumbnail_image = context.create_element<Image>(this, "");
thumbnail_image->set_width(100.0f);
thumbnail_image->set_height(100.0f);
thumbnail_image->set_min_width(100.0f);
thumbnail_image->set_min_height(100.0f);
thumbnail_image->set_background_color(Color{ 190, 184, 219, 25 });
body_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::FlexStart);
body_container->set_width_auto();
body_container->set_height(100.0f);
body_container->set_margin_left(16.0f);
body_container->set_overflow(Overflow::Hidden);
{
name_label = context.create_element<Label>(body_container, LabelStyle::Normal);
description_label = context.create_element<Label>(body_container, LabelStyle::Small);
} // body_container
} // this
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&checked_hover_style, { checked_state, hover_state });
}
ModEntryView::~ModEntryView() {
}
void ModEntryView::set_mod_details(const recomp::mods::ModDetails &details) {
name_label->set_text(details.display_name);
description_label->set_text(details.short_description);
}
void ModEntryView::set_mod_thumbnail(const std::string &thumbnail) {
thumbnail_image->set_src(thumbnail);
}
void ModEntryView::set_mod_enabled(bool enabled) {
set_opacity(enabled ? 1.0f : 0.5f);
}
void ModEntryView::set_selected(bool selected) {
set_style_enabled(checked_state, selected);
}
// ModEntryButton
ModEntryButton::ModEntryButton(Element *parent, uint32_t mod_index) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Focus, EventType::Drag)) {
this->mod_index = mod_index;
set_drag(Drag::Drag);
ContextId context = get_current_context();
view = context.create_element<ModEntryView>(this);
}
ModEntryButton::~ModEntryButton() {
}
void ModEntryButton::set_mod_selected_callback(std::function<void(uint32_t)> callback) {
selected_callback = callback;
}
void ModEntryButton::set_mod_drag_callback(std::function<void(uint32_t, EventDrag)> callback) {
drag_callback = callback;
}
void ModEntryButton::set_mod_details(const recomp::mods::ModDetails &details) {
view->set_mod_details(details);
}
void ModEntryButton::set_mod_thumbnail(const std::string &thumbnail) {
view->set_mod_thumbnail(thumbnail);
}
void ModEntryButton::set_mod_enabled(bool enabled) {
view->set_mod_enabled(enabled);
}
void ModEntryButton::set_selected(bool selected) {
view->set_selected(selected);
}
void ModEntryButton::process_event(const Event& e) {
switch (e.type) {
case EventType::Click:
selected_callback(mod_index);
break;
case EventType::Hover:
view->set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
break;
case EventType::Focus:
break;
case EventType::Drag:
drag_callback(mod_index, std::get<EventDrag>(e.variant));
break;
default:
break;
}
}
// ModEntrySpacer
void ModEntrySpacer::check_height_distance() {
constexpr float tolerance = 0.01f;
if (abs(target_height - height) < tolerance) {
height = target_height;
set_height(height, Unit::Dp);
}
else {
queue_update();
}
}
void ModEntrySpacer::process_event(const Event &e) {
switch (e.type) {
case EventType::Update: {
std::chrono::high_resolution_clock::duration now = ultramodern::time_since_start();
float delta_time = std::max(std::chrono::duration<float>(now - last_time).count(), 0.0f);
constexpr float dp_speed = 1000.0f;
last_time = now;
if (target_height < height) {
height += std::max(-dp_speed * delta_time, target_height - height);
}
else {
height += std::min(dp_speed * delta_time, target_height - height);
}
set_height(height, Unit::Dp);
check_height_distance();
break;
}
default:
break;
}
}
ModEntrySpacer::ModEntrySpacer(Element *parent) : Element(parent) {
// Do nothing.
}
void ModEntrySpacer::set_target_height(float target_height, bool animate_to_target) {
this->target_height = target_height;
if (animate_to_target) {
last_time = ultramodern::time_since_start();
check_height_distance();
}
else {
height = target_height;
set_height(target_height, Unit::Dp);
}
}
// ModMenu
void ModMenu::refresh_mods() {
for (const std::string &thumbnail : loaded_thumbnails) {
recompui::release_image(thumbnail);
}
recomp::mods::scan_mods();
mod_details = recomp::mods::get_mod_details(game_mod_id);
create_mod_list();
}
void ModMenu::open_mods_folder() {
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
#if defined(WIN32)
std::wstring path_wstr = mods_directory.wstring();
ShellExecuteW(NULL, L"open", path_wstr.c_str(), NULL, NULL, SW_SHOWDEFAULT);
#elif defined(__linux__)
std::string command = "xdg-open " + mods_directory.string() + " &";
std::system(command.c_str());
#else
static_assert(false, "Not implemented for this platform.");
#endif
}
void ModMenu::mod_toggled(bool enabled) {
if (active_mod_index >= 0) {
recomp::mods::enable_mod(mod_details[active_mod_index].mod_id, enabled);
// Refresh enabled status for all mods in case one of them got auto-enabled due to being a dependency.
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
mod_entry_buttons[i]->set_mod_enabled(is_mod_enabled_or_auto(mod_details[i].mod_id));
}
}
}
void ModMenu::mod_selected(uint32_t mod_index) {
if (active_mod_index >= 0) {
mod_entry_buttons[active_mod_index]->set_selected(false);
}
active_mod_index = mod_index;
if (active_mod_index >= 0) {
std::string thumbnail_src = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id);
const recomp::mods::ConfigSchema &config_schema = recomp::mods::get_mod_config_schema(mod_details[active_mod_index].mod_id);
bool toggle_checked = is_mod_enabled_or_auto(mod_details[mod_index].mod_id);
bool auto_enabled = recomp::mods::is_mod_auto_enabled(mod_details[mod_index].mod_id);
bool toggle_enabled = !auto_enabled && (mod_details[mod_index].runtime_toggleable || !ultramodern::is_game_started());
bool configure_enabled = !config_schema.options.empty();
mod_details_panel->set_mod_details(mod_details[mod_index], thumbnail_src, toggle_checked, toggle_enabled, auto_enabled, configure_enabled);
mod_entry_buttons[active_mod_index]->set_selected(true);
}
}
void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) {
constexpr float spacer_height = 110.0f;
switch (drag.phase) {
case DragPhase::Start: {
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
mod_entry_middles[i] = mod_entry_buttons[i]->get_absolute_top() + mod_entry_buttons[i]->get_client_height() / 2.0f;
}
// When the drag phase starts, we make the floating mod details visible and store the relative coordinate of the
// mouse cursor. Instantly hide the real element and use a spacer in its place that will stay on the same size as
// long as the cursor is hovering over this slot.
float width = mod_entry_buttons[mod_index]->get_client_width();
float height = mod_entry_buttons[mod_index]->get_client_height();
float left = mod_entry_buttons[mod_index]->get_absolute_left() - get_absolute_left();
float top = mod_entry_buttons[mod_index]->get_absolute_top() - (height / 2.0f); // TODO: Figure out why this adjustment is even necessary.
mod_entry_buttons[mod_index]->set_display(Display::None);
mod_entry_floating_view->set_display(Display::Flex);
mod_entry_floating_view->set_mod_details(mod_details[mod_index]);
mod_entry_floating_view->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id));
mod_entry_floating_view->set_mod_enabled(is_mod_enabled_or_auto(mod_details[mod_index].mod_id));
mod_entry_floating_view->set_left(left, Unit::Px);
mod_entry_floating_view->set_top(top, Unit::Px);
mod_entry_floating_view->set_width(width, Unit::Px);
mod_entry_floating_view->set_height(height, Unit::Px);
mod_drag_start_coordinates[0] = drag.x;
mod_drag_start_coordinates[1] = drag.y;
mod_drag_view_coordinates[0] = left;
mod_drag_view_coordinates[1] = top;
mod_drag_target_index = mod_index;
mod_entry_spacers[mod_drag_target_index]->set_target_height(spacer_height, false);
break;
}
case DragPhase::Move: {
// Binary search for the drag area.
uint32_t low = 0;
uint32_t high = mod_entry_buttons.size();
while (low < high) {
uint32_t mid = low + (high - low) / 2;
if (drag.y < mod_entry_middles[mid]) {
high = mid;
}
else {
low = mid + 1;
}
}
uint32_t new_index = low;
float delta_x = drag.x - mod_drag_start_coordinates[0];
float delta_y = drag.y - mod_drag_start_coordinates[1];
mod_entry_floating_view->set_left(mod_drag_view_coordinates[0] + delta_x, Unit::Px);
mod_entry_floating_view->set_top(mod_drag_view_coordinates[1] + delta_y, Unit::Px);
if (mod_drag_target_index != new_index) {
mod_entry_spacers[mod_drag_target_index]->set_target_height(0.0f, true);
mod_entry_spacers[new_index]->set_target_height(spacer_height, true);
mod_drag_target_index = new_index;
}
break;
}
case DragPhase::End: {
// Dragging has ended, hide the floating view.
mod_entry_buttons[mod_index]->set_display(Display::Block);
mod_entry_buttons[mod_index]->set_selected(false);
mod_entry_spacers[mod_drag_target_index]->set_target_height(0.0f, false);
mod_entry_floating_view->set_display(Display::None);
// Result needs a small substraction when dragging downwards.
if (mod_drag_target_index > mod_index) {
mod_drag_target_index--;
}
// Re-order the mods and update all the details on the menu.
recomp::mods::set_mod_index(game_mod_id, mod_details[mod_index].mod_id, mod_drag_target_index);
mod_details = recomp::mods::get_mod_details(game_mod_id);
for (size_t i = 0; i < mod_entry_buttons.size(); i++) {
mod_entry_buttons[i]->set_mod_details(mod_details[i]);
mod_entry_buttons[i]->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[i].mod_id));
mod_entry_buttons[i]->set_mod_enabled(is_mod_enabled_or_auto(mod_details[i].mod_id));
}
mod_entry_buttons[mod_drag_target_index]->set_selected(true);
active_mod_index = mod_drag_target_index;
break;
}
default:
break;
}
}
// TODO remove this once this is migrated to the new system.
ContextId sub_menu_context;
ContextId get_config_sub_menu_context_id() {
return sub_menu_context;
}
void ModMenu::mod_configure_requested() {
if (active_mod_index >= 0) {
// Record the context that was open when this function was called and close it.
ContextId prev_context = recompui::get_current_context();
prev_context.close();
// Open the sub menu context and set up the element.
sub_menu_context.open();
config_sub_menu->clear_options();
const recomp::mods::ConfigSchema &config_schema = recomp::mods::get_mod_config_schema(mod_details[active_mod_index].mod_id);
for (const recomp::mods::ConfigOption &option : config_schema.options) {
recomp::mods::ConfigValueVariant config_value = recomp::mods::get_mod_config_value(mod_details[active_mod_index].mod_id, option.id);
if (std::holds_alternative<std::monostate>(config_value)) {
continue;
}
switch (option.type) {
case recomp::mods::ConfigOptionType::Enum: {
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options, std::bind(&ModMenu::mod_enum_option_changed, this, std::placeholders::_1, std::placeholders::_2));
break;
}
case recomp::mods::ConfigOptionType::Number: {
const recomp::mods::ConfigOptionNumber &option_number = std::get<recomp::mods::ConfigOptionNumber>(option.variant);
config_sub_menu->add_slider_option(option.id, option.name, option.description, std::get<double>(config_value), option_number.min, option_number.max, option_number.step, option_number.percent, std::bind(&ModMenu::mod_number_option_changed, this, std::placeholders::_1, std::placeholders::_2));
break;
}
case recomp::mods::ConfigOptionType::String: {
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value), std::bind(&ModMenu::mod_string_option_changed, this, std::placeholders::_1, std::placeholders::_2));
break;
}
default:
assert(false && "Unknown config option type.");
break;
}
}
config_sub_menu->enter(mod_details[active_mod_index].display_name);
sub_menu_context.close();
// Reopen the context that was open when this function was called.
prev_context.open();
// Hide the config menu and show the sub menu.
recompui::hide_context(recompui::get_config_context_id());
recompui::show_context(sub_menu_context, "");
}
}
void ModMenu::mod_enum_option_changed(const std::string &id, uint32_t value) {
if (active_mod_index >= 0) {
recomp::mods::set_mod_config_value(mod_details[active_mod_index].mod_id, id, value);
}
}
void ModMenu::mod_string_option_changed(const std::string &id, const std::string &value) {
if (active_mod_index >= 0) {
recomp::mods::set_mod_config_value(mod_details[active_mod_index].mod_id, id, value);
}
}
void ModMenu::mod_number_option_changed(const std::string &id, double value) {
if (active_mod_index >= 0) {
recomp::mods::set_mod_config_value(mod_details[active_mod_index].mod_id, id, value);
}
}
void ModMenu::create_mod_list() {
ContextId context = get_current_context();
// Clear the contents of the list scroll.
list_scroll_container->clear_children();
mod_entry_buttons.clear();
mod_entry_spacers.clear();
// Create the child elements for the list scroll.
for (size_t mod_index = 0; mod_index < mod_details.size(); mod_index++) {
const std::vector<char> &thumbnail = recomp::mods::get_mod_thumbnail(mod_details[mod_index].mod_id);
std::string thumbnail_name = generate_thumbnail_src_for_mod(mod_details[mod_index].mod_id);
if (!thumbnail.empty()) {
recompui::queue_image_from_bytes(thumbnail_name, thumbnail);
loaded_thumbnails.emplace(thumbnail_name);
}
ModEntrySpacer *spacer = context.create_element<ModEntrySpacer>(list_scroll_container);
mod_entry_spacers.emplace_back(spacer);
ModEntryButton *mod_entry = context.create_element<ModEntryButton>(list_scroll_container, mod_index);
mod_entry->set_mod_selected_callback(std::bind(&ModMenu::mod_selected, this, std::placeholders::_1));
mod_entry->set_mod_drag_callback(std::bind(&ModMenu::mod_dragged, this, std::placeholders::_1, std::placeholders::_2));
mod_entry->set_mod_details(mod_details[mod_index]);
mod_entry->set_mod_thumbnail(thumbnail_name);
mod_entry->set_mod_enabled(is_mod_enabled_or_auto(mod_details[mod_index].mod_id));
mod_entry_buttons.emplace_back(mod_entry);
}
// Add one extra spacer at the bottom.
ModEntrySpacer *spacer = context.create_element<ModEntrySpacer>(list_scroll_container);
mod_entry_spacers.emplace_back(spacer);
mod_entry_middles.resize(mod_entry_buttons.size());
bool mods_available = !mod_details.empty();
body_container->set_display(mods_available ? Display::Flex : Display::None);
body_empty_container->set_display(mods_available ? Display::None : Display::Flex);
if (mods_available) {
mod_selected(0);
}
}
ModMenu::ModMenu(Element *parent) : Element(parent) {
game_mod_id = "mm";
ContextId context = get_current_context();
set_display(Display::Flex);
set_flex(1.0f, 1.0f, 100.0f);
set_flex_direction(FlexDirection::Column);
set_align_items(AlignItems::Center);
set_justify_content(JustifyContent::FlexStart);
set_width(100.0f, Unit::Percent);
set_height(100.0f, Unit::Percent);
{
body_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexStart);
body_container->set_flex(1.0f, 1.0f, 100.0f);
body_container->set_width(100.0f, Unit::Percent);
body_container->set_height(100.0f, Unit::Percent);
{
list_container = context.create_element<Container>(body_container, FlexDirection::Column, JustifyContent::Center);
list_container->set_display(Display::Block);
list_container->set_flex_basis(100.0f);
list_container->set_align_items(AlignItems::Center);
list_container->set_height(100.0f, Unit::Percent);
list_container->set_background_color(Color{ 0, 0, 0, 89 });
list_container->set_border_bottom_left_radius(16.0f);
{
list_scroll_container = context.create_element<ScrollContainer>(list_container, ScrollDirection::Vertical);
} // list_container
mod_details_panel = context.create_element<ModDetailsPanel>(body_container);
mod_details_panel->set_mod_toggled_callback(std::bind(&ModMenu::mod_toggled, this, std::placeholders::_1));
mod_details_panel->set_mod_configure_pressed_callback(std::bind(&ModMenu::mod_configure_requested, this));
} // body_container
body_empty_container = context.create_element<Container>(this, FlexDirection::Column, JustifyContent::SpaceBetween);
body_empty_container->set_flex(1.0f, 1.0f, 100.0f);
body_empty_container->set_display(Display::None);
{
context.create_element<Element>(body_empty_container);
context.create_element<Label>(body_empty_container, "You have no mods. Go get some!", LabelStyle::Large);
context.create_element<Element>(body_empty_container);
} // body_empty_container
footer_container = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::SpaceBetween);
footer_container->set_width(100.0f, recompui::Unit::Percent);
footer_container->set_align_items(recompui::AlignItems::Center);
footer_container->set_background_color(Color{ 0, 0, 0, 89 });
footer_container->set_border_top_width(1.1f);
footer_container->set_border_top_color(Color{ 255, 255, 255, 25 });
footer_container->set_padding(20.0f);
footer_container->set_border_bottom_left_radius(16.0f);
footer_container->set_border_bottom_right_radius(16.0f);
{
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
refresh_button->add_pressed_callback(std::bind(&ModMenu::refresh_mods, this));
context.create_element<Label>(footer_container, "⚠ UNDER CONSTRUCTION ⚠", LabelStyle::Small);
mods_folder_button = context.create_element<Button>(footer_container, "Open Mods Folder", recompui::ButtonStyle::Primary);
mods_folder_button->add_pressed_callback(std::bind(&ModMenu::open_mods_folder, this));
} // footer_container
} // this
mod_entry_floating_view = context.create_element<ModEntryView>(this);
mod_entry_floating_view->set_display(Display::None);
mod_entry_floating_view->set_position(Position::Absolute);
mod_entry_floating_view->set_selected(true);
refresh_mods();
context.close();
sub_menu_context = recompui::create_context("assets/config_sub_menu.rml");
sub_menu_context.open();
Rml::ElementDocument* sub_menu_doc = sub_menu_context.get_document();
Rml::Element* config_sub_menu_generic = sub_menu_doc->GetElementById("config_sub_menu");
ElementConfigSubMenu* config_sub_menu_element = rmlui_dynamic_cast<ElementConfigSubMenu*>(config_sub_menu_generic);
config_sub_menu = config_sub_menu_element->get_config_sub_menu_element();
sub_menu_context.close();
context.open();
}
ModMenu::~ModMenu() {
}
// Placeholder class until the rest of the UI refactor is finished.
ElementModMenu::ElementModMenu(const Rml::String &tag) : Rml::Element(tag) {
SetProperty("width", "100%");
SetProperty("height", "100%");
recompui::Element this_compat(this);
recompui::ContextId context = get_current_context();
mod_menu = context.create_element<ModMenu>(&this_compat);
}
ElementModMenu::~ElementModMenu() {
}
} // namespace recompui
+112
View File
@@ -0,0 +1,112 @@
#ifndef RECOMPUI_ELEMENT_MOD_MENU_H
#define RECOMPUI_ELEMENT_MOD_MENU_H
#include "librecomp/mods.hpp"
#include "elements/ui_scroll_container.h"
#include "ui_config_sub_menu.h"
#include "ui_mod_details_panel.h"
namespace recompui {
class ModMenu;
class ModEntryView : public Element {
public:
ModEntryView(Element *parent);
virtual ~ModEntryView();
void set_mod_details(const recomp::mods::ModDetails &details);
void set_mod_thumbnail(const std::string &thumbnail);
void set_mod_enabled(bool enabled);
void set_selected(bool selected);
private:
Image *thumbnail_image = nullptr;
Container *body_container = nullptr;
Label *name_label = nullptr;
Label *description_label = nullptr;
Style checked_style;
Style hover_style;
Style checked_hover_style;
};
class ModEntryButton : public Element {
public:
ModEntryButton(Element *parent, uint32_t mod_index);
virtual ~ModEntryButton();
void set_mod_selected_callback(std::function<void(uint32_t)> callback);
void set_mod_drag_callback(std::function<void(uint32_t, EventDrag)> callback);
void set_mod_details(const recomp::mods::ModDetails &details);
void set_mod_thumbnail(const std::string &thumbnail);
void set_mod_enabled(bool enabled);
void set_selected(bool selected);
protected:
virtual void process_event(const Event &e) override;
private:
uint32_t mod_index = 0;
ModEntryView *view = nullptr;
std::function<void(uint32_t)> selected_callback = nullptr;
std::function<void(uint32_t, EventDrag)> drag_callback = nullptr;
};
class ModEntrySpacer : public Element {
private:
float height = 0.0f;
float target_height = 0.0f;
std::chrono::high_resolution_clock::duration last_time;
void check_height_distance();
protected:
virtual void process_event(const Event &e) override;
public:
ModEntrySpacer(Element *parent);
void set_target_height(float target_height, bool animate_to_target);
};
class ModMenu : public Element {
public:
ModMenu(Element *parent);
virtual ~ModMenu();
private:
void refresh_mods();
void open_mods_folder();
void mod_toggled(bool enabled);
void mod_selected(uint32_t mod_index);
void mod_dragged(uint32_t mod_index, EventDrag drag);
void mod_configure_requested();
void mod_enum_option_changed(const std::string &id, uint32_t value);
void mod_string_option_changed(const std::string &id, const std::string &value);
void mod_number_option_changed(const std::string &id, double value);
void create_mod_list();
Container *body_container = nullptr;
Container *list_container = nullptr;
ScrollContainer *list_scroll_container = nullptr;
ModDetailsPanel *mod_details_panel = nullptr;
Container *body_empty_container = nullptr;
Container *footer_container = nullptr;
Button *refresh_button = nullptr;
Button *mods_folder_button = nullptr;
int32_t active_mod_index = -1;
std::vector<ModEntryButton *> mod_entry_buttons;
std::vector<ModEntrySpacer *> mod_entry_spacers;
std::vector<float> mod_entry_middles;
ModEntryView *mod_entry_floating_view = nullptr;
float mod_drag_start_coordinates[2] = {};
float mod_drag_view_coordinates[2] = {};
uint32_t mod_drag_target_index = 0;
std::vector<recomp::mods::ModDetails> mod_details{};
std::unordered_set<std::string> loaded_thumbnails;
std::string game_mod_id;
ConfigSubMenu *config_sub_menu;
};
class ElementModMenu : public Rml::Element {
public:
ElementModMenu(const Rml::String& tag);
virtual ~ElementModMenu();
private:
ModMenu *mod_menu;
};
} // namespace recompui
#endif
+654
View File
@@ -0,0 +1,654 @@
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN
#endif
#include <fstream>
#include <filesystem>
#include <concurrentqueue.h>
#include "rt64_render_hooks.h"
#include "rt64_render_interface_builders.h"
#include "rt64_texture_cache.h"
#include "RmlUi/Core/RenderInterfaceCompatibility.h"
#include "ui_renderer.h"
#include "InterfaceVS.hlsl.spirv.h"
#include "InterfacePS.hlsl.spirv.h"
#ifdef _WIN32
# include "InterfaceVS.hlsl.dxil.h"
# include "InterfacePS.hlsl.dxil.h"
#endif
#ifdef _WIN32
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : \
(format) == RT64::RenderShaderFormat::DXIL ? name##BlobDXIL : nullptr)
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::DXIL ? std::size(name##BlobDXIL) : 0)
#else
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : nullptr)
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : 0)
#endif
// TODO deduplicate from rt64_common.h
void CalculateTextureRowWidthPadding(uint32_t rowPitch, uint32_t &rowWidth, uint32_t &rowPadding) {
const int RowMultiple = 256;
rowWidth = rowPitch;
rowPadding = (rowWidth % RowMultiple) ? RowMultiple - (rowWidth % RowMultiple) : 0;
rowWidth += rowPadding;
}
struct RmlPushConstants {
Rml::Matrix4f transform;
Rml::Vector2f translation;
};
struct TextureHandle {
std::unique_ptr<RT64::RenderTexture> texture;
std::unique_ptr<RT64::RenderDescriptorSet> set;
bool transitioned = false;
};
template <typename T>
T from_bytes_le(const char* input) {
return *reinterpret_cast<const T*>(input);
}
typedef std::pair<std::string, std::vector<char>> ImageFromBytes;
namespace recompui {
class RmlRenderInterface_RT64_impl : public Rml::RenderInterfaceCompatibility {
struct DynamicBuffer {
std::unique_ptr<RT64::RenderBuffer> buffer_{};
uint32_t size_ = 0;
uint32_t bytes_used_ = 0;
uint8_t* mapped_data_ = nullptr;
RT64::RenderBufferFlags flags_ = RT64::RenderBufferFlag::NONE;
};
static constexpr uint32_t per_frame_descriptor_set = 0;
static constexpr uint32_t per_draw_descriptor_set = 1;
static constexpr uint32_t initial_upload_buffer_size = 1024 * 1024;
static constexpr uint32_t initial_vertex_buffer_size = 512 * sizeof(Rml::Vertex);
static constexpr uint32_t initial_index_buffer_size = 1024 * sizeof(int);
static constexpr RT64::RenderFormat RmlTextureFormat = RT64::RenderFormat::R8G8B8A8_UNORM;
static constexpr RT64::RenderFormat RmlTextureFormatBgra = RT64::RenderFormat::B8G8R8A8_UNORM;
static constexpr RT64::RenderFormat SwapChainFormat = RT64::RenderFormat::B8G8R8A8_UNORM;
static constexpr uint32_t RmlTextureFormatBytesPerPixel = RenderFormatSize(RmlTextureFormat);
static_assert(RenderFormatSize(RmlTextureFormatBgra) == RmlTextureFormatBytesPerPixel);
RT64::RenderInterface* interface_;
RT64::RenderDevice* device_;
int scissor_x_ = 0;
int scissor_y_ = 0;
int scissor_width_ = 0;
int scissor_height_ = 0;
int window_width_ = 0;
int window_height_ = 0;
RT64::RenderMultisampling multisampling_ = RT64::RenderMultisampling();
Rml::Matrix4f projection_mtx_ = Rml::Matrix4f::Identity();
Rml::Matrix4f transform_ = Rml::Matrix4f::Identity();
Rml::Matrix4f mvp_ = Rml::Matrix4f::Identity();
std::unordered_map<Rml::TextureHandle, TextureHandle> textures_{};
Rml::TextureHandle texture_count_ = 2; // Start at 1 to reserve texture 0 as the 1x1 pixel white texture
DynamicBuffer upload_buffer_;
DynamicBuffer vertex_buffer_;
DynamicBuffer index_buffer_;
std::unique_ptr<RT64::RenderSampler> nearestSampler_{};
std::unique_ptr<RT64::RenderSampler> linearSampler_{};
std::unique_ptr<RT64::RenderShader> vertex_shader_{};
std::unique_ptr<RT64::RenderShader> pixel_shader_{};
std::unique_ptr<RT64::RenderDescriptorSet> sampler_set_{};
std::unique_ptr<RT64::RenderDescriptorSetBuilder> texture_set_builder_{};
std::unique_ptr<RT64::RenderPipelineLayout> layout_{};
std::unique_ptr<RT64::RenderPipeline> pipeline_{};
std::unique_ptr<RT64::RenderPipeline> pipeline_ms_{};
std::unique_ptr<RT64::RenderTexture> screen_texture_ms_{};
std::unique_ptr<RT64::RenderTexture> screen_texture_{};
std::unique_ptr<RT64::RenderFramebuffer> screen_framebuffer_{};
std::unique_ptr<RT64::RenderDescriptorSet> screen_descriptor_set_{};
std::unique_ptr<RT64::RenderBuffer> screen_vertex_buffer_{};
std::unique_ptr<RT64::RenderCommandQueue> copy_command_queue_{};
std::unique_ptr<RT64::RenderCommandList> copy_command_list_{};
std::unique_ptr<RT64::RenderBuffer> copy_buffer_{};
std::unique_ptr<RT64::RenderCommandFence> copy_command_fence_;
uint64_t copy_buffer_size_ = 0;
uint64_t screen_vertex_buffer_size_ = 0;
uint32_t gTexture_descriptor_index;
RT64::RenderInputSlot vertex_slot_{ 0, sizeof(Rml::Vertex) };
RT64::RenderCommandList* list_ = nullptr;
bool scissor_enabled_ = false;
std::vector<std::unique_ptr<RT64::RenderBuffer>> stale_buffers_{};
moodycamel::ConcurrentQueue<ImageFromBytes> image_from_bytes_queue;
std::unordered_map<std::string, std::vector<char>> image_from_bytes_map;
public:
RmlRenderInterface_RT64_impl(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
interface_ = interface;
device_ = device;
// Enable 4X MSAA if supported by the device.
const RT64::RenderSampleCounts desired_sample_count = RT64::RenderSampleCount::COUNT_8;
if (device_->getSampleCountsSupported(SwapChainFormat) & desired_sample_count) {
multisampling_.sampleCount = desired_sample_count;
}
vertex_buffer_.flags_ = RT64::RenderBufferFlag::VERTEX;
index_buffer_.flags_ = RT64::RenderBufferFlag::INDEX;
// Create the texture upload buffer, vertex buffer and index buffer
resize_dynamic_buffer(upload_buffer_, initial_upload_buffer_size, false);
resize_dynamic_buffer(vertex_buffer_, initial_vertex_buffer_size, false);
resize_dynamic_buffer(index_buffer_, initial_index_buffer_size, false);
// Describe the vertex format
std::vector<RT64::RenderInputElement> vertex_elements{};
vertex_elements.emplace_back(RT64::RenderInputElement{ "POSITION", 0, 0, RT64::RenderFormat::R32G32_FLOAT, 0, offsetof(Rml::Vertex, position) });
vertex_elements.emplace_back(RT64::RenderInputElement{ "COLOR", 0, 1, RT64::RenderFormat::R8G8B8A8_UNORM, 0, offsetof(Rml::Vertex, colour) });
vertex_elements.emplace_back(RT64::RenderInputElement{ "TEXCOORD", 0, 2, RT64::RenderFormat::R32G32_FLOAT, 0, offsetof(Rml::Vertex, tex_coord) });
// Create a nearest sampler and a linear sampler
RT64::RenderSamplerDesc samplerDesc;
samplerDesc.minFilter = RT64::RenderFilter::NEAREST;
samplerDesc.magFilter = RT64::RenderFilter::NEAREST;
samplerDesc.addressU = RT64::RenderTextureAddressMode::CLAMP;
samplerDesc.addressV = RT64::RenderTextureAddressMode::CLAMP;
samplerDesc.addressW = RT64::RenderTextureAddressMode::CLAMP;
nearestSampler_ = device_->createSampler(samplerDesc);
samplerDesc.minFilter = RT64::RenderFilter::LINEAR;
samplerDesc.magFilter = RT64::RenderFilter::LINEAR;
linearSampler_ = device_->createSampler(samplerDesc);
// Create the shaders
RT64::RenderShaderFormat shaderFormat = interface_->getCapabilities().shaderFormat;
vertex_shader_ = device_->createShader(GET_SHADER_BLOB(InterfaceVS, shaderFormat), GET_SHADER_SIZE(InterfaceVS, shaderFormat), "VSMain", shaderFormat);
pixel_shader_ = device_->createShader(GET_SHADER_BLOB(InterfacePS, shaderFormat), GET_SHADER_SIZE(InterfacePS, shaderFormat), "PSMain", shaderFormat);
// Create the descriptor set that contains the sampler
RT64::RenderDescriptorSetBuilder sampler_set_builder{};
sampler_set_builder.begin();
sampler_set_builder.addImmutableSampler(1, linearSampler_.get());
sampler_set_builder.addConstantBuffer(3, 1); // Workaround D3D12 crash due to an empty RT64 descriptor set
sampler_set_builder.end();
sampler_set_ = sampler_set_builder.create(device_);
// Create a builder for the descriptor sets that will contain textures
texture_set_builder_ = std::make_unique<RT64::RenderDescriptorSetBuilder>();
texture_set_builder_->begin();
gTexture_descriptor_index = texture_set_builder_->addTexture(2);
texture_set_builder_->end();
// Create the pipeline layout
RT64::RenderPipelineLayoutBuilder layout_builder{};
layout_builder.begin(false, true);
layout_builder.addPushConstant(0, 0, sizeof(RmlPushConstants), RT64::RenderShaderStageFlag::VERTEX);
// Add the descriptor set for descriptors changed once per frame.
layout_builder.addDescriptorSet(sampler_set_builder);
// Add the descriptor set for descriptors changed once per draw.
layout_builder.addDescriptorSet(*texture_set_builder_);
layout_builder.end();
layout_ = layout_builder.create(device_);
// Create the pipeline description
RT64::RenderGraphicsPipelineDesc pipeline_desc{};
pipeline_desc.renderTargetBlend[0] = RT64::RenderBlendDesc::AlphaBlend();
pipeline_desc.renderTargetFormat[0] = SwapChainFormat; // TODO: Use whatever format the swap chain was created with.
pipeline_desc.renderTargetCount = 1;
pipeline_desc.cullMode = RT64::RenderCullMode::NONE;
pipeline_desc.inputSlots = &vertex_slot_;
pipeline_desc.inputSlotsCount = 1;
pipeline_desc.inputElements = vertex_elements.data();
pipeline_desc.inputElementsCount = uint32_t(vertex_elements.size());
pipeline_desc.pipelineLayout = layout_.get();
pipeline_desc.primitiveTopology = RT64::RenderPrimitiveTopology::TRIANGLE_LIST;
pipeline_desc.vertexShader = vertex_shader_.get();
pipeline_desc.pixelShader = pixel_shader_.get();
pipeline_ = device_->createGraphicsPipeline(pipeline_desc);
if (multisampling_.sampleCount > 1) {
pipeline_desc.multisampling = multisampling_;
pipeline_ms_ = device_->createGraphicsPipeline(pipeline_desc);
// Create the descriptor set for the screen drawer.
RT64::RenderDescriptorRange screen_descriptor_range(RT64::RenderDescriptorRangeType::TEXTURE, 2, 1);
screen_descriptor_set_ = device_->createDescriptorSet(RT64::RenderDescriptorSetDesc(&screen_descriptor_range, 1));
// Create vertex buffer for the screen drawer (full-screen triangle).
screen_vertex_buffer_size_ = sizeof(Rml::Vertex) * 3;
screen_vertex_buffer_ = device_->createBuffer(RT64::RenderBufferDesc::VertexBuffer(screen_vertex_buffer_size_, RT64::RenderHeapType::UPLOAD));
Rml::Vertex *vertices = (Rml::Vertex *)(screen_vertex_buffer_->map());
const Rml::ColourbPremultiplied white(255, 255, 255, 255);
vertices[0] = Rml::Vertex{ Rml::Vector2f(-1.0f, 1.0f), white, Rml::Vector2f(0.0f, 0.0f) };
vertices[1] = Rml::Vertex{ Rml::Vector2f(-1.0f, -3.0f), white, Rml::Vector2f(0.0f, 2.0f) };
vertices[2] = Rml::Vertex{ Rml::Vector2f(3.0f, 1.0f), white, Rml::Vector2f(2.0f, 0.0f) };
screen_vertex_buffer_->unmap();
}
copy_command_queue_ = device->createCommandQueue(RT64::RenderCommandListType::COPY);
copy_command_list_ = device->createCommandList(RT64::RenderCommandListType::COPY);
copy_command_fence_ = device->createCommandFence();
}
void reset_dynamic_buffer(DynamicBuffer &dynamic_buffer) {
assert(dynamic_buffer.mapped_data_ == nullptr);
dynamic_buffer.bytes_used_ = 0;
dynamic_buffer.mapped_data_ = reinterpret_cast<uint8_t*>(dynamic_buffer.buffer_->map());
}
void end_dynamic_buffer(DynamicBuffer &dynamic_buffer) {
assert(dynamic_buffer.mapped_data_ != nullptr);
dynamic_buffer.buffer_->unmap();
dynamic_buffer.mapped_data_ = nullptr;
}
void resize_dynamic_buffer(DynamicBuffer &dynamic_buffer, uint32_t new_size, bool map = true) {
// Unmap the buffer if it's mapped
if (dynamic_buffer.mapped_data_ != nullptr) {
dynamic_buffer.buffer_->unmap();
}
// If there's already a buffer, move it into the stale buffers so it persists until the start of next frame.
if (dynamic_buffer.buffer_ != nullptr) {
stale_buffers_.emplace_back(std::move(dynamic_buffer.buffer_));
}
// Create the new buffer, update the size and map it.
dynamic_buffer.buffer_ = device_->createBuffer(RT64::RenderBufferDesc::UploadBuffer(new_size, dynamic_buffer.flags_));
dynamic_buffer.size_ = new_size;
dynamic_buffer.bytes_used_ = 0;
if (map) {
dynamic_buffer.mapped_data_ = reinterpret_cast<uint8_t*>(dynamic_buffer.buffer_->map());
}
}
uint32_t allocate_dynamic_data(DynamicBuffer &dynamic_buffer, uint32_t num_bytes) {
// Check if there's enough remaining room in the buffer to allocate the requested bytes.
uint32_t total_bytes = num_bytes + dynamic_buffer.bytes_used_;
if (total_bytes > dynamic_buffer.size_) {
// There isn't, so mark the current buffer as stale and allocate a new one with 50% more space than the required amount.
resize_dynamic_buffer(dynamic_buffer, total_bytes + total_bytes / 2);
}
// Record the current end of the buffer to return.
uint32_t offset = dynamic_buffer.bytes_used_;
// Bump the buffer's end forward by the number of bytes allocated.
dynamic_buffer.bytes_used_ += num_bytes;
return offset;
}
uint32_t allocate_dynamic_data_aligned(DynamicBuffer &dynamic_buffer, uint32_t num_bytes, uint32_t alignment) {
// Check if there's enough remaining room in the buffer to allocate the requested bytes.
uint32_t total_bytes = num_bytes + dynamic_buffer.bytes_used_;
// Determine the amount of padding needed to meet the target alignment.
uint32_t padding_bytes = ((dynamic_buffer.bytes_used_ + alignment - 1) / alignment) * alignment - dynamic_buffer.bytes_used_;
// If there isn't enough room to allocate the required bytes plus the padding then resize the buffer and allocate from the start of the new one.
if (total_bytes + padding_bytes > dynamic_buffer.size_) {
resize_dynamic_buffer(dynamic_buffer, total_bytes + total_bytes / 2);
dynamic_buffer.bytes_used_ += num_bytes;
return 0;
}
// Otherwise allocate the padding and required bytes and offset the allocated position by the padding size.
return allocate_dynamic_data(dynamic_buffer, padding_bytes + num_bytes) + padding_bytes;
}
void RenderGeometry(Rml::Vertex* vertices, int num_vertices, int* indices, int num_indices, Rml::TextureHandle texture, const Rml::Vector2f& translation) override {
if (!textures_.contains(texture)) {
if (texture == 0) {
Rml::byte white_pixel[] = { 255, 255, 255, 255 };
create_texture(0, white_pixel, Rml::Vector2i{ 1, 1 });
}
else if (texture == 1) {
Rml::byte transparent_pixel[] = { 0, 0, 0, 0 };
create_texture(1, transparent_pixel, Rml::Vector2i{ 1, 1 });
}
else {
assert(false && "Rendered without texture!");
}
}
// Copy the vertex and index data into the mapped buffers.
uint32_t vert_size_bytes = num_vertices * sizeof(*vertices);
uint32_t index_size_bytes = num_indices * sizeof(*indices);
uint32_t vertex_buffer_offset = allocate_dynamic_data(vertex_buffer_, vert_size_bytes);
uint32_t index_buffer_offset = allocate_dynamic_data(index_buffer_, index_size_bytes);
memcpy(vertex_buffer_.mapped_data_ + vertex_buffer_offset, vertices, vert_size_bytes);
memcpy(index_buffer_.mapped_data_ + index_buffer_offset, indices, index_size_bytes);
list_->setViewports(RT64::RenderViewport{ 0, 0, float(window_width_), float(window_height_) });
if (scissor_enabled_) {
list_->setScissors(RT64::RenderRect{
scissor_x_,
scissor_y_,
(scissor_width_ + scissor_x_),
(scissor_height_ + scissor_y_) });
}
else {
list_->setScissors(RT64::RenderRect{ 0, 0, window_width_, window_height_ });
}
RT64::RenderIndexBufferView index_view{index_buffer_.buffer_->at(index_buffer_offset), index_size_bytes, RT64::RenderFormat::R32_UINT};
list_->setIndexBuffer(&index_view);
RT64::RenderVertexBufferView vertex_view{vertex_buffer_.buffer_->at(vertex_buffer_offset), vert_size_bytes};
list_->setVertexBuffers(0, &vertex_view, 1, &vertex_slot_);
TextureHandle &texture_handle = textures_.at(texture);
if (!texture_handle.transitioned) {
// Prepare the texture for being read from a pixel shader.
list_->barriers(RT64::RenderBarrierStage::GRAPHICS, RT64::RenderTextureBarrier(texture_handle.texture.get(), RT64::RenderTextureLayout::SHADER_READ));
texture_handle.transitioned = true;
}
list_->setGraphicsDescriptorSet(texture_handle.set.get(), 1);
RmlPushConstants constants{
.transform = mvp_,
.translation = translation
};
list_->setGraphicsPushConstants(0, &constants);
list_->drawIndexedInstanced(num_indices, 1, 0, 0, 0);
}
void EnableScissorRegion(bool enable) override {
scissor_enabled_ = enable;
}
void SetScissorRegion(int x, int y, int width, int height) override {
scissor_x_ = x;
scissor_y_ = y;
scissor_width_ = width;
scissor_height_ = height;
}
bool LoadTexture(Rml::TextureHandle& texture_handle, Rml::Vector2i& texture_dimensions, const Rml::String& source) override {
flush_image_from_bytes_queue();
auto it = image_from_bytes_map.find(source);
if (it == image_from_bytes_map.end()) {
// Return a transparent texture if the image can't be found.
texture_handle = 1;
texture_dimensions.x = 1;
texture_dimensions.y = 1;
return true;
}
// TODO: This data copy can be avoided when RT64::TextureCache::loadTextureFromBytes's function is updated to only take a pointer and size as the input.
std::vector<uint8_t> data_copy(it->second.data(), it->second.data() + it->second.size());
std::unique_ptr<RT64::RenderBuffer> texture_buffer;
copy_command_list_->begin();
RT64::Texture *texture = RT64::TextureCache::loadTextureFromBytes(device_, copy_command_list_.get(), data_copy, texture_buffer);
copy_command_list_->end();
copy_command_queue_->executeCommandLists(copy_command_list_.get(), copy_command_fence_.get());
copy_command_queue_->waitForCommandFence(copy_command_fence_.get());
if (texture == nullptr) {
return false;
}
texture_handle = texture_count_++;
texture_dimensions.x = texture->width;
texture_dimensions.y = texture->height;
std::unique_ptr<RT64::RenderDescriptorSet> set = texture_set_builder_->create(device_);
set->setTexture(gTexture_descriptor_index, texture->texture.get(), RT64::RenderTextureLayout::SHADER_READ);
textures_.emplace(texture_handle, TextureHandle{ std::move(texture->texture), std::move(set), false });
delete texture;
return true;
}
bool GenerateTexture(Rml::TextureHandle& texture_handle, const Rml::byte* source, const Rml::Vector2i& source_dimensions) override {
if (source_dimensions.x == 0 || source_dimensions.y == 0) {
texture_handle = 0;
return true;
}
texture_handle = texture_count_++;
return create_texture(texture_handle, source, source_dimensions);
}
bool create_texture(Rml::TextureHandle texture_handle, const Rml::byte* source, const Rml::Vector2i& source_dimensions, bool flip_y = false, bool bgra = false) {
std::unique_ptr<RT64::RenderTexture> texture =
device_->createTexture(RT64::RenderTextureDesc::Texture2D(source_dimensions.x, source_dimensions.y, 1, bgra ? RmlTextureFormatBgra : RmlTextureFormat));
if (texture != nullptr) {
uint32_t image_size_bytes = source_dimensions.x * source_dimensions.y * RmlTextureFormatBytesPerPixel;
// Calculate the texture padding for alignment purposes.
uint32_t row_pitch = source_dimensions.x * RmlTextureFormatBytesPerPixel;
uint32_t row_byte_width, row_byte_padding;
CalculateTextureRowWidthPadding(row_pitch, row_byte_width, row_byte_padding);
uint32_t row_width = row_byte_width / RmlTextureFormatBytesPerPixel;
// Calculate the real number of bytes to upload including padding.
uint32_t uploaded_size_bytes = row_byte_width * source_dimensions.y;
// Allocate room in the upload buffer for the uploaded data.
if (uploaded_size_bytes > copy_buffer_size_) {
copy_buffer_size_ = (uploaded_size_bytes * 3) / 2;
copy_buffer_ = device_->createBuffer(RT64::RenderBufferDesc::UploadBuffer(copy_buffer_size_));
}
// Copy the source data into the upload buffer.
uint8_t* dst_data = (uint8_t *)(copy_buffer_->map());
if (row_byte_padding == 0) {
// Copy row-by-row if the image is flipped.
if (flip_y) {
for (int row = 0; row < source_dimensions.y; row++) {
memcpy(dst_data + row_byte_width * (source_dimensions.y - row - 1), source + row_byte_width * row, row_byte_width);
}
}
// Directly copy if no padding is needed and the image isn't flipped.
else {
memcpy(dst_data, source, image_size_bytes);
}
}
// Otherwise pad each row as necessary.
else {
const Rml::byte *src_data = flip_y ? source + row_pitch * (source_dimensions.y - 1) : source;
uint32_t src_stride = flip_y ? -row_pitch : row_pitch;
for (int row = 0; row < source_dimensions.y; row++) {
memcpy(dst_data, src_data, row_pitch);
src_data += src_stride;
dst_data += row_byte_width;
}
}
copy_buffer_->unmap();
// Reset the command list.
copy_command_list_->begin();
// Prepare the texture to be a destination for copying.
copy_command_list_->barriers(RT64::RenderBarrierStage::COPY, RT64::RenderTextureBarrier(texture.get(), RT64::RenderTextureLayout::COPY_DEST));
// Copy the upload buffer into the texture.
copy_command_list_->copyTextureRegion(
RT64::RenderTextureCopyLocation::Subresource(texture.get()),
RT64::RenderTextureCopyLocation::PlacedFootprint(copy_buffer_.get(), RmlTextureFormat, source_dimensions.x, source_dimensions.y, 1, row_width));
// End the command list, execute it and wait.
copy_command_list_->end();
copy_command_queue_->executeCommandLists(copy_command_list_.get(), copy_command_fence_.get());
copy_command_queue_->waitForCommandFence(copy_command_fence_.get());
// Create a descriptor set with this texture in it.
std::unique_ptr<RT64::RenderDescriptorSet> set = texture_set_builder_->create(device_);
set->setTexture(gTexture_descriptor_index, texture.get(), RT64::RenderTextureLayout::SHADER_READ);
textures_.emplace(texture_handle, TextureHandle{ std::move(texture), std::move(set), false });
return true;
}
return false;
}
void ReleaseTexture(Rml::TextureHandle texture) override {
if (texture > 1) {
// Textures #0 and #1 are reserved and should never be released.
textures_.erase(texture);
}
}
void SetTransform(const Rml::Matrix4f* transform) override {
transform_ = transform ? *transform : Rml::Matrix4f::Identity();
recalculate_mvp();
}
void recalculate_mvp() {
mvp_ = projection_mtx_ * transform_;
}
void start(RT64::RenderCommandList* list, int image_width, int image_height) {
list_ = list;
if (multisampling_.sampleCount > 1) {
if (window_width_ != image_width || window_height_ != image_height) {
screen_framebuffer_.reset();
screen_texture_ = device_->createTexture(RT64::RenderTextureDesc::ColorTarget(image_width, image_height, SwapChainFormat));
screen_texture_ms_ = device_->createTexture(RT64::RenderTextureDesc::ColorTarget(image_width, image_height, SwapChainFormat, multisampling_));
const RT64::RenderTexture *color_attachment = screen_texture_ms_.get();
screen_framebuffer_ = device_->createFramebuffer(RT64::RenderFramebufferDesc(&color_attachment, 1));
screen_descriptor_set_->setTexture(0, screen_texture_.get(), RT64::RenderTextureLayout::SHADER_READ);
}
list_->setPipeline(pipeline_ms_.get());
}
else {
list_->setPipeline(pipeline_.get());
}
list_->setGraphicsPipelineLayout(layout_.get());
// Bind the set for descriptors that don't change across draws
list_->setGraphicsDescriptorSet(sampler_set_.get(), 0);
window_width_ = image_width;
window_height_ = image_height;
projection_mtx_ = Rml::Matrix4f::ProjectOrtho(0.0f, float(image_width), float(image_height), 0.0f, -10000, 10000);
recalculate_mvp();
// The following code assumes command lists aren't double buffered.
// Clear out any stale buffers from the last command list.
stale_buffers_.clear();
// Reset buffers.
reset_dynamic_buffer(upload_buffer_);
reset_dynamic_buffer(vertex_buffer_);
reset_dynamic_buffer(index_buffer_);
// Set an internal texture as the render target if MSAA is enabled.
if (multisampling_.sampleCount > 1) {
list->barriers(RT64::RenderBarrierStage::GRAPHICS, RT64::RenderTextureBarrier(screen_texture_ms_.get(), RT64::RenderTextureLayout::COLOR_WRITE));
list->setFramebuffer(screen_framebuffer_.get());
list->clearColor(0, RT64::RenderColor(0.0f, 0.0f, 0.0f, 0.0f));
}
}
void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer) {
// Draw the texture were rendered the UI in to the swap chain framebuffer if MSAA is enabled.
if (multisampling_.sampleCount > 1) {
RT64::RenderTextureBarrier before_resolve_barriers[] = {
RT64::RenderTextureBarrier(screen_texture_ms_.get(), RT64::RenderTextureLayout::RESOLVE_SOURCE),
RT64::RenderTextureBarrier(screen_texture_.get(), RT64::RenderTextureLayout::RESOLVE_DEST)
};
list->barriers(RT64::RenderBarrierStage::COPY, before_resolve_barriers, uint32_t(std::size(before_resolve_barriers)));
list->resolveTexture(screen_texture_.get(), screen_texture_ms_.get());
list->barriers(RT64::RenderBarrierStage::GRAPHICS, RT64::RenderTextureBarrier(screen_texture_.get(), RT64::RenderTextureLayout::SHADER_READ));
list->setFramebuffer(framebuffer);
list->setPipeline(pipeline_.get());
list->setGraphicsPipelineLayout(layout_.get());
list->setGraphicsDescriptorSet(sampler_set_.get(), 0);
list->setGraphicsDescriptorSet(screen_descriptor_set_.get(), 1);
RT64::RenderVertexBufferView vertex_view(screen_vertex_buffer_.get(), screen_vertex_buffer_size_);
list->setVertexBuffers(0, &vertex_view, 1, &vertex_slot_);
RmlPushConstants constants{
.transform = Rml::Matrix4f::Identity(),
.translation = Rml::Vector2f(0.0f, 0.0f)
};
list_->setGraphicsPushConstants(0, &constants);
list->drawInstanced(3, 1, 0, 0);
}
end_dynamic_buffer(upload_buffer_);
end_dynamic_buffer(vertex_buffer_);
end_dynamic_buffer(index_buffer_);
list_ = nullptr;
}
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
image_from_bytes_queue.enqueue(ImageFromBytes(src, bytes));
}
void flush_image_from_bytes_queue() {
ImageFromBytes image_from_bytes;
while (image_from_bytes_queue.try_dequeue(image_from_bytes)) {
image_from_bytes_map.emplace(image_from_bytes.first, std::move(image_from_bytes.second));
}
}
};
} // namespace recompui
recompui::RmlRenderInterface_RT64::RmlRenderInterface_RT64() = default;
recompui::RmlRenderInterface_RT64::~RmlRenderInterface_RT64() = default;
void recompui::RmlRenderInterface_RT64::reset() {
impl.reset();
}
void recompui::RmlRenderInterface_RT64::init(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
impl = std::make_unique<RmlRenderInterface_RT64_impl>(interface, device);
}
Rml::RenderInterface* recompui::RmlRenderInterface_RT64::get_rml_interface() {
if (impl) {
return impl->GetAdaptedInterface();
}
return nullptr;
}
void recompui::RmlRenderInterface_RT64::start(RT64::RenderCommandList* list, int image_width, int image_height) {
assert(static_cast<bool>(impl));
impl->start(list, image_width, image_height);
}
void recompui::RmlRenderInterface_RT64::end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer) {
assert(static_cast<bool>(impl));
impl->end(list, framebuffer);
}
void recompui::RmlRenderInterface_RT64::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
assert(static_cast<bool>(impl));
impl->queue_image_from_bytes(src, bytes);
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef __UI_RENDERER_H__
#define __UI_RENDERER_H__
#include <memory>
namespace RT64 {
struct RenderInterface;
struct RenderDevice;
struct RenderCommandList;
struct RenderFramebuffer;
};
namespace Rml {
class RenderInterface;
}
namespace recompui {
class RmlRenderInterface_RT64_impl;
class RmlRenderInterface_RT64 {
private:
std::unique_ptr<RmlRenderInterface_RT64_impl> impl;
public:
RmlRenderInterface_RT64();
~RmlRenderInterface_RT64();
void reset();
void init(RT64::RenderInterface* interface, RT64::RenderDevice* device);
Rml::RenderInterface* get_rml_interface();
void start(RT64::RenderCommandList* list, int image_width, int image_height);
void end(RT64::RenderCommandList* list, RT64::RenderFramebuffer* framebuffer);
void queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes);
};
} // namespace recompui
#endif
+106
View File
@@ -0,0 +1,106 @@
#include "recomp_ui.h"
#include "RmlUi/Core.h"
#include "ui_rml_hacks.hpp"
//! these are hidden methods not exposed by RmlUi
//! they may need to be updated eventually with RmlUi
RecompRml::CanFocus RecompRml::CanFocusElement(Rml::Element* element)
{
if (!element->IsVisible())
return RecompRml::CanFocus::NoAndNoChildren;
const Rml::ComputedValues& computed = element->GetComputedValues();
if (computed.focus() == Rml::Style::Focus::None)
return RecompRml::CanFocus::NoAndNoChildren;
if (computed.tab_index() == Rml::Style::TabIndex::Auto)
return RecompRml::CanFocus::Yes;
return RecompRml::CanFocus::No;
}
Rml::Element* SearchFocusSubtree(Rml::Element* element, bool forward)
{
auto can_focus = RecompRml::CanFocusElement(element);
if (can_focus == RecompRml::CanFocus::Yes)
return element;
else if (can_focus == RecompRml::CanFocus::NoAndNoChildren)
return nullptr;
for (int i = 0; i < element->GetNumChildren(); i++)
{
int child_index = i;
if (!forward)
child_index = element->GetNumChildren() - i - 1;
if (Rml::Element* result = SearchFocusSubtree(element->GetChild(child_index), forward))
return result;
}
return nullptr;
}
Rml::Element* RecompRml::FindNextTabElement(Rml::Element* current_element, bool forward)
{
// This algorithm is quite sneaky, I originally thought a depth first search would work, but it appears not. What is
// required is to cut the tree in half along the nodes from current_element up the root and then either traverse the
// tree in a clockwise or anticlock wise direction depending if you're searching forward or backward respectively.
// If we're searching forward, check the immediate children of this node first off.
if (forward)
{
for (int i = 0; i < current_element->GetNumChildren(); i++)
if (Rml::Element* result = SearchFocusSubtree(current_element->GetChild(i), forward))
return result;
}
// Now walk up the tree, testing either the bottom or top
// of the tree, depending on whether we're going forward
// or backward respectively.
bool search_enabled = false;
Rml::Element* document = current_element->GetOwnerDocument();
Rml::Element* child = current_element;
Rml::Element* parent = current_element->GetParentNode();
while (child != document)
{
const int num_children = parent->GetNumChildren();
for (int i = 0; i < num_children; i++)
{
// Calculate index into children
const int child_index = forward ? i : (num_children - i - 1);
Rml::Element* search_child = parent->GetChild(child_index);
// Do a search if its enabled
if (search_enabled)
if (Rml::Element* result = SearchFocusSubtree(search_child, forward))
return result;
// Enable searching when we reach the child.
if (search_child == child)
search_enabled = true;
}
// Advance up the tree
child = parent;
parent = parent->GetParentNode();
search_enabled = false;
}
// We could not find anything to focus along this direction.
// If we can focus the document, then focus that now.
if (current_element != document && RecompRml::CanFocusElement(document) == RecompRml::CanFocus::Yes)
return document;
// Otherwise, search the entire document tree. This way we will wrap around.
const int num_children = document->GetNumChildren();
for (int i = 0; i < num_children; i++)
{
const int child_index = forward ? i : (num_children - i - 1);
if (Rml::Element* result = SearchFocusSubtree(document->GetChild(child_index), forward))
return result;
}
return nullptr;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef UI_RML_HACKS_H
#define UI_RML_HACKS_H
#include "RmlUi/Core.h"
namespace RecompRml {
Rml::Element* FindNextTabElement(Rml::Element* current_element, bool forward);
enum class CanFocus { Yes, No, NoAndNoChildren };
CanFocus CanFocusElement(Rml::Element* element);
}
#endif
+792
View File
@@ -0,0 +1,792 @@
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <SDL_video.h>
#else
#include <SDL2/SDL_video.h>
#endif
#include "rt64_render_hooks.h"
#include "concurrentqueue.h"
#include "RmlUi/Core.h"
#include "RmlUi/Debugger.h"
#include "RmlUi/Core/RenderInterfaceCompatibility.h"
#include "RmlUi/../../Source/Core/Elements/ElementLabel.h"
#include "RmlUi_Platform_SDL.h"
#include "recomp_ui.h"
#include "recomp_input.h"
#include "librecomp/game.hpp"
#include "banjo_config.h"
#include "ui_rml_hacks.hpp"
#include "ui_elements.h"
#include "ui_mod_menu.h"
#include "ui_renderer.h"
bool can_focus(Rml::Element* element) {
return element->GetOwnerDocument() != nullptr && element->GetProperty(Rml::PropertyId::TabIndex)->Get<Rml::Style::TabIndex>() != Rml::Style::TabIndex::None;
}
//! Copied from lib\RmlUi\Source\Core\Elements\ElementLabel.cpp
// Get the first descending element whose tag name matches one of tags.
static Rml::Element* TagMatchRecursive(const Rml::StringList& tags, Rml::Element* element)
{
const int num_children = element->GetNumChildren();
for (int i = 0; i < num_children; i++)
{
Rml::Element* child = element->GetChild(i);
for (const Rml::String& tag : tags)
{
if (child->GetTagName() == tag)
return child;
}
Rml::Element* matching_element = TagMatchRecursive(tags, child);
if (matching_element)
return matching_element;
}
return nullptr;
}
Rml::Element* get_target(Rml::ElementDocument* document, Rml::Element* element) {
// Labels can have targets, so check if this element is a label.
if (element->GetTagName() == "label") {
Rml::ElementLabel* labelElement = (Rml::ElementLabel*)element;
const Rml::String target_id = labelElement->GetAttribute<Rml::String>("for", "");
if (target_id.empty())
{
const Rml::StringList matching_tags = {"button", "input", "textarea", "progress", "progressbar", "select"};
return TagMatchRecursive(matching_tags, element);
}
else
{
Rml::Element* target = labelElement->GetElementById(target_id);
if (target != element)
return target;
}
return nullptr;
}
// Return the element directly if no target exists.
return element;
}
namespace recompui {
class UiEventListener : public Rml::EventListener {
event_handler_t* handler_;
Rml::String param_;
public:
UiEventListener(event_handler_t* handler, Rml::String&& param) : handler_(handler), param_(std::move(param)) {}
void ProcessEvent(Rml::Event& event) override {
handler_(param_, event);
}
};
class UiEventListenerInstancer : public Rml::EventListenerInstancer {
std::unordered_map<Rml::String, event_handler_t*> handler_map_;
std::unordered_map<Rml::String, UiEventListener> listener_map_;
public:
Rml::EventListener* InstanceEventListener(const Rml::String& value, Rml::Element* element) override {
// Check if a listener has already been made for the full event string and return it if so.
auto find_listener_it = listener_map_.find(value);
if (find_listener_it != listener_map_.end()) {
return &find_listener_it->second;
}
// No existing listener, so check if a handler has been registered for this event type and create a listener for it if so.
size_t delimiter_pos = value.find(':');
Rml::String event_type = value.substr(0, delimiter_pos);
auto find_handler_it = handler_map_.find(event_type);
if (find_handler_it != handler_map_.end()) {
// A handler was found, create a listener and return it.
Rml::String event_param = value.substr(std::min(delimiter_pos, value.size()));
return &listener_map_.emplace(value, UiEventListener{ find_handler_it->second, std::move(event_param) }).first->second;
}
return nullptr;
}
void register_event(const Rml::String& value, event_handler_t* handler) {
handler_map_.emplace(value, handler);
}
};
}
void recompui::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
listener.register_event(name, handler);
}
Rml::Element* find_autofocus_element(Rml::Element* start) {
Rml::Element* cur_element = start;
Rml::Element* first_found = nullptr;
while (cur_element) {
if (cur_element->HasAttribute("autofocus")) {
break;
}
cur_element = RecompRml::FindNextTabElement(cur_element, true);
// Track the first element that was found to know when we've wrapped around.
if (!first_found) {
first_found = cur_element;
}
// Stop searching if we found the first element again.
else {
if (cur_element == first_found) {
// Return the first tab element as there was nothing marked with autofocus.
return first_found;
}
}
}
return cur_element;
}
struct ContextDetails {
recompui::ContextId context;
Rml::ElementDocument* document;
bool takes_input;
};
class UIState {
Rml::Element* prev_focused = nullptr;
bool mouse_is_active_changed = false;
std::unique_ptr<recompui::MenuController> launcher_menu_controller{};
std::unique_ptr<recompui::MenuController> config_menu_controller{};
std::vector<ContextDetails> shown_contexts{};
public:
bool mouse_is_active_initialized = false;
bool mouse_is_active = false;
bool cont_is_active = false;
bool await_stick_return_x = false;
bool await_stick_return_y = false;
int last_active_mouse_position[2] = {0, 0};
std::unique_ptr<recompui::MenuController> config_controller;
std::unique_ptr<recompui::MenuController> launcher_controller;
std::unique_ptr<SystemInterface_SDL> system_interface;
recompui::RmlRenderInterface_RT64 render_interface;
Rml::Context* context;
recompui::UiEventListenerInstancer event_listener_instancer;
UIState(const UIState& rhs) = delete;
UIState& operator=(const UIState& rhs) = delete;
UIState(UIState&& rhs) = delete;
UIState& operator=(UIState&& rhs) = delete;
UIState(SDL_Window* window, RT64::RenderInterface* interface, RT64::RenderDevice* device) {
launcher_menu_controller = recompui::create_launcher_menu();
config_menu_controller = recompui::create_config_menu();
system_interface = std::make_unique<SystemInterface_SDL>();
system_interface->SetWindow(window);
render_interface.init(interface, device);
launcher_menu_controller->register_events(event_listener_instancer);
config_menu_controller->register_events(event_listener_instancer);
Rml::SetSystemInterface(system_interface.get());
Rml::SetRenderInterface(render_interface.get_rml_interface());
Rml::Factory::RegisterEventListenerInstancer(&event_listener_instancer);
recompui::register_custom_elements();
Rml::Initialise();
// Apply the hack to replace RmlUi's default color parser with one that conforms to HTML5 alpha parsing for SASS compatibility
recompui::apply_color_hack();
int width, height;
SDL_GetWindowSizeInPixels(window, &width, &height);
context = Rml::CreateContext("main", Rml::Vector2i(width, height));
launcher_menu_controller->make_bindings(context);
config_menu_controller->make_bindings(context);
Rml::Debugger::Initialise(context);
{
const Rml::String directory = "assets/";
struct FontFace {
const char* filename;
bool fallback_face;
};
FontFace font_faces[] = {
{"LatoLatin-Regular.ttf", false},
{"LatoLatin-Italic.ttf", false},
{"LatoLatin-Bold.ttf", false},
{"LatoLatin-BoldItalic.ttf", false},
{"NotoEmoji-Regular.ttf", true},
{"promptfont/promptfont.ttf", false},
};
for (const FontFace& face : font_faces) {
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
}
}
}
void load_documents() {
launcher_menu_controller->load_document(context);
config_menu_controller->load_document(context);
}
void unload() {
render_interface.reset();
}
void update_primary_input(bool mouse_moved, bool non_mouse_interacted) {
mouse_is_active_changed = false;
if (non_mouse_interacted) {
// controller newly interacted with
if (mouse_is_active) {
mouse_is_active = false;
mouse_is_active_changed = true;
}
}
else if (mouse_moved) {
// mouse newly interacted with
if (!mouse_is_active) {
mouse_is_active = true;
mouse_is_active_changed = true;
}
}
if (mouse_moved || non_mouse_interacted) {
mouse_is_active_initialized = true;
}
if (mouse_is_active_initialized) {
recompui::set_cursor_visible(mouse_is_active);
}
Rml::ElementDocument* current_document = top_input_document();
if (current_document == nullptr) {
return;
}
// TODO is this needed?
Rml::Element* window_el = current_document->GetElementById("window");
if (window_el != nullptr) {
if (mouse_is_active) {
if (!window_el->HasAttribute("mouse-active")) {
window_el->SetAttribute("mouse-active", true);
}
}
else if (window_el->HasAttribute("mouse-active")) {
window_el->RemoveAttribute("mouse-active");
}
}
}
void update_focus(bool mouse_moved, bool non_mouse_interacted) {
Rml::ElementDocument* current_document = top_input_document();
if (current_document == nullptr) {
return;
}
if (cont_is_active || non_mouse_interacted) {
if (non_mouse_interacted) {
auto focusedEl = current_document->GetFocusLeafNode();
if (focusedEl == nullptr || RecompRml::CanFocusElement(focusedEl) != RecompRml::CanFocus::Yes) {
Rml::Element* element = find_autofocus_element(current_document);
if (element != nullptr) {
element->Focus();
}
}
}
return;
}
// If there was mouse motion, get the current hovered element (or its target if it points to one) and focus that if applicable.
if (mouse_is_active) {
if (mouse_is_active_changed) {
Rml::Element* focused = current_document->GetFocusLeafNode();
if (focused) focused->Blur();
} else if (mouse_moved) {
Rml::Element* hovered = context->GetHoverElement();
if (hovered) {
Rml::Element* hover_target = get_target(current_document, hovered);
if (hover_target && can_focus(hover_target)) {
prev_focused = hover_target;
}
}
}
}
if (!mouse_is_active) {
if (!prev_focused || !can_focus(prev_focused)) {
// Find the autofocus element in the tab chain
Rml::Element* element = find_autofocus_element(current_document);
if (element && can_focus(element)) {
prev_focused = element;
}
}
if (mouse_is_active_changed && prev_focused && can_focus(prev_focused)) {
prev_focused->Focus();
}
}
}
void show_context(recompui::ContextId context) {
if (std::find_if(shown_contexts.begin(), shown_contexts.end(), [context](auto& c){ return c.context == context; }) != shown_contexts.end()) {
recompui::message_box("Attemped to show the same context twice");
assert(false);
}
bool takes_input = context.takes_input();
Rml::ElementDocument* document = context.get_document();
shown_contexts.push_back(ContextDetails{
.context = context,
.document = document,
.takes_input = takes_input
});
// auto& on_show = context.on_show;
// if (on_show) {
// context.open();
// on_show();
// context.close();
// }
document->PullToFront();
document->Show();
}
void hide_context(recompui::ContextId context) {
auto remove_it = std::remove_if(shown_contexts.begin(), shown_contexts.end(), [context](auto& c) { return c.context == context; });
if (remove_it == shown_contexts.end()) {
recompui::message_box("Attemped to hide a context that isn't shown");
assert(false);
}
shown_contexts.erase(remove_it, shown_contexts.end());
context.get_document()->Hide();
}
void hide_all_contexts() {
for (auto& context : shown_contexts) {
context.document->Hide();
}
shown_contexts.clear();
}
bool is_context_shown(recompui::ContextId context) {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [context](auto& c){ return c.context == context; }) != shown_contexts.end();
}
bool is_context_taking_input() {
return std::find_if(shown_contexts.begin(), shown_contexts.end(), [](auto& c){ return c.takes_input; }) != shown_contexts.end();
}
bool is_any_context_shown() {
return !shown_contexts.empty();
}
Rml::ElementDocument* top_input_document() {
// Iterate backwards and stop at the first context that takes input.
for (auto it = shown_contexts.rbegin(); it != shown_contexts.rend(); it++) {
if (it->takes_input) {
return it->document;
}
}
return nullptr;
}
void update_contexts() {
for (auto& context_details : shown_contexts) {
context_details.context.open();
context_details.context.process_updates();
context_details.context.close();
}
}
};
std::unique_ptr<UIState> ui_state;
std::recursive_mutex ui_state_mutex{};
// TODO make this not be global
extern SDL_Window* window;
void recompui::get_window_size(int& width, int& height) {
SDL_GetWindowSizeInPixels(window, &width, &height);
}
inline const std::string read_file_to_string(std::filesystem::path path) {
std::ifstream stream = std::ifstream{path};
std::ostringstream ss;
ss << stream.rdbuf();
return ss.str();
}
void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
#if defined(__linux__)
std::locale::global(std::locale::classic());
#endif
ui_state = std::make_unique<UIState>(window, interface, device);
ui_state->load_documents();
}
moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};
void recompui::queue_event(const SDL_Event& event) {
ui_event_queue.enqueue(event);
}
bool recompui::try_deque_event(SDL_Event& out) {
return ui_event_queue.try_dequeue(out);
}
int cont_button_to_key(SDL_ControllerButtonEvent& button) {
// Configurable accept button in menu
auto menuAcceptBinding0 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller);
auto menuAcceptBinding1 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller);
// note - magic number: 0 is InputType::None
if ((menuAcceptBinding0.input_type != 0 && button.button == menuAcceptBinding0.input_id) ||
(menuAcceptBinding1.input_type != 0 && button.button == menuAcceptBinding1.input_id)) {
return SDLK_RETURN;
}
// Configurable apply button in menu
auto menuApplyBinding0 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller);
auto menuApplyBinding1 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller);
// note - magic number: 0 is InputType::None
if ((menuApplyBinding0.input_type != 0 && button.button == menuApplyBinding0.input_id) ||
(menuApplyBinding1.input_type != 0 && button.button == menuApplyBinding1.input_id)) {
return SDLK_f;
}
// Allows closing the menu
auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
// note - magic number: 0 is InputType::None
if ((menuToggleBinding0.input_type != 0 && button.button == menuToggleBinding0.input_id) ||
(menuToggleBinding1.input_type != 0 && button.button == menuToggleBinding1.input_id)) {
return SDLK_ESCAPE;
}
switch (button.button) {
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP:
return SDLK_UP;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN:
return SDLK_DOWN;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT:
return SDLK_LEFT;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
return SDLK_RIGHT;
}
return 0;
}
int cont_axis_to_key(SDL_ControllerAxisEvent& axis, float value) {
switch (axis.axis) {
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY:
if (value < 0) return SDLK_UP;
return SDLK_DOWN;
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX:
if (value >= 0) return SDLK_RIGHT;
return SDLK_LEFT;
}
return 0;
}
void apply_background_input_mode() {
static recomp::BackgroundInputMode last_input_mode = recomp::BackgroundInputMode::OptionCount;
recomp::BackgroundInputMode cur_input_mode = recomp::get_background_input_mode();
if (last_input_mode != cur_input_mode) {
SDL_SetHint(
SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
cur_input_mode == recomp::BackgroundInputMode::On
? "1"
: "0"
);
}
last_input_mode = cur_input_mode;
}
bool recompui::get_cont_active() {
return ui_state->cont_is_active;
}
void recompui::set_cont_active(bool active) {
ui_state->cont_is_active = active;
}
void recompui::activate_mouse() {
ui_state->update_primary_input(true, false);
ui_state->update_focus(true, false);
}
void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* swap_chain_framebuffer) {
apply_background_input_mode();
// Return early if the ui context has been destroyed already.
if (!ui_state) {
return;
}
// Return to the launcher if no menu is open and the game isn't started.
if (!recompui::is_any_context_shown() && !ultramodern::is_game_started()) {
recompui::show_context(recompui::get_launcher_context_id(), "");
}
std::lock_guard lock{ ui_state_mutex };
SDL_Event cur_event{};
bool mouse_moved = false;
bool mouse_clicked = false;
bool non_mouse_interacted = false;
bool cont_interacted = false;
bool kb_interacted = false;
bool config_was_open = recompui::is_context_shown(recompui::get_config_context_id()) || recompui::is_context_shown(recompui::get_config_sub_menu_context_id());
while (recompui::try_deque_event(cur_event)) {
bool context_taking_input = recompui::is_context_taking_input();
if (!recomp::all_input_disabled()) {
// Implement some additional behavior for specific events on top of what RmlUi normally does with them.
switch (cur_event.type) {
case SDL_EventType::SDL_MOUSEMOTION: {
int *last_mouse_pos = ui_state->last_active_mouse_position;
if (!ui_state->mouse_is_active) {
float xD = cur_event.motion.x - last_mouse_pos[0];
float yD = cur_event.motion.y - last_mouse_pos[1];
if (sqrt(xD * xD + yD * yD) < 100) {
break;
}
}
last_mouse_pos[0] = cur_event.motion.x;
last_mouse_pos[1] = cur_event.motion.y;
// if controller is the primary input, don't use mouse movement to allow cursor to reactivate
if (recompui::get_cont_active()) {
break;
}
}
// fallthrough
case SDL_EventType::SDL_MOUSEBUTTONDOWN:
mouse_moved = true;
mouse_clicked = true;
break;
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
int rml_key = cont_button_to_key(cur_event.cbutton);
if (context_taking_input && rml_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
}
non_mouse_interacted = true;
cont_interacted = true;
break;
}
case SDL_EventType::SDL_KEYDOWN:
non_mouse_interacted = true;
kb_interacted = true;
break;
case SDL_EventType::SDL_USEREVENT:
if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
ui_state->await_stick_return_y = true;
} else if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
ui_state->await_stick_return_x = true;
}
break;
case SDL_EventType::SDL_CONTROLLERAXISMOTION:
SDL_ControllerAxisEvent* axis_event = &cur_event.caxis;
if (axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY && axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
break;
}
float axis_value = axis_event->value * (1 / 32768.0f);
bool* await_stick_return = axis_event->axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY
? &ui_state->await_stick_return_y
: &ui_state->await_stick_return_x;
if (fabsf(axis_value) > 0.5f) {
if (!*await_stick_return) {
*await_stick_return = true;
non_mouse_interacted = true;
int rml_key = cont_axis_to_key(cur_event.caxis, axis_value);
if (context_taking_input && rml_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
}
}
non_mouse_interacted = true;
cont_interacted = true;
}
else if (*await_stick_return && fabsf(axis_value) < 0.15f) {
*await_stick_return = false;
}
break;
}
if (context_taking_input) {
RmlSDL::InputEventHandler(ui_state->context, cur_event);
}
}
// If the config menu isn't open and the game has been started and either the escape key or select button are pressed, open the config menu.
if (!config_was_open && ultramodern::is_game_started()) {
bool open_config = false;
switch (cur_event.type) {
case SDL_EventType::SDL_KEYDOWN:
if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
open_config = true;
}
break;
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
// note - magic number: 0 is InputType::None
if ((menuToggleBinding0.input_type != 0 && cur_event.cbutton.button == menuToggleBinding0.input_id) ||
(menuToggleBinding1.input_type != 0 && cur_event.cbutton.button == menuToggleBinding1.input_id)) {
open_config = true;
}
break;
}
if (open_config) {
recompui::show_context(recompui::get_config_context_id(), "");
}
}
} // end dequeue event loop
if (cont_interacted || kb_interacted || mouse_clicked) {
recompui::set_cont_active(cont_interacted);
}
recomp::config_menu_set_cont_or_kb(ui_state->cont_is_active);
recomp::InputField scanned_field = recomp::get_scanned_input();
if (scanned_field != recomp::InputField{}) {
recomp::finish_scanning_input(scanned_field);
}
ui_state->update_primary_input(mouse_moved, non_mouse_interacted);
ui_state->update_focus(mouse_moved, non_mouse_interacted);
if (recompui::is_any_context_shown()) {
ui_state->update_contexts();
int width = swap_chain_framebuffer->getWidth();
int height = swap_chain_framebuffer->getHeight();
// Scale the UI based on the window size with 1080 vertical resolution as the reference point.
ui_state->context->SetDensityIndependentPixelRatio((height) / 1080.0f);
ui_state->render_interface.start(command_list, width, height);
static int prev_width = 0;
static int prev_height = 0;
if (prev_width != width || prev_height != height) {
ui_state->context->SetDimensions({ width, height });
}
prev_width = width;
prev_height = height;
ui_state->context->Update();
ui_state->context->Render();
ui_state->render_interface.end(command_list, swap_chain_framebuffer);
}
}
void deinit_hook() {
recompui::destroy_all_contexts();
std::lock_guard lock {ui_state_mutex};
Rml::Debugger::Shutdown();
Rml::Shutdown();
ui_state->unload();
ui_state.reset();
}
void recompui::set_render_hooks() {
RT64::SetRenderHooks(init_hook, draw_hook, deinit_hook);
}
void recompui::message_box(const char* msg) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, banjo::program_name.data(), msg, nullptr);
printf("[ERROR] %s\n", msg);
}
void recompui::show_context(ContextId context, std::string_view param) {
std::lock_guard lock{ui_state_mutex};
// TODO call the context's on_show callback with the param.
ui_state->show_context(context);
}
void recompui::hide_context(ContextId context) {
std::lock_guard lock{ui_state_mutex};
ui_state->hide_context(context);
}
void recompui::hide_all_contexts() {
std::lock_guard lock{ui_state_mutex};
if (ui_state) {
ui_state->hide_all_contexts();
}
}
bool recompui::is_context_shown(ContextId context) {
std::lock_guard lock{ui_state_mutex};
if (!ui_state) {
return false;
}
return ui_state->is_context_shown(context);
}
bool recompui::is_context_taking_input() {
std::lock_guard lock{ui_state_mutex};
if (!ui_state) {
return false;
}
return ui_state->is_context_taking_input();
}
bool recompui::is_any_context_shown() {
std::lock_guard lock{ui_state_mutex};
if (!ui_state) {
return false;
}
return ui_state->is_any_context_shown();
}
Rml::ElementDocument* recompui::load_document(const std::filesystem::path& path) {
std::lock_guard lock{ui_state_mutex};
return ui_state->context->LoadDocument(path.string());
}
Rml::ElementDocument* recompui::create_empty_document() {
std::lock_guard lock{ui_state_mutex};
return ui_state->context->CreateDocument();
}
void recompui::queue_image_from_bytes(const std::string &src, const std::vector<char> &bytes) {
ui_state->render_interface.queue_image_from_bytes(src, bytes);
}
void recompui::release_image(const std::string &src) {
Rml::ReleaseTexture(src);
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef RECOMPUI_ELEMENTS_BEM
#define RECOMPUI_ELEMENTS_BEM
#include <string>
#define EL(bem, element) bem "__" element
#define EL_DYN(bem, element) bem "__" + element
#define MOD(bem, modifier) bem "--" modifier
#define MOD_DYN(bem, modifier) bem "--" + modifier
#define BLOCK(bem) bem
#endif
+145
View File
@@ -0,0 +1,145 @@
#include "hsv.h"
#include <algorithm> // std::min, std::max and std::clamp
#include <math.h>
namespace recompui {
void HsvToRgb(HsvColor& hsv, RgbColor& rgb)
{
unsigned char region, remainder, p, q, t;
if (hsv.s == 0)
{
rgb.r = hsv.v;
rgb.g = hsv.v;
rgb.b = hsv.v;
return;
}
region = hsv.h / 43;
remainder = (hsv.h - (region * 43)) * 6;
p = (hsv.v * (255 - hsv.s)) >> 8;
q = (hsv.v * (255 - ((hsv.s * remainder) >> 8))) >> 8;
t = (hsv.v * (255 - ((hsv.s * (255 - remainder)) >> 8))) >> 8;
switch (region)
{
case 0:
rgb.r = hsv.v; rgb.g = t; rgb.b = p;
break;
case 1:
rgb.r = q; rgb.g = hsv.v; rgb.b = p;
break;
case 2:
rgb.r = p; rgb.g = hsv.v; rgb.b = t;
break;
case 3:
rgb.r = p; rgb.g = q; rgb.b = hsv.v;
break;
case 4:
rgb.r = t; rgb.g = p; rgb.b = hsv.v;
break;
default:
rgb.r = hsv.v; rgb.g = p; rgb.b = q;
break;
}
}
void RgbToHsv(RgbColor& rgb, HsvColor& hsv)
{
unsigned char rgbMin, rgbMax;
rgbMin = rgb.r < rgb.g ? (rgb.r < rgb.b ? rgb.r : rgb.b) : (rgb.g < rgb.b ? rgb.g : rgb.b);
rgbMax = rgb.r > rgb.g ? (rgb.r > rgb.b ? rgb.r : rgb.b) : (rgb.g > rgb.b ? rgb.g : rgb.b);
hsv.v = rgbMax;
if (hsv.v == 0)
{
hsv.h = 0;
hsv.s = 0;
return;
}
hsv.s = 255 * long(rgbMax - rgbMin) / hsv.v;
if (hsv.s == 0)
{
hsv.h = 0;
return;
}
if (rgbMax == rgb.r)
hsv.h = 0 + 43 * (rgb.g - rgb.b) / (rgbMax - rgbMin);
else if (rgbMax == rgb.g)
hsv.h = 85 + 43 * (rgb.b - rgb.r) / (rgbMax - rgbMin);
else
hsv.h = 171 + 43 * (rgb.r - rgb.g) / (rgbMax - rgbMin);
}
static unsigned char clamp_255(float f) {
return std::clamp((int)round(f), 0, 255);
}
void HsvFToRgb(HsvColorF in, RgbColor& out)
{
float hh, p, q, t, ff;
long i;
unsigned char val = clamp_255(in.v * 255.0f);
if (in.s <= 0.0) {
out.r = val;
out.g = val;
out.b = val;
return;
}
hh = in.h;
if (hh >= 360.0f) hh = 0.0f;
hh /= 60.0f;
i = (float)hh;
ff = hh - i;
p = in.v * (1.0f - in.s);
q = in.v * (1.0f - (in.s * ff));
t = in.v * (1.0f - (in.s * (1.0f - ff)));
unsigned char up = clamp_255(p * 255.0f);
unsigned char uq = clamp_255(q * 255.0f);
unsigned char ut = clamp_255(t * 255.0f);
switch (i) {
case 0:
out.r = val;
out.g = ut;
out.b = up;
return;
case 1:
out.r = uq;
out.g = val;
out.b = up;
return;
case 2:
out.r = up;
out.g = val;
out.b = ut;
return;
case 3:
out.r = up;
out.g = uq;
out.b = val;
return;
case 4:
out.r = ut;
out.g = up;
out.b = val;
return;
case 5:
default:
out.r = val;
out.g = up;
out.b = uq;
return;
}
}
}
+77
View File
@@ -0,0 +1,77 @@
#ifndef RECOMP_UI_HSV
#define RECOMP_UI_HSV
namespace recompui {
typedef struct RgbColor
{
union {
struct {
unsigned char r;
unsigned char g;
unsigned char b;
};
unsigned char data[3]; // Array access
};
// Operator[] to access members by index
unsigned char& operator[](int index) {
return data[index];
}
// Const version for read-only access
const unsigned char& operator[](int index) const {
return data[index];
}
} RgbColor;
typedef struct HsvColor
{
union {
struct {
unsigned char h;
unsigned char s;
unsigned char v;
};
unsigned char data[3]; // Array access
};
// Operator[] to access members by index
unsigned char& operator[](int index) {
return data[index];
}
// Const version for read-only access
const unsigned char& operator[](int index) const {
return data[index];
}
} HsvColor;
typedef struct HsvColorF
{
union {
struct {
float h; // 0-360
float s; // 0-1
float v; // 0-1
};
float data[3]; // Array access
};
// Operator[] to access members by index
float& operator[](int index) {
return data[index];
}
// Const version for read-only access
const float& operator[](int index) const {
return data[index];
}
} HsvColorF;
void HsvToRgb(HsvColor& hsv, RgbColor& rgb);
void HsvFToRgb(HsvColorF in, RgbColor& out);
void RgbToHsv(RgbColor& rgb, HsvColor& hsv);
}
#endif