mirror of
https://github.com/BanjoRecomp/BanjoRecomp
synced 2026-09-01 09:12:52 -04:00
Initial boot
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#include <atomic>
|
||||
#include "banjo_debug.h"
|
||||
#include "librecomp/helpers.hpp"
|
||||
#include "../patches/input.h"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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{};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¶m, Rml::Event &event) {
|
||||
recompui::set_config_tab(recompui::ConfigTab::Mods);
|
||||
recompui::hide_all_contexts();
|
||||
recompui::show_context(recompui::get_config_context_id(), "");
|
||||
}
|
||||
);
|
||||
recompui::register_event(listener, "exit_game",
|
||||
[](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>();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user