changes up to swapping to recompfrontend library

This commit is contained in:
thecozies
2025-09-09 08:23:28 -05:00
parent dc687ab318
commit 1b2b40bc6e
72 changed files with 3756 additions and 249 deletions
+11 -1
View File
@@ -169,8 +169,9 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/ui/ui_launcher.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_assign_players_modal.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_color_element.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config_modal.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config_page_example.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config_page_controls.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_config_page_controls_element.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_prompt.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_player_card.cpp
@@ -186,6 +187,12 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/ui/ui_api_images.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_utils.cpp
${CMAKE_SOURCE_DIR}/src/ui/util/hsv.cpp
${CMAKE_SOURCE_DIR}/src/ui/config/ui_config_page_options_menu.cpp
${CMAKE_SOURCE_DIR}/src/ui/config/ui_config_page_controls.cpp
${CMAKE_SOURCE_DIR}/src/ui/config/ui_config_tab_controls.cpp
${CMAKE_SOURCE_DIR}/src/ui/config/ui_config_tab_graphics.cpp
${CMAKE_SOURCE_DIR}/src/ui/config/ui_config_tab_manifest.cpp
${CMAKE_SOURCE_DIR}/src/ui/config/ui_config_option.cpp
${CMAKE_SOURCE_DIR}/src/ui/core/ui_context.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_binding_button.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp
@@ -196,7 +203,9 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_element.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_image.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_label.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_modal.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_pill_button.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_pseudo_border.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_radio.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_scroll_container.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_select.cpp
@@ -204,6 +213,7 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_span.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_style.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_svg.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_tab_set.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_text_input.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_theme.cpp
${CMAKE_SOURCE_DIR}/src/ui/elements/ui_toggle.cpp
+5
View File
@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00008 11C9.00008 6.5 12.0002 4 16.0002 4C20.0002 4 23.0002 7 23.0002 11C23.0002 15 20.0002 18 16.0002 18" stroke="white" stroke-width="4" stroke-linecap="round"/>
<path d="M16 22V18" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="16" cy="28" r="2" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

+3 -1
View File
@@ -13,7 +13,9 @@ namespace banjo {
// TODO: Move loading configs to the runtime once we have a way to allow per-project customization.
void load_config();
void save_config();
bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& json_out);
bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data);
void reset_input_bindings();
void reset_cont_input_bindings(int profile_index);
void reset_kb_input_bindings(int profile_index);
+2 -1
View File
@@ -6,6 +6,7 @@
#include "common/rt64_user_configuration.h"
#include "ultramodern/renderer_context.hpp"
#include "librecomp/config.hpp"
#include "librecomp/mods.hpp"
namespace RT64 {
@@ -54,7 +55,7 @@ namespace banjo {
// Texture pack enable option. Must be an enum with two options.
// The first option is treated as disabled and the second option is treated as enabled.
bool is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors);
bool is_texture_pack_enable_config_option(const recomp::config::ConfigOption& option, bool show_errors);
}
}
+10 -1
View File
@@ -45,7 +45,10 @@ namespace recomp {
#define DEFINE_RECOMP_UI_INPUTS() \
DEFINE_INPUT(TOGGLE_MENU, 0, "Toggle Menu") \
DEFINE_INPUT(ACCEPT_MENU, 0, "Accept (Menu)") \
DEFINE_INPUT(APPLY_MENU, 0, "Apply (Menu)")
DEFINE_INPUT(BACK_MENU, 0, "Back (Menu)") \
DEFINE_INPUT(APPLY_MENU, 0, "Apply (Menu)") \
DEFINE_INPUT(TAB_LEFT_MENU, 0, "Tab Left (Menu)") \
DEFINE_INPUT(TAB_RIGHT_MENU, 0, "Tab Right (Menu)")
#define DEFINE_ALL_INPUTS() \
DEFINE_N64_BUTTON_INPUTS() \
@@ -130,7 +133,10 @@ namespace recomp {
std::vector<InputField> toggle_menu;
std::vector<InputField> accept_menu;
std::vector<InputField> back_menu;
std::vector<InputField> apply_menu;
std::vector<InputField> tab_left_menu;
std::vector<InputField> tab_right_menu;
};
inline const std::vector<InputField>& get_default_mapping_for_input(const DefaultN64Mappings& defaults, const GameInput input) {
@@ -156,7 +162,10 @@ namespace recomp {
case GameInput::Y_AXIS_NEG: return defaults.analog_down;
case GameInput::TOGGLE_MENU: return defaults.toggle_menu;
case GameInput::ACCEPT_MENU: return defaults.accept_menu;
case GameInput::BACK_MENU: return defaults.back_menu;
case GameInput::APPLY_MENU: return defaults.apply_menu;
case GameInput::TAB_LEFT_MENU: return defaults.tab_left_menu;
case GameInput::TAB_RIGHT_MENU: return defaults.tab_right_menu;
default: return empty_input_field;
}
}
+40 -3
View File
@@ -10,11 +10,13 @@
#include "SDL.h"
#include "RmlUi/Core.h"
#include "recomp_input.h"
#include "../src/ui/util/hsv.h"
#include "../src/ui/util/bem.h"
#include "../src/ui/elements/ui_button.h"
#include "../src/ui/elements/ui_theme.h"
#include "../src/ui/elements/ui_types.h"
#include "../src/ui/core/ui_context.h"
@@ -61,7 +63,7 @@ namespace recompui {
ContextId get_config_context_id();
ContextId get_config_sub_menu_context_id();
enum class ConfigTab {
enum class ConfigTabId {
General,
Controls,
Graphics,
@@ -70,8 +72,8 @@ namespace recompui {
Debug,
};
void set_config_tab(ConfigTab tab);
int config_tab_to_index(ConfigTab tab);
void set_config_tab(ConfigTabId tab);
int config_tab_to_index(ConfigTabId tab);
Rml::ElementTabSet* get_config_tabset();
Rml::Element* get_mod_tab();
void set_config_tabset_mod_nav();
@@ -133,6 +135,41 @@ namespace recompui {
void release_image(const std::string &src);
void drop_files(const std::list<std::filesystem::path> &file_list);
void report_removed_element(Rml::Element* element);
namespace menu_action_mapping {
struct key_map {
// End result menu action
const MenuAction action;
// Mapped input from controller
const recomp::GameInput input;
// SDL key code from controller -> keyboard
const int sdl;
// RML key identifier passed to rmlui
const Rml::Input::KeyIdentifier rml;
};
static const key_map accept = { MenuAction::Accept, recomp::GameInput::ACCEPT_MENU, SDLK_RETURN, Rml::Input::KI_RETURN };
static const key_map apply = { MenuAction::Apply, recomp::GameInput::APPLY_MENU, SDLK_f, Rml::Input::KI_F };
static const key_map back = { MenuAction::Back, recomp::GameInput::BACK_MENU, SDLK_F15, Rml::Input::KI_F15 };
static const key_map toggle = { MenuAction::Toggle, recomp::GameInput::TOGGLE_MENU, SDLK_ESCAPE, Rml::Input::KI_ESCAPE };
static const key_map tab_left = { MenuAction::TabLeft, recomp::GameInput::TAB_LEFT_MENU, SDLK_F16, Rml::Input::KI_F16 };
static const key_map tab_right = { MenuAction::TabRight, recomp::GameInput::TAB_RIGHT_MENU, SDLK_F17, Rml::Input::KI_F17 };
static const std::unordered_map<Rml::Input::KeyIdentifier, key_map> rml_key_to_action {
{ accept.rml, accept },
{ apply.rml, apply },
{ back.rml, back },
{ toggle.rml, toggle },
{ tab_left.rml, tab_left },
{ tab_right.rml, tab_right }
};
MenuAction menu_action_from_rml_key(const Rml::Input::KeyIdentifier& key);
};
// Constant that represents an input that doesn't have a promptfont icon associated with it.
extern const std::string unknown_input;
}
#endif
+18 -1
View File
@@ -1,7 +1,7 @@
#ifndef __UI_FUNCS_H__
#define __UI_FUNCS_H__
// These two enums must be kept in sync with src/ui/elements/ui_types.h!
// These three enums must be kept in sync with src/ui/elements/ui_types.h!
typedef enum {
UI_EVENT_NONE,
UI_EVENT_CLICK,
@@ -11,6 +11,9 @@ typedef enum {
UI_EVENT_DRAG,
UI_EVENT_RESERVED1, // Would be UI_EVENT_TEXT but text events aren't usable in mods currently
UI_EVENT_UPDATE,
UI_EVENT_NAVIGATE,
UI_EVENT_MOUSE_BUTTON,
UI_EVENT_MENU_ACTION,
UI_EVENT_COUNT
} RecompuiEventType;
@@ -21,6 +24,16 @@ typedef enum {
UI_DRAG_END
} RecompuiDragPhase;
typedef enum {
UI_MENU_ACTION_NONE,
UI_MENU_ACTION_ACCEPT,
UI_MENU_ACTION_APPLY,
UI_MENU_ACTION_BACK,
UI_MENU_ACTION_TOGGLE,
UI_MENU_ACTION_TAB_LEFT,
UI_MENU_ACTION_TAB_RIGHT
} RecompuiMenuAction;
typedef struct {
RecompuiEventType type;
union {
@@ -46,6 +59,10 @@ typedef struct {
float y;
RecompuiDragPhase phase;
} drag;
struct {
RecompuiMenuAction action;
} menu_action;
} data;
} RecompuiEventData;
+13 -10
View File
@@ -213,7 +213,7 @@ bool read_json(std::ifstream input_file, nlohmann::json& json_out) {
return true;
}
bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& json_out) {
bool banjo::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;
@@ -228,7 +228,7 @@ bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& j
return false;
}
bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) {
bool banjo::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()) {
@@ -253,7 +253,7 @@ bool save_general_config(const std::filesystem::path& path) {
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);
return banjo::save_json_with_backups(path, config_json);
}
void set_general_settings_from_json(const nlohmann::json& config_json) {
@@ -270,7 +270,7 @@ void set_general_settings_from_json(const nlohmann::json& config_json) {
bool load_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
if (!banjo::read_json_with_backups(path, config_json)) {
return false;
}
@@ -318,7 +318,10 @@ void assign_all_mappings(int profile_index, const recomp::DefaultN64Mappings& va
assign_mapping_complete(profile_index, recomp::GameInput::TOGGLE_MENU, values.toggle_menu);
assign_mapping_complete(profile_index, recomp::GameInput::ACCEPT_MENU, values.accept_menu);
assign_mapping_complete(profile_index, recomp::GameInput::BACK_MENU, values.back_menu);
assign_mapping_complete(profile_index, recomp::GameInput::APPLY_MENU, values.apply_menu);
assign_mapping_complete(profile_index, recomp::GameInput::TAB_LEFT_MENU, values.tab_left_menu);
assign_mapping_complete(profile_index, recomp::GameInput::TAB_RIGHT_MENU, values.tab_right_menu);
};
void banjo::reset_input_bindings() {
@@ -365,12 +368,12 @@ void reset_graphics_options() {
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);
return banjo::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)) {
if (!banjo::read_json_with_backups(path, config_json)) {
return false;
}
@@ -420,7 +423,7 @@ bool save_controls_config(const std::filesystem::path& path) {
controller["profile"] = recomp::get_input_profile_key(recomp::get_controller_profile_index(i));
}
return save_json_with_backups(path, config_json);
return banjo::save_json_with_backups(path, config_json);
}
bool load_input_device_from_json(const nlohmann::json& config_json, int profile_index, recomp::InputDevice device, const std::string& key) {
@@ -466,7 +469,7 @@ bool load_input_device_from_json(const nlohmann::json& config_json, int profile_
bool load_controls_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
if (!read_json_with_backups(path, config_json)) {
if (!banjo::read_json_with_backups(path, config_json)) {
return false;
}
@@ -526,12 +529,12 @@ bool save_sound_config(const std::filesystem::path& path) {
config_json["main_volume"] = banjo::get_main_volume();
config_json["bgm_volume"] = banjo::get_bgm_volume();
return save_json_with_backups(path, config_json);
return banjo::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)) {
if (!banjo::read_json_with_backups(path, config_json)) {
return false;
}
+3
View File
@@ -389,7 +389,10 @@ void clear_all_mappings(int profile_index) {
clear_mapping(profile_index, recomp::GameInput::TOGGLE_MENU);
clear_mapping(profile_index, recomp::GameInput::ACCEPT_MENU);
clear_mapping(profile_index, recomp::GameInput::BACK_MENU);
clear_mapping(profile_index, recomp::GameInput::APPLY_MENU);
clear_mapping(profile_index, recomp::GameInput::TAB_LEFT_MENU);
clear_mapping(profile_index, recomp::GameInput::TAB_RIGHT_MENU);
};
void recomp::initialize_input_bindings() {
+46 -14
View File
@@ -601,8 +601,17 @@ const recomp::DefaultN64Mappings recomp::default_n64_keyboard_mappings = {
.accept_menu = {
{.input_type = InputType::Keyboard, .input_id = SDL_SCANCODE_RETURN}
},
.back_menu = {
{.input_type = InputType::Keyboard, .input_id = SDL_SCANCODE_F15}
},
.apply_menu = {
{.input_type = InputType::Keyboard, .input_id = SDL_SCANCODE_F}
},
.tab_left_menu = {
{.input_type = InputType::Keyboard, .input_id = SDL_SCANCODE_F16}
},
.tab_right_menu = {
{.input_type = InputType::Keyboard, .input_id = SDL_SCANCODE_F17}
}
};
@@ -671,9 +680,18 @@ const recomp::DefaultN64Mappings recomp::default_n64_controller_mappings = {
.accept_menu = {
{.input_type = InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_SOUTH},
},
.apply_menu = {
.back_menu = {
{.input_type = InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_WEST},
},
.apply_menu = {
{.input_type = InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_NORTH},
{.input_type = InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_START}
},
.tab_left_menu = {
{.input_type = InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_LEFTSHOULDER}
},
.tab_right_menu = {
{.input_type = InputType::ControllerDigital, .input_id = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER}
}
};
@@ -976,6 +994,8 @@ void recomp::set_single_controller_mode(bool single_controller) {
InputState.single_controller = single_controller;
}
const std::string recompui::unknown_input = "UNKNOWN";
std::string controller_button_to_string(SDL_GameControllerButton button) {
switch (button) {
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_A:
@@ -1008,20 +1028,20 @@ std::string controller_button_to_string(SDL_GameControllerButton button) {
return PF_DPAD_LEFT;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
return PF_DPAD_RIGHT;
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
// return "";
// case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
// return "";
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MISC1:
return PF_STEAM_OPTIONS;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE1:
return PF_GAMEPAD_L4;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE2:
return PF_GAMEPAD_R4;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE3:
return PF_GAMEPAD_L5;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_PADDLE4:
return PF_GAMEPAD_R5;
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD:
return PF_SONY_TOUCHPAD;
default:
return "Button " + std::to_string(button);
return recompui::unknown_input;
}
}
@@ -1080,6 +1100,18 @@ std::unordered_map<SDL_Scancode, std::string> scancode_codepoints{
{SDL_SCANCODE_F10, PF_KEYBOARD_F10},
{SDL_SCANCODE_F11, PF_KEYBOARD_F11},
{SDL_SCANCODE_F12, PF_KEYBOARD_F12},
{SDL_SCANCODE_F13, "F13"},
{SDL_SCANCODE_F14, "F14"},
{SDL_SCANCODE_F15, "F15"},
{SDL_SCANCODE_F16, "F16"},
{SDL_SCANCODE_F17, "F17"},
{SDL_SCANCODE_F18, "F18"},
{SDL_SCANCODE_F19, "F19"},
{SDL_SCANCODE_F20, "F20"},
{SDL_SCANCODE_F21, "F21"},
{SDL_SCANCODE_F22, "F22"},
{SDL_SCANCODE_F23, "F23"},
{SDL_SCANCODE_F24, "F24"},
{SDL_SCANCODE_PRINTSCREEN, PF_KEYBOARD_PRINT_SCREEN},
{SDL_SCANCODE_SCROLLLOCK, PF_KEYBOARD_SCROLL_LOCK},
{SDL_SCANCODE_PAUSE, PF_KEYBOARD_PAUSE},
@@ -1103,7 +1135,7 @@ 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);
return recompui::unknown_input;
}
std::string controller_axis_to_string(int axis) {
@@ -1138,6 +1170,6 @@ std::string recomp::InputField::to_string() const {
case InputType::Keyboard:
return keyboard_input_to_string((SDL_Scancode)input_id);
default:
return std::to_string((uint32_t)input_type) + "," + std::to_string(input_id);
return recompui::unknown_input;
}
}
+9 -6
View File
@@ -14,6 +14,7 @@
#include "banjo_render.h"
#include "recomp_ui.h"
#include "concurrentqueue.h"
#include "../ui/config/ui_config_tab_graphics.h"
static RT64::UserConfiguration::Antialiasing device_max_msaa = RT64::UserConfiguration::Antialiasing::None;
static bool sample_positions_supported = false;
@@ -326,6 +327,8 @@ banjo::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::renderer:
sample_positions_supported = false;
}
recompui::update_msaa_supported(sample_positions_supported);
high_precision_fb_enabled = app->shaderLibrary->usesHDR;
}
@@ -482,13 +485,13 @@ void banjo::renderer::enable_texture_pack(const recomp::mods::ModContext& contex
texture_pack_action_queue.enqueue(TexturePackEnableAction{mod.manifest.mod_id});
// Check for the texture pack enabled config option.
const recomp::mods::ConfigSchema& config_schema = context.get_mod_config_schema(mod.manifest.mod_id);
const recomp::config::ConfigSchema& config_schema = context.get_mod_config_schema(mod.manifest.mod_id);
auto find_it = config_schema.options_by_id.find(banjo::renderer::special_option_texture_pack_enabled);
if (find_it != config_schema.options_by_id.end()) {
const recomp::mods::ConfigOption& config_option = config_schema.options[find_it->second];
const recomp::config::ConfigOption& config_option = config_schema.options[find_it->second];
if (is_texture_pack_enable_config_option(config_option, false)) {
recomp::mods::ConfigValueVariant value_variant = context.get_mod_config_value(mod.manifest.mod_id, config_option.id);
recomp::config::ConfigValueVariant value_variant = context.get_mod_config_value(mod.manifest.mod_id, config_option.id);
uint32_t value;
if (uint32_t* value_ptr = std::get_if<uint32_t>(&value_variant)) {
value = *value_ptr;
@@ -522,16 +525,16 @@ void banjo::renderer::secondary_disable_texture_pack(const std::string& mod_id)
// HD texture enable option. Must be an enum with two options.
// The first option is treated as disabled and the second option is treated as enabled.
bool banjo::renderer::is_texture_pack_enable_config_option(const recomp::mods::ConfigOption& option, bool show_errors) {
bool banjo::renderer::is_texture_pack_enable_config_option(const recomp::config::ConfigOption& option, bool show_errors) {
if (option.id == banjo::renderer::special_option_texture_pack_enabled) {
if (option.type != recomp::mods::ConfigOptionType::Enum) {
if (option.type != recomp::config::ConfigOptionType::Enum) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + banjo::renderer::special_option_texture_pack_enabled + "\"), but the config option is not an enum.").c_str());
}
return false;
}
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
const recomp::config::ConfigOptionEnum &option_enum = std::get<recomp::config::ConfigOptionEnum>(option.variant);
if (option_enum.options.size() != 2) {
if (show_errors) {
recompui::message_box(("Mod has the special config option id for enabling an HD texture pack (\"" + banjo::renderer::special_option_texture_pack_enabled + "\"), but the config option doesn't have exactly 2 values.").c_str());
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include "librecomp/config.hpp"
#include "librecomp/mods.hpp"
namespace recompui {
// Callbacks for render updates relating to config option changes.
using get_config_option_updates_callback = std::function<std::vector<recomp::config::ConfigOptionUpdateContext>()>;
using clear_config_option_updates_callback = std::function<void()>;
using check_page_dirty_callback = std::function<bool()>;
using set_option_value_callback = std::function<void(const std::string &option_id, recomp::config::ConfigValueVariant value)>;
// Callbacks for various config option properties
using get_option_value_callback = std::function<const recomp::config::ConfigValueVariant(const std::string &option_id)>;
using get_option_name_callback = std::function<std::string(const std::string &option_id)>;
using get_option_description_callback = std::function<std::string(const std::string &option_id)>;
using get_option_disabled_callback = std::function<bool(const std::string &option_id)>;
using get_option_hidden_callback = std::function<bool(const std::string &option_id)>;
using get_option_configuration_callback = std::function<recomp::config::ConfigOptionVariant(const std::string &option_id)>;
// Text that displays on the right side of the options
using get_enum_option_details_callback = std::function<std::string(const std::string &option_id)>;
// Fetches if an individual enum option is disabled
using get_enum_option_disabled_callback = std::function<bool(const std::string &option_id, uint32_t enum_index)>;
using on_option_hover_callback = std::function<void(const std::string &option_id)>;
class ConfigOptionPropertyCallbacks {
// private:
// const get_option_configuration_callback get_configuration;
public:
const get_option_configuration_callback get_configuration;
const get_option_value_callback get_value;
const get_option_name_callback get_name;
const get_option_description_callback get_description;
const get_option_hidden_callback get_hidden;
const get_option_disabled_callback get_disabled = nullptr;
const get_enum_option_details_callback get_enum_option_details = nullptr;
const get_enum_option_disabled_callback get_enum_option_disabled = nullptr;
ConfigOptionPropertyCallbacks(
get_option_configuration_callback get_configuration,
get_option_value_callback get_value,
get_option_name_callback get_name,
get_option_description_callback get_description,
get_option_hidden_callback get_hidden,
get_option_disabled_callback get_disabled = nullptr,
get_enum_option_details_callback get_enum_option_details = nullptr,
get_enum_option_disabled_callback get_enum_option_disabled = nullptr
) :
get_configuration(get_configuration),
get_value(get_value),
get_name(get_name),
get_description(get_description),
get_hidden(get_hidden),
get_disabled(get_disabled),
get_enum_option_details(get_enum_option_details),
get_enum_option_disabled(get_enum_option_disabled) {}
ConfigOptionPropertyCallbacks(const ConfigOptionPropertyCallbacks&) = default;
recomp::config::ConfigOptionEnum get_enum_configuration(const std::string &option_id) {
return std::get<recomp::config::ConfigOptionEnum>(get_configuration(option_id));
}
recomp::config::ConfigOptionNumber get_number_configuration(const std::string &option_id) {
return std::get<recomp::config::ConfigOptionNumber>(get_configuration(option_id));
}
recomp::config::ConfigOptionString get_string_configuration(const std::string &option_id) {
return std::get<recomp::config::ConfigOptionString>(get_configuration(option_id));
}
};
};
+293
View File
@@ -0,0 +1,293 @@
#include "ui_config_option.h"
namespace recompui {
constexpr float config_option_element_margin_vertical = 12.0f;
NovaConfigOptionElement::NovaConfigOptionElement(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
) :
Element(parent, Events(EventType::Hover, EventType::Focus), "div", false),
option_id(option_id),
option_index(option_index),
callbacks(callbacks),
set_option_value(set_option_value),
on_hover(on_hover)
{
set_display(Display::Flex);
set_position(Position::Relative);
set_flex_direction(FlexDirection::Column);
set_align_items(AlignItems::FlexStart);
set_justify_content(JustifyContent::FlexStart);
set_padding_top(config_option_element_margin_vertical);
set_padding_left(12.0f);
set_padding_right(12.0f);
set_padding_bottom(config_option_element_margin_vertical);
set_height_auto();
set_width(100.0f, Unit::Percent);
set_as_navigation_container(NavigationType::Auto);
ContextId context = recompui::get_current_context();
auto name_text = callbacks->get_name(option_id);
if (!name_text.empty()) {
name_label = context.create_element<Label>(this, name_text, theme::Typography::LabelMD);
name_label->set_margin_bottom(8.0f);
} else {
name_label = context.create_element<Label>(this, "", theme::Typography::LabelMD);
set_padding_top(0.0f);
}
update_hidden();
}
void NovaConfigOptionElement::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover: {
bool active = std::get<EventHover>(e.variant).active;
if (active) {
on_hover(option_id);
}
break;
}
case EventType::Focus: {
bool active = std::get<EventFocus>(e.variant).active;
if (active) {
on_hover(option_id);
scroll_into_view();
}
break;
}
case EventType::Update:
break;
default:
assert(false && "Unknown event type.");
break;
}
}
void NovaConfigOptionElement::update_hidden() {
if (callbacks->get_hidden(option_id)) {
set_display(Display::None);
set_enabled(false);
} else {
set_display(Display::Flex);
set_enabled(true);
}
}
// NovaConfigOptionEnum
NovaConfigOptionEnum::NovaConfigOptionEnum(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
) : NovaConfigOptionElement(parent, option_id, option_index, callbacks, set_option_value, on_hover)
{
ContextId context = recompui::get_current_context();
Element *wrapper = context.create_element<Element>(this, 0, "div", false);
wrapper->set_display(Display::Flex);
wrapper->set_flex_direction(FlexDirection::Row);
wrapper->set_align_items(AlignItems::Center);
wrapper->set_justify_content(JustifyContent::FlexStart);
wrapper->set_width(100.0f, Unit::Percent);
wrapper->set_gap(12.0f);
// Negates the extra padding that radio tabs automatically add.
name_label->set_margin_bottom(4.0f);
radio = context.create_element<Radio>(wrapper);
radio->set_focus_callback([this](bool active) {
if (active) {
this->on_hover(this->option_id);
}
});
radio->add_index_changed_callback([this](uint32_t index) {
auto enum_opt = this->callbacks->get_enum_configuration(this->option_id);
if (index < enum_opt.options.size()) {
this->set_option_value(this->option_id, enum_opt.options[index].value);
}
});
details_label = context.create_element<Label>(wrapper, "", theme::Typography::LabelXS);
details_label->set_color(theme::color::Primary);
auto enum_opt = callbacks->get_enum_configuration(option_id);
for (uint32_t i = 0; i < enum_opt.options.size(); i++) {
radio->add_option(enum_opt.options[i].name);
}
update_value();
update_enum_details();
update_enum_disabled();
// enable_focus();
};
void NovaConfigOptionEnum::update_value() {
recomp::config::ConfigValueVariant value_variant = callbacks->get_value(option_id);
uint32_t option_value = std::get<uint32_t>(value_variant);
auto enum_opt = callbacks->get_enum_configuration(option_id);
for (uint32_t i = 0; i < enum_opt.options.size(); i++) {
if (enum_opt.options[i].value == option_value) {
radio->set_index(i);
return;
}
}
// No matching option, set invalid index so no option is selected
radio->set_index(enum_opt.options.size());
};
void NovaConfigOptionEnum::update_disabled() {
update_enum_disabled();
};
void NovaConfigOptionEnum::update_enum_details() {
const std::string &enum_details = callbacks->get_enum_option_details(option_id);
if (enum_details.empty()) {
details_label->set_text("");
} else {
details_label->set_text(enum_details);
}
};
void NovaConfigOptionEnum::update_enum_disabled() {
bool all_disabled = callbacks->get_disabled(option_id);
auto enum_opt = callbacks->get_enum_configuration(option_id);
for (uint32_t i = 0; i < enum_opt.options.size(); i++) {
bool enum_disabled = all_disabled || callbacks->get_enum_option_disabled(option_id, i);
auto opt_element = radio->get_option_element(i);
opt_element->set_enabled(!enum_disabled);
}
};
// NovaConfigOptionNumber
NovaConfigOptionNumber::NovaConfigOptionNumber(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
) : NovaConfigOptionElement(parent, option_id, option_index, callbacks, set_option_value, on_hover)
{
ContextId context = recompui::get_current_context();
auto num_opt = callbacks->get_number_configuration(option_id);
slider = context.create_element<Slider>(this, num_opt.percent ? SliderType::Percent : SliderType::Double);
slider->set_width(100.0f, Unit::Percent);
slider->set_max_width(100.0f, Unit::Percent);
slider->set_min_value(num_opt.min);
slider->set_max_value(num_opt.max);
slider->set_step_value(num_opt.step);
slider->set_precision(num_opt.precision);
slider->add_value_changed_callback([this](double v){
this->set_option_value(this->option_id, v);
});
slider->set_focus_callback([this](bool active) {
if (active) {
this->on_hover(this->option_id);
}
});
update_value();
// enable_focus();
update_disabled();
};
void NovaConfigOptionNumber::update_value() {
recomp::config::ConfigValueVariant value_variant = callbacks->get_value(option_id);
double value = std::get<double>(value_variant);
slider->set_value(value);
};
void NovaConfigOptionNumber::update_disabled() {
bool disabled = callbacks->get_disabled(option_id);
set_enabled(!disabled);
};
// NovaConfigOptionString
NovaConfigOptionString::NovaConfigOptionString(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
) : NovaConfigOptionElement(parent, option_id, option_index, callbacks, set_option_value, on_hover)
{
// Negates the extra padding that text inputs automatically add.
name_label->set_margin_bottom(4.0f);
ContextId context = recompui::get_current_context();
text_input = context.create_element<TextInput>(this);
text_input->set_width(100.0f, Unit::Percent);
text_input->set_max_width(100.0f, Unit::Percent);
update_value();
text_input->add_text_changed_callback([this](const std::string &text){
this->set_option_value(this->option_id, text);
});
text_input->set_focus_callback([this](bool active) {
if (active) {
this->on_hover(this->option_id);
}
});
// enable_focus();
};
void NovaConfigOptionString::update_value() {
recomp::config::ConfigValueVariant value_variant = callbacks->get_value(option_id);
std::string value = std::get<std::string>(value_variant);
text_input->set_text(value);
};
void NovaConfigOptionString::update_disabled() {
bool disabled = callbacks->get_disabled(option_id);
text_input->set_enabled(!disabled);
};
// NovaConfigOptionBool
NovaConfigOptionBool::NovaConfigOptionBool(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
) : NovaConfigOptionElement(parent, option_id, option_index, callbacks, set_option_value, on_hover)
{
// Negates the extra padding that text inputs automatically add.
name_label->set_margin_bottom(4.0f);
ContextId context = recompui::get_current_context();
toggle = context.create_element<Toggle>(this, ToggleSize::Medium);
update_value();
toggle->add_checked_callback([this](bool checked){
this->set_option_value(this->option_id, checked);
});
// toggle->set_focus_callback([this](bool active) {
// if (active) {
// this->on_hover(this->option_id);
// }
// });
// enable_focus();
};
void NovaConfigOptionBool::update_value() {
recomp::config::ConfigValueVariant value_variant = callbacks->get_value(option_id);
bool value = std::get<bool>(value_variant);
toggle->set_checked(value);
};
void NovaConfigOptionBool::update_disabled() {
bool disabled = callbacks->get_disabled(option_id);
toggle->set_enabled(!disabled);
};
} // namespace recompui
+131
View File
@@ -0,0 +1,131 @@
#pragma once
#include "ui_config_common.h"
#include "../elements/ui_element.h"
#include "../elements/ui_label.h"
#include "../elements/ui_radio.h"
#include "../elements/ui_text_input.h"
#include "../elements/ui_slider.h"
#include "../elements/ui_toggle.h"
namespace recompui {
class NovaConfigOptionElement : public Element {
protected:
std::string option_id;
size_t option_index;
ConfigOptionPropertyCallbacks *callbacks;
on_option_hover_callback on_hover;
set_option_value_callback set_option_value;
Label *name_label = nullptr;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "NovaConfigOptionElement"; }
public:
NovaConfigOptionElement(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
);
virtual ~NovaConfigOptionElement() = default;
// required overrides
virtual void update_value() = 0;
virtual void update_disabled() = 0;
virtual Element *get_focus_element() = 0;
void update_hidden();
const std::string &get_option_id() { return option_id; }
void set_nav_auto(NavDirection dir) override { get_focus_element()->set_nav_auto(dir); }
void set_nav_none(NavDirection dir) override { get_focus_element()->set_nav_none(dir); }
void set_nav(NavDirection dir, Element* element) override { get_focus_element()->set_nav(dir, element); }
void set_nav_manual(NavDirection dir, const std::string& target) override { get_focus_element()->set_nav_manual(dir, target); }
};
class NovaConfigOptionEnum : public NovaConfigOptionElement {
protected:
Radio *radio = nullptr;
Label *details_label = nullptr;
std::string_view get_type_name() override { return "NovaConfigOptionEnum"; }
public:
NovaConfigOptionEnum(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
);
Element* get_focus_element() override { return radio; }
void update_value() override;
void update_disabled() override;
void update_enum_details();
void update_enum_disabled();
};
class NovaConfigOptionNumber : public NovaConfigOptionElement {
protected:
Slider *slider = nullptr;
std::string_view get_type_name() override { return "NovaConfigOptionNumber"; }
public:
NovaConfigOptionNumber(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
);
Element* get_focus_element() override { return slider; }
void update_value() override;
void update_disabled() override;
};
class NovaConfigOptionString : public NovaConfigOptionElement {
protected:
TextInput *text_input = nullptr;
std::string_view get_type_name() override { return "NovaConfigOptionString"; }
public:
NovaConfigOptionString(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
);
Element* get_focus_element() override { return text_input; }
void update_value() override;
void update_disabled() override;
};
class NovaConfigOptionBool : public NovaConfigOptionElement {
protected:
Toggle *toggle = nullptr;
std::string_view get_type_name() override { return "NovaConfigOptionBool"; }
public:
NovaConfigOptionBool(
Element *parent,
std::string option_id,
size_t option_index,
ConfigOptionPropertyCallbacks *callbacks,
set_option_value_callback set_option_value,
on_option_hover_callback on_hover
);
Element* get_focus_element() override { return toggle; }
void update_value() override;
void update_disabled() override;
};
} // namespace recompui
@@ -1,14 +1,16 @@
#include "ui_config_page_controls.h"
#include "ui_assign_players_modal.h"
#include "elements/ui_button.h"
#include "elements/ui_label.h"
#include "elements/ui_toggle.h"
#include "elements/ui_container.h"
#include "elements/ui_binding_button.h"
#include "elements/ui_select.h"
#include "../ui_assign_players_modal.h"
#include "../elements/ui_button.h"
#include "../elements/ui_label.h"
#include "../elements/ui_toggle.h"
#include "../elements/ui_container.h"
#include "../elements/ui_binding_button.h"
#include "../elements/ui_select.h"
namespace recompui {
ConfigPageControls *controls_page = nullptr;
const std::string_view active_state_style_name = "cont_opt_active";
GameInputRow::GameInputRow(
@@ -129,12 +131,17 @@ void GameInputRow::process_event(const Event &e) {
ConfigPageControls::ConfigPageControls(
Element *parent,
int num_players,
std::vector<GameInputContext> game_input_contexts
std::vector<GameInputContext> game_input_contexts,
Element *nav_up_element,
set_first_focusable_below_tabs_t set_first_focusable_below_tabs
) : ConfigPage(parent) {
controls_page = this;
this->game_input_contexts = game_input_contexts;
this->num_players = num_players;
this->multiplayer_enabled = num_players > 1;
this->nav_up_element = nav_up_element;
this->set_first_navigation_element_cb = set_first_focusable_below_tabs;
multiplayer_enabled = this->num_players > 1;
multiplayer_view_mappings = !multiplayer_enabled;
set_selected_player(selected_player);
@@ -167,16 +174,19 @@ void ConfigPageControls::force_update() {
}
void ConfigPageControls::render_all() {
render_header();
render_body();
set_first_navigation_element(nullptr);
Element *header_focusable = render_header();
render_body(header_focusable);
render_footer();
confirm_first_navigation_element();
}
void ConfigPageControls::render_header() {
Element *ConfigPageControls::render_header() {
if (!multiplayer_enabled) {
hide_header();
return;
return nullptr;
}
Element *header_focusable = nullptr;
recompui::ContextId context = get_current_context();
add_header();
@@ -207,36 +217,62 @@ void ConfigPageControls::render_header() {
this->multiplayer_view_mappings = false;
this->force_update();
});
set_first_navigation_element(go_back_button);
if (nav_up_element) {
go_back_button->set_nav(NavDirection::Up, nav_up_element);
}
header_focusable = go_back_button;
} else {
Button* assign_players_button = context.create_element<Button>(header_right, "Assign players", ButtonStyle::Primary);
assign_players_button->add_pressed_callback([]() {
recompui::assign_players_modal->open();
recompinput::start_player_assignment();
});
set_first_navigation_element(assign_players_button);
if (nav_up_element) {
assign_players_button->set_nav(NavDirection::Up, nav_up_element);
}
header_focusable = assign_players_button;
}
}
return first_nav_element;
}
void ConfigPageControls::render_body() {
void ConfigPageControls::render_body(Element *header_focusable) {
bool show_mappings = (multiplayer_enabled && multiplayer_view_mappings) || !multiplayer_enabled;
recompui::ContextId context = get_current_context();
if (show_mappings) {
body->get_right()->set_display(Display::Flex);
render_body_mappings();
render_body_mappings(header_focusable);
} else {
body->get_right()->set_display(Display::None);
render_body_players();
render_body_players(header_focusable);
}
}
void ConfigPageControls::render_body_mappings() {
void ConfigPageControls::set_first_navigation_element(Element *element) {
// Only reset the first nav element or set it once
if (element == nullptr || first_nav_element == nullptr) {
first_nav_element = element;
}
}
void ConfigPageControls::confirm_first_navigation_element() {
if (set_first_navigation_element_cb) {
set_first_navigation_element_cb(first_nav_element);
}
}
void ConfigPageControls::render_body_mappings(Element *header_focusable) {
recompui::ContextId context = get_current_context();
body->set_as_navigation_container(NavigationType::Horizontal);
// left side
{
render_control_mappings();
render_control_mappings(header_focusable);
}
// right side
@@ -254,13 +290,15 @@ void ConfigPageControls::render_body_mappings() {
}
}
void ConfigPageControls::render_body_players() {
void ConfigPageControls::render_body_players(Element *header_focusable) {
recompui::ContextId context = get_current_context();
body->set_as_navigation_container(NavigationType::Horizontal);
auto body_left = body->get_left();
body_left->clear_children();
auto player_grid = context.create_element<Element>(body_left, 0, "div", false);
player_grid->set_as_navigation_container(NavigationType::Auto);
player_grid->set_display(Display::Flex);
player_grid->set_flex_direction(FlexDirection::Row);
player_grid->set_flex_wrap(FlexWrap::Wrap);
@@ -283,6 +321,17 @@ void ConfigPageControls::render_body_players() {
this->on_edit_player_profile(player_index);
});
player_cards.push_back(player_card);
if (i == 0) {
player_card->set_as_primary_focus(true);
// if (header_focusable) {
// player_card->set_nav(NavDirection::Up, header_focusable);
// } else {
// if (nav_up_element) {
// player_card->set_nav(NavDirection::Up, nav_up_element);
// }
// set_first_navigation_element(player_card);
// }
}
}
}
@@ -313,6 +362,7 @@ void ConfigPageControls::render_footer() {
recompui::ContextId context = get_current_context();
add_footer();
footer->set_as_navigation_container(NavigationType::Horizontal);
{
auto footer_left = footer->get_left();
footer_left->clear_children();
@@ -338,8 +388,9 @@ void ConfigPageControls::render_footer() {
}
}
void ConfigPageControls::render_control_mappings() {
void ConfigPageControls::render_control_mappings(Element *header_focusable) {
recompui::ContextId context = get_current_context();
auto body_left = body->get_left();
body_left->clear_children();
@@ -353,6 +404,7 @@ void ConfigPageControls::render_control_mappings() {
body_left_scroll->set_width(100.0f, Unit::Percent);
body_left_scroll->set_max_height(100.0f, Unit::Percent);
body_left_scroll->set_overflow_y(Overflow::Scroll);
body_left_scroll->set_as_navigation_container(NavigationType::GridCol);
game_input_rows.clear();
for (int i = 0; i < game_input_contexts.size(); i++) {
@@ -371,6 +423,19 @@ void ConfigPageControls::render_control_mappings() {
}
);
game_input_rows.push_back(row);
row->set_as_navigation_container(NavigationType::GridRow);
// if (i == 0) {
// if (header_focusable) {
// row->set_nav(NavDirection::Up, header_focusable);
// } else {
// // Something above the whole page
// if (nav_up_element) {
// row->set_nav(NavDirection::Up, nav_up_element);
// }
// set_first_navigation_element(row);
// }
// }
}
}
update_control_mappings();
@@ -404,6 +469,7 @@ void ConfigPageControls::update_control_mappings() {
}
ConfigPageControls::~ConfigPageControls() {
controls_page = nullptr;
}
recompinput::InputDevice ConfigPageControls::get_player_input_device() {
@@ -1,12 +1,13 @@
#pragma once
#include "elements/ui_config_page.h"
#include "../elements/ui_config_page.h"
#include "recomp_input.h"
#include "elements/ui_icon_button.h"
#include "elements/ui_binding_button.h"
#include "elements/ui_pill_button.h"
#include "elements/ui_toggle.h"
#include "ui_player_card.h"
#include "../elements/ui_icon_button.h"
#include "../elements/ui_binding_button.h"
#include "../elements/ui_pill_button.h"
#include "../elements/ui_toggle.h"
#include "../elements/ui_modal.h"
#include "../ui_player_card.h"
// TODO: remove after moving to recompinput
namespace recompinput {
@@ -90,6 +91,9 @@ protected:
Toggle *keyboard_toggle;
Element *description_container = nullptr;
on_player_bind_callback on_player_bind;
Element *nav_up_element = nullptr;
Element *first_nav_element = nullptr;
set_first_focusable_below_tabs_t set_first_navigation_element_cb = nullptr;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ConfigPageControls"; }
@@ -105,25 +109,32 @@ private:
void on_edit_player_profile(int player_index);
void render_all();
void render_header();
void render_body();
void render_body_players();
void render_body_mappings();
// returns pointer to primary focusable element in header or nullptr if not rendered
Element *render_header();
void render_body(Element *header_focusable);
void render_body_players(Element *header_focusable);
void render_body_mappings(Element *header_focusable);
void render_footer();
void set_first_navigation_element(Element *element);
void confirm_first_navigation_element();
recompinput::InputDevice get_player_input_device();
public:
ConfigPageControls(
Element *parent,
int num_players,
std::vector<GameInputContext> game_input_contexts
std::vector<GameInputContext> game_input_contexts,
Element *nav_up_element = nullptr,
set_first_focusable_below_tabs_t set_first_focusable_below_tabs = nullptr
);
virtual ~ConfigPageControls();
void force_update();
void render_control_mappings();
void render_control_mappings(Element *header_focusable);
void update_control_mappings();
void set_selected_player(int player);
};
extern ConfigPageControls *controls_page;
} // namespace recompui
@@ -0,0 +1,307 @@
#include "ui_config_page_options_menu.h"
namespace recompui {
ConfigPageOptionsMenu::ConfigPageOptionsMenu(
Element *parent,
recomp::config::Config *config,
const ConfigOptionPropertyCallbacks &callbacks,
set_option_value_callback set_option_value,
bool requires_confirmation,
bool is_internal
) : ConfigPage(parent, Events(EventType::Hover, EventType::Update, EventType::MenuAction)),
config(config),
callbacks(ConfigOptionPropertyCallbacks(callbacks)),
set_option_value(set_option_value),
requires_confirmation(requires_confirmation),
is_internal(is_internal)
{
ContextId context = recompui::get_current_context();
set_as_navigation_container(NavigationType::Vertical);
render_config_options();
description_container = context.create_element<Element>(body->get_right(), 0, "p", true);
description_container->set_typography(theme::Typography::Body);
description_container->set_line_height(28.0f);
description_container->set_padding(8.0f);
set_description_text("");
if (requires_confirmation) {
add_footer();
render_confirmation_footer();
}
}
void ConfigPageOptionsMenu::set_description_text(const std::string &text) {
if (description_container) {
if (is_internal) {
description_container->set_text_unsafe(text);
} else {
description_container->set_text(text);
}
}
}
NovaConfigOptionElement* ConfigPageOptionsMenu::get_element_from_option_id(const std::string &option_id) {
for (auto *element : config_option_elements) {
if (element->get_option_id() == option_id) {
return element;
}
}
return nullptr;
}
void ConfigPageOptionsMenu::perform_option_render_updates() {
using ConfigOptionUpdateType = recomp::config::ConfigOptionUpdateType;
if (get_config_option_updates == nullptr || clear_config_option_updates == nullptr) {
return;
}
queue_update();
auto options_updates = get_config_option_updates();
bool has_updates = !options_updates.empty();
for (auto &option_update : options_updates) {
size_t option_index = option_update.option_index;
auto schema = config->get_config_schema();
std::string &option_id = schema.options[option_index].id;
NovaConfigOptionElement* element = get_element_from_option_id(option_id);
if (element == nullptr) {
printf("Failed to update conf option: '%s'\n", option_id.c_str());
continue;
}
for (auto &update_type :option_update.updates) {
switch (update_type) {
case ConfigOptionUpdateType::Disabled: {
element->update_disabled();
break;
}
case ConfigOptionUpdateType::Hidden: {
element->update_hidden();
break;
}
case ConfigOptionUpdateType::EnumDetails: {
NovaConfigOptionEnum *enum_element = static_cast<NovaConfigOptionEnum*>(element);
enum_element->update_enum_details();
break;
}
case ConfigOptionUpdateType::EnumDisabled: {
NovaConfigOptionEnum *enum_element = static_cast<NovaConfigOptionEnum*>(element);
enum_element->update_enum_disabled();
break;
}
case ConfigOptionUpdateType::Value: {
element->update_value();
break;
}
case ConfigOptionUpdateType::Description: {
if (description_container && description_option_id == option_id) {
set_description_text(callbacks.get_description(option_id));
}
break;
}
}
}
}
clear_config_option_updates();
if (has_updates) {
// apply_option_navigation();
}
}
void ConfigPageOptionsMenu::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover: {
bool active = std::get<EventHover>(e.variant).active;
if (!active) {
set_description_text("");
}
break;
}
case EventType::Update: {
perform_option_render_updates();
break;
}
case EventType::MenuAction: {
auto action = std::get<EventMenuAction>(e.variant).action;
if (action == MenuAction::Apply) {
if (on_apply_callback != nullptr) {
on_apply_callback();
}
}
break;
}
default:
assert(false && "Unknown event type.");
break;
}
}
void ConfigPageOptionsMenu::render_confirmation_footer() {
ContextId context = recompui::get_current_context();
footer->set_as_navigation_container(NavigationType::Horizontal);
apply_button = context.create_element<Button>(footer->get_right(), "Apply", ButtonStyle::Secondary);
apply_button->set_enabled(false);
apply_button->set_as_primary_focus();
}
void ConfigPageOptionsMenu::on_set_option_value(const std::string &option_id, recomp::config::ConfigValueVariant value) {
set_option_value(option_id, value);
if (check_page_dirty && apply_button != nullptr) {
apply_button->set_enabled(check_page_dirty());
}
}
void ConfigPageOptionsMenu::render_config_options() {
ContextId context = recompui::get_current_context();
Element *body_left = get_body()->get_left();
body_left->clear_children();
body_left->set_padding(0.0f);
body_left->set_display(Display::Block);
body_left->set_position(Position::Relative);
body_left->set_height(100.0f, Unit::Percent);
{
auto body_left_scroll = context.create_element<Element>(body_left, 0, "div", false);
body_left_scroll->set_display(Display::Block);
body_left_scroll->set_width(100.0f, Unit::Percent);
body_left_scroll->set_min_height(100.0f, Unit::Percent);
body_left_scroll->set_max_height(100.0f, Unit::Percent);
body_left_scroll->set_padding(16.0f);
body_left_scroll->set_overflow_y(Overflow::Auto);
config_option_elements.clear();
bound_on_option_hover = [this](const std::string &option_id) {
this->on_option_hover(option_id);
};
bound_set_option_value = [this](const std::string &option_id, recomp::config::ConfigValueVariant value) {
this->on_set_option_value(option_id, value);
};
auto schema = config->get_config_schema();
for (size_t i = 0; i < schema.options.size(); i++) {
auto &config_option = schema.options[i];
NovaConfigOptionElement *element = nullptr;
switch (config_option.type) {
case recomp::config::ConfigOptionType::Enum: {
element = context.create_element<NovaConfigOptionEnum>(
body_left_scroll,
config_option.id,
i,
&callbacks,
bound_set_option_value,
bound_on_option_hover
);
break;
}
case recomp::config::ConfigOptionType::Number: {
element = context.create_element<NovaConfigOptionNumber>(
body_left_scroll,
config_option.id,
i,
&callbacks,
bound_set_option_value,
bound_on_option_hover
);
break;
}
case recomp::config::ConfigOptionType::String: {
element = context.create_element<NovaConfigOptionString>(
body_left_scroll,
config_option.id,
i,
&callbacks,
bound_set_option_value,
bound_on_option_hover
);
break;
}
case recomp::config::ConfigOptionType::Bool: {
element = context.create_element<NovaConfigOptionBool>(
body_left_scroll,
config_option.id,
i,
&callbacks,
bound_set_option_value,
bound_on_option_hover
);
break;
}
default: {
assert(false && "Unknown config option type");
}
};
element->update_disabled();
element->update_hidden();
config_option_elements.push_back(element);
}
apply_option_navigation();
}
}
Element* ConfigPageOptionsMenu::get_navigation_element(int cur_index, int direction) {
int new_index = cur_index + direction;
if (new_index < 0) {
// need to handle getting above element
return nullptr;
}
auto schema = config->get_config_schema();
if (
new_index >= static_cast<int>(config_option_elements.size()) ||
new_index >= static_cast<int>(schema.options.size())) {
// need to handle getting below element
return nullptr;
}
auto option = schema.options[new_index];
if (callbacks.get_disabled(option.id) || callbacks.get_hidden(option.id)) {
return get_navigation_element(new_index, direction);
}
return config_option_elements[new_index]->get_focus_element();
}
void ConfigPageOptionsMenu::apply_option_navigation() {
Element *first_enabled = nullptr;
auto schema = config->get_config_schema();
for (size_t i = 0; i < schema.options.size(); i++) {
auto option = schema.options[i];
bool this_is_fully_enabled = !callbacks.get_disabled(option.id) && !callbacks.get_hidden(option.id);
if (first_enabled == nullptr && this_is_fully_enabled) {
first_enabled = config_option_elements[i];
config_option_elements[i]->set_as_primary_focus(true);
} else {
config_option_elements[i]->set_as_primary_focus(false);
}
auto *prev_element = get_navigation_element(i, -1);
if (prev_element != nullptr) {
config_option_elements[i]->set_nav(NavDirection::Up, prev_element);
}
auto *next_element = get_navigation_element(i, 1);
if (next_element != nullptr) {
config_option_elements[i]->set_nav(NavDirection::Down, next_element);
}
}
}
void ConfigPageOptionsMenu::on_option_hover(const std::string &option_id) {
if (description_container) {
description_option_id = option_id;
set_description_text(callbacks.get_description(option_id));
}
}
void ConfigPageOptionsMenu::set_on_apply_callback(std::function<void()> callback) {
if (apply_button) {
on_apply_callback = [this, callback]{
callback();
if (this->apply_button && this->check_page_dirty) {
this->apply_button->set_enabled(this->check_page_dirty());
}
};
apply_button->add_pressed_callback(on_apply_callback);
}
}
} // namespace recompui
@@ -0,0 +1,71 @@
#pragma once
#include "ui_config_common.h"
#include "ui_config_option.h"
#include "../elements/ui_config_page.h"
#include "../elements/ui_modal.h"
#include "../elements/ui_button.h"
#include "librecomp/config.hpp"
#include "librecomp/mods.hpp"
namespace recompui {
class ConfigPageOptionsMenu : public ConfigPage {
protected:
recomp::config::Config *config;
ConfigOptionPropertyCallbacks callbacks;
set_option_value_callback set_option_value;
std::function<void()> on_apply_callback;
bool requires_confirmation = false;
bool is_internal = false;
check_page_dirty_callback check_page_dirty = nullptr;
get_config_option_updates_callback get_config_option_updates = nullptr;
clear_config_option_updates_callback clear_config_option_updates = nullptr;
std::vector<NovaConfigOptionElement*> config_option_elements;
Element *description_container = nullptr;
std::string description_option_id = "";
Button *apply_button = nullptr;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ConfigPageOptionsMenu"; }
private:
on_option_hover_callback bound_on_option_hover;
set_option_value_callback bound_set_option_value;
void render_config_options();
void render_confirmation_footer();
void on_option_hover(const std::string &option_id);
void on_set_option_value(const std::string &option_id, recomp::config::ConfigValueVariant value);
NovaConfigOptionElement* get_element_from_option_id(const std::string &option_id);
void perform_option_render_updates();
void apply_option_navigation();
Element* get_navigation_element(int cur_index, int direction);
void set_description_text(const std::string &text);
public:
ConfigPageOptionsMenu(
Element *parent,
recomp::config::Config *config,
const ConfigOptionPropertyCallbacks &callbacks,
set_option_value_callback set_option_value,
bool requires_confirmation = false,
bool is_internal = false
);
virtual ~ConfigPageOptionsMenu() = default;
void set_check_page_dirty_callback(check_page_dirty_callback callback) { check_page_dirty = callback; }
void set_on_apply_callback(std::function<void()> callback);
void set_get_config_option_updates_callback(get_config_option_updates_callback callback) {
get_config_option_updates = callback;
if (clear_config_option_updates != nullptr) {
queue_update();
}
}
void set_clear_config_option_updates_callback(clear_config_option_updates_callback callback) {
clear_config_option_updates = callback;
if (get_config_option_updates != nullptr) {
queue_update();
}
}
};
} // namespace recompui
+29
View File
@@ -0,0 +1,29 @@
#include "ui_config_tab_controls.h"
namespace recompui {
#define DEFINE_INPUT(name, value, readable) GameInputContext{ readable, "Description for " #readable " input.", recompinput::GameInput::##name, !(recompinput::GameInput::##name == recompinput::GameInput::TOGGLE_MENU || recompinput::GameInput::##name == recompinput::GameInput::ACCEPT_MENU) },
static std::vector<struct GameInputContext> temp_game_input_contexts = {
DEFINE_ALL_INPUTS()
};
#undef DEFINE_INPUT
Element* ConfigTabControls::create_tab_contents(recompui::ContextId context, Element* parent) {
Modal *modal = get_parent_modal();
apply_set_first_focusable_below_tabs_callback();
Element *active_tab = modal != nullptr ? modal->get_active_tab() : nullptr;
ConfigPage *controls_page = context.create_element<ConfigPageControls>(
parent,
recompinput::get_num_players(),
temp_game_input_contexts,
active_tab,
set_first_focusable_below_tabs
);
return controls_page;
}
ConfigTabControls config_tab_controls = ConfigTabControls();
} // namespace recompui
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "../elements/ui_element.h"
#include "../elements/ui_modal.h"
#include "ui_config_page_controls.h"
namespace recompui {
class ConfigTabControls : public TabContext {
public:
ConfigTabControls() : TabContext("Controls") {}
Element* create_tab_contents(recompui::ContextId context, Element* parent) override;
};
extern ConfigTabControls config_tab_controls;
} // namespace recompui
+385
View File
@@ -0,0 +1,385 @@
#include "ui_config_tab_graphics.h"
#include "banjo_render.h"
namespace recompui {
recomp::config::Config config_graphics("Graphics", "graphics_2", true);
ConfigTabGraphics config_tab_graphics(&config_graphics);
using EnumOptionVector = const std::vector<recomp::config::ConfigOptionEnumOption>;
namespace gfx_keys {
const std::string developer_mode = "developer_mode";
const std::string res_option = "res_option";
const std::string wm_option = "wm_option";
const std::string hr_option = "hr_option";
const std::string api_option = "api_option";
const std::string ar_option = "ar_option";
const std::string msaa_option = "msaa_option";
const std::string rr_option = "rr_option";
const std::string hpfb_option = "hpfb_option";
const std::string rr_manual_value = "rr_manual_value";
const std::string ds_option = "ds_option";
}
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
EnumOptionVector resolution_options = {
{ultramodern::renderer::Resolution::Original, "Original", "Original"},
{ultramodern::renderer::Resolution::Original2x, "Original2x", "Original 2x"},
{ultramodern::renderer::Resolution::Auto, "Auto", "Auto"},
};
enum class DownsamplingOption {
Off = 0,
X2 = 2,
X4 = 4,
};
EnumOptionVector downsampling_options = {
{DownsamplingOption::Off, "Off"},
{DownsamplingOption::X2, "2x"},
{DownsamplingOption::X4, "4x"},
};
EnumOptionVector window_mode_options = {
{ultramodern::renderer::WindowMode::Windowed, "Windowed"},
{ultramodern::renderer::WindowMode::Fullscreen, "Fullscreen"}
};
#if defined(_WIN32)
#define ALLOW_D3D12
#endif
#if defined(_WIN32) || defined(__linux__)
#define ALLOW_VULKAN
#endif
#if defined(__APPLE__)
#define ALLOW_METAL
#endif
EnumOptionVector graphics_api_options = {
{ultramodern::renderer::GraphicsApi::Auto, "Auto"},
#ifdef ALLOW_D3D12
{ultramodern::renderer::GraphicsApi::D3D12, "D3D12"},
#endif
#ifdef ALLOW_VULKAN
{ultramodern::renderer::GraphicsApi::Vulkan, "Vulkan"},
#endif
#ifdef ALLOW_METAL
{ultramodern::renderer::GraphicsApi::Metal, "Metal"},
#endif
};
EnumOptionVector aspect_ratio_options = {
{ultramodern::renderer::AspectRatio::Original, "Original"},
{ultramodern::renderer::AspectRatio::Expand, "Expand"},
// {ultramodern::renderer::AspectRatio::Manual, "Manual"},
};
EnumOptionVector antialiasing_options = {
{ultramodern::renderer::Antialiasing::None, "None"},
{ultramodern::renderer::Antialiasing::MSAA2X, "MSAA2X", "2X"},
{ultramodern::renderer::Antialiasing::MSAA4X, "MSAA4X", "4X"},
// {ultramodern::renderer::Antialiasing::MSAA8X, "MSAA8X"},
};
EnumOptionVector refresh_rate_options = {
{ultramodern::renderer::RefreshRate::Original, "Original"},
{ultramodern::renderer::RefreshRate::Display, "Display"},
{ultramodern::renderer::RefreshRate::Manual, "Manual"},
};
EnumOptionVector hpfb_options = {
{ultramodern::renderer::HighPrecisionFramebuffer::Auto, "Auto"},
{ultramodern::renderer::HighPrecisionFramebuffer::On, "On"},
{ultramodern::renderer::HighPrecisionFramebuffer::Off, "Off"},
};
EnumOptionVector hud_ratio_mode_options = {
{ultramodern::renderer::HUDRatioMode::Original, "Original"},
{ultramodern::renderer::HUDRatioMode::Clamp16x9, "Clamp16x9", "16:9"},
{ultramodern::renderer::HUDRatioMode::Full, "Expand"},
};
std::string get_downsampling_details(ultramodern::renderer::Resolution res_option, DownsamplingOption ds_option) {
switch (res_option) {
default:
case ultramodern::renderer::Resolution::Auto:
return "Downsampling is not available at auto resolution";
case ultramodern::renderer::Resolution::Original:
if (ds_option == DownsamplingOption::X2) {
return "Rendered in 480p and scaled to 240p";
} else if (ds_option == DownsamplingOption::X4) {
return "Rendered in 960p and scaled to 240p";
}
return "";
case ultramodern::renderer::Resolution::Original2x:
if (ds_option == DownsamplingOption::X2) {
return "Rendered in 960p and scaled to 480p";
} else if (ds_option == DownsamplingOption::X4) {
return "Rendered in 4K and scaled to 480p";
}
return "";
}
}
using OptionChangeContext = recomp::config::OptionChangeContext;
template <typename T = uint32_t>
T get_graphics_enum_value(const std::string& key, OptionChangeContext change_context = OptionChangeContext::Permanent) {
if (change_context == OptionChangeContext::Temporary) {
return static_cast<T>(std::get<uint32_t>(config_graphics.get_temp_option_value(key)));
} else {
return static_cast<T>(std::get<uint32_t>(config_graphics.get_option_value(key)));
}
}
template <typename T = uint32_t>
T get_graphics_number_value(const std::string& key, OptionChangeContext change_context = OptionChangeContext::Permanent) {
if (change_context == OptionChangeContext::Temporary) {
return static_cast<T>(std::get<double>(config_graphics.get_temp_option_value(key)));
} else {
return static_cast<T>(std::get<double>(config_graphics.get_option_value(key)));
}
}
bool get_graphics_bool_value(const std::string& key, OptionChangeContext change_context = OptionChangeContext::Permanent) {
if (change_context == OptionChangeContext::Temporary) {
return std::get<bool>(config_graphics.get_temp_option_value(key));
} else {
return std::get<bool>(config_graphics.get_option_value(key));
}
}
void determine_downsampling_display(ultramodern::renderer::Resolution res_option, OptionChangeContext change_context) {
DownsamplingOption ds_opt = get_graphics_enum_value<DownsamplingOption>(gfx_keys::ds_option, change_context);
config_graphics.update_option_enum_details(gfx_keys::ds_option, get_downsampling_details(res_option, ds_opt));
}
std::string get_framerate_text() {
return
"Sets the game's output framerate. This option does not affect gameplay."
"<br />"
"<br />"
"Note: If you have issues with <recomp-color primary>Display</recomp-color> mode while using an external frame limiter, use <recomp-color primary>Manual</recomp-color> mode instead and configure it to that same frame limit."
"<br />"
"<br />"
"<recomp-color primary>Detected display refresh rate: " + std::to_string(ultramodern::get_display_refresh_rate()) + "hz</recomp-color>";
}
void save_graphics_config() {
ultramodern::renderer::GraphicsConfig new_config;
new_config.developer_mode = get_graphics_bool_value(gfx_keys::developer_mode);
new_config.res_option = get_graphics_enum_value<ultramodern::renderer::Resolution>(gfx_keys::res_option);
new_config.wm_option = get_graphics_enum_value<ultramodern::renderer::WindowMode>(gfx_keys::wm_option);
new_config.hr_option = get_graphics_enum_value<ultramodern::renderer::HUDRatioMode>(gfx_keys::hr_option);
new_config.api_option = get_graphics_enum_value<ultramodern::renderer::GraphicsApi>(gfx_keys::api_option);
new_config.ar_option = get_graphics_enum_value<ultramodern::renderer::AspectRatio>(gfx_keys::ar_option);
new_config.msaa_option = get_graphics_enum_value<ultramodern::renderer::Antialiasing>(gfx_keys::msaa_option);
new_config.rr_option = get_graphics_enum_value<ultramodern::renderer::RefreshRate>(gfx_keys::rr_option);
new_config.hpfb_option = get_graphics_enum_value<ultramodern::renderer::HighPrecisionFramebuffer>(gfx_keys::hpfb_option);
new_config.rr_manual_value = get_graphics_number_value<int>(gfx_keys::rr_manual_value);
new_config.ds_option = get_graphics_enum_value<int>(gfx_keys::ds_option);
ultramodern::renderer::set_graphics_config(new_config);
}
void ConfigTabGraphics::on_init() {
config_graphics.set_save_callback(save_graphics_config);
config_graphics.add_bool_option(
gfx_keys::developer_mode,
"Dev Mode",
"Enables developer features.",
false,
true
);
config_graphics.add_enum_option(
gfx_keys::res_option,
"Resolution",
"Sets the output resolution of the game. <recomp-color primary>Original</recomp-color> matches the game's original 240p resolution. <recomp-color primary>Original 2x</recomp-color> will render at 480p. <recomp-color primary>Auto</recomp-color> will scale based on the game window's resolution.",
resolution_options,
ultramodern::renderer::Resolution::Auto
);
config_graphics.add_enum_option(
gfx_keys::ds_option,
"Downsampling Quality",
"Renders at a higher resolution and scales it down to the output resolution for increased quality. Only available in <recomp-color primary>Original</recomp-color> and <recomp-color primary>Original 2x</recomp-color> resolution."
"<br />"
"<br />"
"Note: <recomp-color primary>4x</recomp-color> downsampling quality at <recomp-color primary>Original 2x</recomp-color> resolution may cause performance issues on low end devices, as it will cause the game to render <recomp-color warning>at almost 4k internal resolution</recomp-color>.",
downsampling_options,
DownsamplingOption::Off
);
config_graphics.add_option_change_callback(
gfx_keys::res_option,
[](recomp::config::ConfigValueVariant cur_value, recomp::config::ConfigValueVariant prev_value, OptionChangeContext change_context) {
auto new_opt = static_cast<ultramodern::renderer::Resolution>(std::get<uint32_t>(cur_value));
determine_downsampling_display(new_opt, change_context);
}
);
config_graphics.add_option_change_callback(
gfx_keys::ds_option,
[](recomp::config::ConfigValueVariant cur_value, recomp::config::ConfigValueVariant prev_value, OptionChangeContext change_context) {
auto resolution_opt = get_graphics_enum_value<ultramodern::renderer::Resolution>(gfx_keys::res_option, change_context);
determine_downsampling_display(resolution_opt, change_context);
}
);
config_graphics.add_option_disable_dependency(
gfx_keys::ds_option,
gfx_keys::res_option,
ultramodern::renderer::Resolution::Auto
);
config_graphics.on_json_parse_option(gfx_keys::ds_option, [](const nlohmann::json& j) {
return j.get<uint32_t>();
});
config_graphics.on_json_serialize_option(gfx_keys::ds_option, [](const recomp::config::ConfigValueVariant& value) {
return nlohmann::json(std::get<uint32_t>(value));
});
config_graphics.add_enum_option(
gfx_keys::ar_option,
"Aspect Ratio",
"Sets the horizontal aspect ratio. <recomp-color primary>Original</recomp-color> uses the game's original 4:3 aspect ratio. <recomp-color primary>Expand</recomp-color> will adjust to match the game window's aspect ratio.",
aspect_ratio_options,
ultramodern::renderer::AspectRatio::Expand
);
config_graphics.add_enum_option(
gfx_keys::wm_option,
"Window Mode",
"Sets whether the game should display <recomp-color primary>Windowed</recomp-color> or <recomp-color primary>Fullscreen</recomp-color>. You can also use <recomp-color primary>F11</recomp-color> or <recomp-color primary>Alt + Enter</recomp-color> to toggle this option.",
window_mode_options,
wm_default()
);
config_graphics.add_enum_option(
gfx_keys::rr_option,
"Framerate",
get_framerate_text(),
refresh_rate_options,
ultramodern::renderer::RefreshRate::Display
);
config_graphics.add_option_change_callback(
gfx_keys::res_option,
[](recomp::config::ConfigValueVariant cur_value, recomp::config::ConfigValueVariant prev_value, OptionChangeContext change_context) {
auto new_opt = static_cast<ultramodern::renderer::Resolution>(std::get<uint32_t>(cur_value));
determine_downsampling_display(new_opt, change_context);
}
);
config_graphics.add_number_option(
gfx_keys::rr_manual_value,
"",
get_framerate_text(),
20.0, 240.0, 1.0, 0, false, 60.0
);
config_graphics.add_option_hidden_dependency(
gfx_keys::rr_manual_value,
gfx_keys::rr_option,
ultramodern::renderer::RefreshRate::Original,
ultramodern::renderer::RefreshRate::Display
);
config_graphics.add_enum_option(
gfx_keys::msaa_option,
"MS Anti-Aliasing",
"Sets the multisample anti-aliasing (MSAA) quality level. This reduces jagged edges in the final image at the expense of rendering performance."
"<br />"
"<br />"
"<recomp-color primary>Note: This option won't be available if your GPU does not support programmable MSAA sample positions, as it is currently required to avoid rendering glitches.</recomp-color>",
antialiasing_options,
ultramodern::renderer::Antialiasing::MSAA2X
);
config_graphics.add_enum_option(
gfx_keys::hr_option,
"HUD Placement",
"Adjusts the placement of HUD elements to fit the selected aspect ratio. <recomp-color primary>Expand</recomp-color> will use the aspect ratio of the game's output window.",
hud_ratio_mode_options,
ultramodern::renderer::HUDRatioMode::Clamp16x9
);
config_graphics.add_enum_option(
gfx_keys::api_option,
"Graphics API",
"Selects the graphics API to use.",
graphics_api_options,
ultramodern::renderer::GraphicsApi::Auto
);
config_graphics.add_option_hidden_dependency(
gfx_keys::api_option,
gfx_keys::developer_mode,
false
);
config_graphics.add_enum_option(
gfx_keys::hpfb_option,
"High Precision Framebuffer",
"Sets whether to use a high precision framebuffer.",
hpfb_options,
ultramodern::renderer::HighPrecisionFramebuffer::Off
);
config_graphics.add_option_hidden_dependency(
gfx_keys::hpfb_option,
gfx_keys::developer_mode,
false
);
config_graphics.load_config();
save_graphics_config();
}
void update_msaa_supported(bool supported) {
if (!supported) {
config_graphics.update_option_enum_details(
gfx_keys::msaa_option,
supported ? "Available" : "Not available (missing sample positions support)"
);
config_graphics.update_option_disabled(
gfx_keys::msaa_option,
true
);
} else {
auto max_msaa = banjo::renderer::RT64MaxMSAA();
if (max_msaa < RT64::UserConfiguration::Antialiasing::MSAA2X) {
config_graphics.update_enum_option_disabled(gfx_keys::msaa_option, static_cast<uint32_t>(ultramodern::renderer::Antialiasing::MSAA2X), true);
}
if (max_msaa < RT64::UserConfiguration::Antialiasing::MSAA4X) {
config_graphics.update_enum_option_disabled(gfx_keys::msaa_option, static_cast<uint32_t>(ultramodern::renderer::Antialiasing::MSAA4X), true);
}
}
}
} // namespace recompui
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "librecomp/config.hpp"
#include "ui_config_tab_manifest.h"
namespace recompui {
class ConfigTabGraphics : public ConfigTab {
public:
ConfigTabGraphics(recomp::config::Config *config) : ConfigTab(config) {}
void on_init() override;
};
extern recomp::config::Config config_graphics;
extern ConfigTabGraphics config_tab_graphics;
void update_msaa_supported(bool supported);
} // namespace recompui
+109
View File
@@ -0,0 +1,109 @@
#include "ui_config_tab_manifest.h"
namespace recompui {
ConfigTab::ConfigTab(recomp::config::Config *config) : TabContext(config->name),
config(config),
callbacks(ConfigOptionPropertyCallbacks(
// get_configuration
[this](const std::string &option_id) {
auto schema = this->config->get_config_schema();
size_t option_index = schema.options_by_id.at(option_id);
auto &option = schema.options[option_index];
return option.variant;
},
// get_value
[this](const std::string &option_id) {
if (this->config->requires_confirmation) {
return this->config->get_temp_option_value(option_id);
}
return this->config->get_option_value(option_id);
},
// get_name
[this](const std::string &option_id) {
auto schema = this->config->get_config_schema();
size_t option_index = schema.options_by_id.at(option_id);
return schema.options[option_index].name;
},
// get_description
[this](const std::string &option_id) {
auto schema = this->config->get_config_schema();
size_t option_index = schema.options_by_id.at(option_id);
return schema.options[option_index].description;
},
// get_hidden
[this](const std::string &option_id) {
auto schema = this->config->get_config_schema();
size_t option_index = schema.options_by_id.at(option_id);
return this->config->is_config_option_hidden(option_index);
},
// get_disabled
[this](const std::string &option_id) {
auto schema = this->config->get_config_schema();
size_t option_index = schema.options_by_id.at(option_id);
return this->config->is_config_option_disabled(option_index);
},
// get_enum_option_details
[this](const std::string &option_id) {
auto schema = this->config->get_config_schema();
size_t option_index = schema.options_by_id.at(option_id);
return this->config->get_enum_option_details(option_index);
},
// get_enum_option_disabled
[this](const std::string &option_id, uint32_t enum_index) {
auto schema = this->config->get_config_schema();
size_t option_index = schema.options_by_id.at(option_id);
return this->config->get_enum_option_disabled(option_index, enum_index);
}
)) {};
Element* ConfigTab::create_tab_contents(recompui::ContextId context, Element* parent) {
ConfigPageOptionsMenu *conf_page = context.create_element<ConfigPageOptionsMenu>(
parent,
config,
callbacks,
[this](const std::string &option_id, recomp::config::ConfigValueVariant value) {
this->config->set_option_value(option_id, value);
},
config->requires_confirmation,
true
);
conf_page->set_get_config_option_updates_callback([this]() {
return this->config->get_config_option_updates();
});
conf_page->set_clear_config_option_updates_callback([this]() {
this->config->clear_config_option_updates();
});
if (config->requires_confirmation) {
conf_page->set_on_apply_callback([this]() {
this->config->save_config();
});
conf_page->set_check_page_dirty_callback([this]() {
return this->config->is_dirty();
});
}
return conf_page;
};
bool ConfigTab::can_be_closed() {
if (!config->requires_confirmation) {
return true;
}
if (config->is_dirty()) {
// TODO: show prompt to apply/cancel changes
return false;
}
return true;
}
void ConfigTab::on_tab_close() {
config->save_config();
}
} // namespace recompui
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "librecomp/config.hpp"
#include "ui_config_common.h"
#include "ui_config_page_options_menu.h"
#include "../elements/ui_element.h"
#include "../elements/ui_modal.h"
namespace recompui {
class ConfigTab : public TabContext {
private:
recomp::config::Config *config = nullptr;
ConfigOptionPropertyCallbacks callbacks;
public:
ConfigTab(recomp::config::Config *config);
Element* create_tab_contents(recompui::ContextId context, Element* parent) override;
bool can_be_closed() override;
void on_tab_close() override;
};
} // namespace recompui
+39
View File
@@ -248,6 +248,7 @@ recompui::ContextId recompui::create_context() {
root->set_width(100.0f, Unit::Percent);
root->set_height(100.0f, Unit::Percent);
root->set_display(Display::Flex);
root->set_as_root_document();
ret.close();
@@ -583,6 +584,11 @@ recompui::Style* recompui::ContextId::create_style() {
return add_resource_impl(std::make_unique<Style>());
}
void recompui::ContextId::destroy_resource(Element* resource) {
recompui::report_removed_element(resource->base);
destroy_resource(resource->resource_id);
}
void recompui::ContextId::destroy_resource(Style* resource) {
destroy_resource(resource->resource_id);
}
@@ -731,3 +737,36 @@ recompui::ContextId recompui::get_context_from_document(Rml::ElementDocument* do
}
return find_it->second;
}
recompui::Element* recompui::ContextId::get_focused_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);
}
auto doc = get_document();
if (doc == nullptr) {
return nullptr;
}
Rml::Context *rml_context = doc->GetContext();
if (rml_context == nullptr) {
return nullptr;
}
Rml::Element *focused = rml_context->GetFocusElement();
if (focused == nullptr) {
return nullptr;
}
for (const auto& resource : opened_context->resources) {
recompui::Element *element = static_cast<Element*>(resource.get());
if (element->id == focused->GetId()) {
return element;
}
}
return nullptr;
}
+2
View File
@@ -35,12 +35,14 @@ namespace recompui {
Style* create_style();
void destroy_resource(Element* resource);
void destroy_resource(Style* resource);
void destroy_resource(ResourceId resource);
void clear_children();
Rml::ElementDocument* get_document();
Element* get_root_element();
Element* get_focused_element();
Element* get_autofocus_element();
void set_autofocus_element(Element* element);
+26 -4
View File
@@ -1,5 +1,6 @@
#include "ui_binding_button.h"
#include "ui_theme.h"
#include "recomp_ui.h"
#include <ultramodern/ultramodern.hpp>
namespace recompui {
@@ -36,8 +37,15 @@ namespace recompui {
ContextId context = get_current_context();
bound_text_el = context.create_element<Element>(this, 0, "div", true);
bound_text_el->set_text(mapped_binding);
apply_binding_style();
unknown_svg_el = context.create_element<Svg>(this, "icons/Question.svg");
unknown_svg_el->set_position(Position::Absolute);
unknown_svg_el->set_width(32);
unknown_svg_el->set_height(32);
unknown_svg_el->set_top(50.0f, recompui::Unit::Percent);
unknown_svg_el->set_left(50.0f, recompui::Unit::Percent);
unknown_svg_el->set_translate_2D(-50.0f, -50.0f, recompui::Unit::Percent);
set_binding(mapped_binding);
recording_parent = context.create_element<Element>(this);
recording_circle = context.create_element<Element>(recording_parent);
@@ -101,11 +109,15 @@ namespace recompui {
}
void BindingButton::apply_binding_style() {
bound_text_el->set_position(Position::Absolute);
bound_text_el->set_top(50.0f, recompui::Unit::Percent);
bound_text_el->set_left(50.0f, recompui::Unit::Percent);
bound_text_el->set_translate_2D(-50.0f, -50.0f, recompui::Unit::Percent);
bound_text_el->set_font_family("promptfont");
bound_text_el->set_font_size(40.0f);
bound_text_el->set_font_size(32.0f);
bound_text_el->set_font_style(FontStyle::Normal);
bound_text_el->set_font_weight(400);
bound_text_el->set_line_height(40.0f);
bound_text_el->set_line_height(32.0f);
bound_text_el->set_opacity(1);
}
@@ -115,7 +127,15 @@ namespace recompui {
void BindingButton::set_binding(const std::string &binding) {
this->mapped_binding = binding;
bound_text_el->set_text(mapped_binding);
if (binding == recompui::unknown_input) {
bound_text_el->set_text("");
bound_text_el->set_display(Display::None);
unknown_svg_el->set_display(Display::Block);
} else {
bound_text_el->set_text(mapped_binding);
bound_text_el->set_display(Display::Block);
unknown_svg_el->set_display(Display::None);
}
}
void BindingButton::set_is_binding(bool is_binding) {
@@ -123,10 +143,12 @@ namespace recompui {
if (is_binding) {
bound_text_el->set_opacity(0);
unknown_svg_el->set_opacity(0);
recording_parent->set_opacity(1);
queue_update();
} else {
bound_text_el->set_opacity(1);
unknown_svg_el->set_opacity(1);
recording_parent->set_opacity(0);
}
}
+1
View File
@@ -10,6 +10,7 @@ namespace recompui {
std::string mapped_binding; // promptfont representation of the binding.
Element *bound_text_el;
Svg *unknown_svg_el;
Element *recording_parent;
Element *recording_circle;
Element *recording_edge;
+4
View File
@@ -1,5 +1,6 @@
#include "ui_button.h"
#include "ui_label.h"
#include "ui_pseudo_border.h"
#include <cassert>
@@ -48,6 +49,9 @@ namespace recompui {
ContextId context = get_current_context();
auto focus_border = context.create_element<FocusBorder>(this, true);
focus_border->set_border_radius(theme::border::radius_md + theme::border::width * 4.0f);
switch (size) {
case ButtonSize::Small: {
auto label = context.create_element<Label>(this, text, LabelStyle::Annotation);
+4 -4
View File
@@ -17,8 +17,8 @@ namespace recompui {
for (const auto &function : clicked_callbacks) {
function(click.x, click.y);
}
break;
}
break;
}
case EventType::MouseButton: {
if (is_enabled()) {
@@ -28,8 +28,8 @@ namespace recompui {
function(mousebutton.x, mousebutton.y);
}
}
break;
}
break;
}
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active && is_enabled());
@@ -54,8 +54,8 @@ namespace recompui {
for (const auto &function : dragged_callbacks) {
function(drag.x, drag.y, drag.phase);
}
break;
}
break;
}
default:
break;
@@ -74,4 +74,4 @@ namespace recompui {
dragged_callbacks.emplace_back(callback);
}
};
};
+15 -10
View File
@@ -51,6 +51,14 @@ namespace recompui {
right = context.create_element<Container>(this, FlexDirection::Row, JustifyContent::FlexEnd, 0);
set_header_footer_side_styles(right);
}
void ConfigHeaderFooter::hide() {
set_display(Display::None);
}
void ConfigHeaderFooter::show() {
set_display(Display::Flex);
}
static void set_config_body_side_styles(Element *el) {
el->set_flex_grow(1.0f);
@@ -78,7 +86,7 @@ namespace recompui {
set_config_body_side_styles(right);
}
ConfigPage::ConfigPage(Element *parent) : Element(parent, 0, "div", false) {
ConfigPage::ConfigPage(Element *parent, uint32_t events_enabled) : Element(parent, events_enabled, "div", false) {
set_display(Display::Flex);
set_position(Position::Relative);
@@ -92,40 +100,37 @@ namespace recompui {
set_width(100.0f, Unit::Percent);
set_height(100.0f, Unit::Percent);
set_border_top_width(theme::border::width);
set_border_top_color(theme::color::BorderSoft);
ContextId context = get_current_context();
header = context.create_element<ConfigHeaderFooter>(this, true);
header->set_visibility(Visibility::Hidden);
header->hide();
body = context.create_element<ConfigBody>(this);
set_border_bottom_left_radius(theme::border::radius_lg);
set_border_bottom_right_radius(theme::border::radius_lg);
footer = context.create_element<ConfigHeaderFooter>(this, false);
footer->set_visibility(Visibility::Hidden);
footer->hide();
}
ConfigHeaderFooter* ConfigPage::add_header() {
header->set_visibility(Visibility::Visible);
header->show();
return header;
}
void ConfigPage::hide_header() {
header->set_visibility(Visibility::Hidden);
header->hide();
}
ConfigHeaderFooter* ConfigPage::add_footer() {
footer->set_visibility(Visibility::Visible);
footer->show();
set_border_bottom_left_radius(0);
set_border_bottom_right_radius(0);
return footer;
}
void ConfigPage::hide_footer() {
footer->set_visibility(Visibility::Hidden);
footer->hide();
set_border_bottom_left_radius(theme::border::radius_lg);
set_border_bottom_right_radius(theme::border::radius_lg);
}
+4 -1
View File
@@ -8,12 +8,15 @@ namespace recompui {
Element *left;
Element *right;
bool is_header;
Display prev_display = Display::Block;
std::string_view get_type_name() override { return "ConfigHeaderFooter"; }
public:
ConfigHeaderFooter(Element *parent, bool is_header);
Element *get_left() { return left; }
Element *get_right() { return right; }
void hide();
void show();
};
class ConfigBody : public Element {
@@ -37,7 +40,7 @@ namespace recompui {
std::string_view get_type_name() override { return "ConfigPage"; }
public:
ConfigPage(Element *parent);
ConfigPage(Element *parent, uint32_t events_enabled = 0);
ConfigHeaderFooter *add_header();
void hide_header();
ConfigHeaderFooter *add_footer();
+509 -15
View File
@@ -9,6 +9,451 @@
namespace recompui {
Element *Element::get_nav_parent() {
Element *cur = parent;
while (cur != nullptr) {
if (cur->is_nav_container) {
return cur;
}
cur = cur->parent;
}
return nullptr;
}
void Element::set_as_navigation_container(NavigationType nav_type) {
is_nav_container = true;
this->nav_type = nav_type;
// set_border_left_width(1.0f);
// set_border_left_color(theme::color::Warning);
Element *parent_nav = get_nav_parent();
if (parent_nav != nullptr) {
parent_nav->nav_children.push_back(this);
}
}
void Element::set_as_primary_focus(bool is_primary_focus) {
this->is_primary_focus = is_primary_focus;
// if (is_primary_focus) {
// set_border_right_width(1.0f);
// set_border_right_color(theme::color::SuccessD);
// } else {
// set_border_right_width(1.0f);
// set_border_right_color(theme::color::WarningD);
// }
}
struct RmlPosSize {
Rml::Vector2f position;
Rml::Vector2f size;
Rml::Vector2f center;
RmlPosSize(Rml::Vector2f position, const Rml::Box& box) : position(position), size(box.GetSize()) {
center = position + (size * 0.5f);
}
float get_distance_sq(const RmlPosSize& other) const {
auto delta = center - other.center;
return delta.SquaredMagnitude();
}
bool can_navigate_vertical(const RmlPosSize& other, int dir) const {
if (position.x + size.x < other.position.x + 1) {
return false;
}
if (position.x + 1 > other.position.x + other.size.x) {
return false;
}
// bool aligns_horizontally = (
// std::abs(position.x - other.position.x) < 5 &&
// std::abs((position.x + size.x) - (other.position.x + other.size.x)) < 5
// );
// bool aligns_horizontally = (
// // left side of this box is to the left of the other
// position.x < other.position.x + other.size.x &&
// // right side of this box is to the right of the other
// position.x + size.x > other.position.x
// );
// if (!aligns_horizontally) {
// return false;
// }
if (dir == 1) {
// Navigating down
return is_above(other);
} else {
// Navigating up
return is_below(other);
}
}
bool can_navigate_horizontal(const RmlPosSize& other, int dir) const {
if (std::abs(position.x - other.position.x) < 5) {
return false;
}
bool aligns_vertically = (
// top side of this box is above the other
position.y < other.position.y + other.size.y &&
// bottom side of this box is below the other
position.y + size.y > other.position.y
);
if (!aligns_vertically) {
return false;
}
if (dir == 1) {
// Navigating right
return is_left_of(other);
} else {
// Navigating left
return is_right_of(other);
}
}
bool is_left_of(const RmlPosSize& other) const {
return position.x <= other.position.x;
}
bool is_right_of(const RmlPosSize& other) const {
return position.x >= other.position.x;
}
bool is_above(const RmlPosSize& other) const {
return position.y <= other.position.y;
}
bool is_below(const RmlPosSize& other) const {
return position.y >= other.position.y;
}
float vertical_distance(const RmlPosSize& other) const {
return std::min(
std::abs(position.y - (other.position.y + other.size.y)),
std::abs((position.y + size.y) - other.position.y)
);
}
float horizontal_distance(const RmlPosSize& other) const {
return std::min(
std::abs(position.x - (other.position.x + other.size.x)),
std::abs((position.x + size.x) - other.position.x)
);
}
};
Element::CanFocus Element::is_focusable() {
if (!base->IsVisible()) {
return CanFocus::NoAndNoChildren;
}
if (enabled == false) {
return CanFocus::NoAndNoChildren;
}
const Rml::ComputedValues& computed = base->GetComputedValues();
if (computed.focus() == Rml::Style::Focus::None) {
return CanFocus::NoAndNoChildren;
}
if (computed.tab_index() == Rml::Style::TabIndex::Auto) {
return CanFocus::Yes;
}
return CanFocus::No;
}
Element *Element::get_first_focusable_child() {
for (auto child : children) {
CanFocus res = child->is_focusable();
if (res == CanFocus::Yes) {
return child;
} else if (res == CanFocus::NoAndNoChildren) {
continue; // Skip this child, it has no focusable children.
} else {
Element *focusable_child = child->get_first_focusable_child();
if (focusable_child != nullptr) {
return focusable_child;
}
}
}
return nullptr;
}
int get_element_index(Element *el, std::vector<Element *> elements) {
for (int i = 0; i < static_cast<int>(elements.size()); i++) {
if (el == elements[i]) {
return i;
}
}
return -1;
}
Element *Element::try_grid_navigation(
int nav_dir,
int cur_element_index
) {
auto *grid_parent = get_nav_parent();
NavigationType parent_check_type = nav_type == NavigationType::GridCol ? NavigationType::GridRow : NavigationType::GridCol;
if (grid_parent == nullptr || grid_parent->nav_type != parent_check_type) {
return nullptr;
}
int parent_element_index = get_element_index(this, grid_parent->nav_children);
if (parent_element_index < 0) {
return nullptr;
}
int next_grid_index = parent_element_index + nav_dir;
if (next_grid_index >= 0 && next_grid_index < grid_parent->nav_children.size()) {
Element *adjacent = grid_parent->nav_children[next_grid_index];
if (cur_element_index >= 0 && cur_element_index < adjacent->nav_children.size()) {
return adjacent->nav_children[cur_element_index];
}
}
return nullptr;
}
Element *Element::try_get_nav_direction(
int vertical_nav,
int horizontal_nav,
Element *cur_nav_child
) {
int cur_element_index = get_element_index(cur_nav_child, nav_children);
if (cur_element_index == -1) {
printf("ERROR: could not find nav child!\n");
return nullptr;
}
int num_nav_children = static_cast<int>(nav_children.size());
int next_nav_index = -1;
switch (nav_type) {
case NavigationType::Auto:
break;
case NavigationType::GridCol:
if (horizontal_nav != 0) {
return try_grid_navigation(horizontal_nav, cur_element_index);
}
// fallthrough
case NavigationType::Vertical:
if (vertical_nav == 0) {
return nullptr;
}
cur_element_index += vertical_nav;
if (cur_element_index >= 0 && cur_element_index < num_nav_children) {
return nav_children[cur_element_index];
}
return nullptr;
case NavigationType::GridRow:
if (vertical_nav != 0) {
return try_grid_navigation(vertical_nav, cur_element_index);
}
// fallthrough
case NavigationType::Horizontal:
if (horizontal_nav == 0) {
return nullptr;
}
cur_element_index += horizontal_nav;
if (cur_element_index >= 0 && cur_element_index < num_nav_children) {
return nav_children[cur_element_index];
}
return nullptr;
}
auto focused_box = cur_nav_child->base->GetBox();
RmlPosSize this_pos_size(cur_nav_child->base->GetAbsoluteOffset(), focused_box);
Element *next_focus = nullptr;
if (nav_children.size() > 0) {
float best_dist = std::numeric_limits<float>::max();
for (auto &nav_sibling : nav_children) {
if (nav_children[cur_element_index] == nav_sibling) {
continue;
}
RmlPosSize sibling_pos_size(nav_sibling->base->GetAbsoluteOffset(), nav_sibling->base->GetBox());
if (vertical_nav != 0 && this_pos_size.can_navigate_vertical(sibling_pos_size, vertical_nav)) {
float dist = this_pos_size.vertical_distance(sibling_pos_size);
if (dist < best_dist) {
best_dist = dist;
next_focus = nav_sibling;
}
} else if (horizontal_nav != 0 && this_pos_size.can_navigate_horizontal(sibling_pos_size, horizontal_nav)) {
float dist = this_pos_size.horizontal_distance(sibling_pos_size);
if (dist < best_dist) {
best_dist = dist;
next_focus = nav_sibling;
}
}
}
}
return next_focus;
}
void Element::get_all_focusable_children(Element *nav_parent) {
for (auto child : children) {
CanFocus res = child->is_focusable();
if (res == CanFocus::Yes) {
nav_parent->nav_children.push_back(child);
} else if (res == CanFocus::NoAndNoChildren) {
continue; // Skip this child, it has no focusable children.
} else {
child->get_all_focusable_children(nav_parent);
}
}
}
void Element::build_navigation(Element *nav_parent, Element *cur_focus_element) {
if (!base->IsVisible()) {
return;
}
for (auto &child : children) {
if (child == cur_focus_element) {
nav_parent->nav_children.push_back(child);
continue;
}
if (!child->base->IsVisible() || !child->enabled) {
continue;
}
if (child->is_focusable() == CanFocus::Yes) {
// End of the line because it is focusable itself
nav_parent->nav_children.push_back(child);
} else if (child->is_nav_container) {
nav_parent->nav_children.push_back(child);
child->nav_children.clear();
child->build_navigation(child, cur_focus_element);
// didn't find any nav children, check for focus elements
if (child->nav_children.size() == 0) {
child->get_all_focusable_children(child);
}
// didn't find any focus elements
if (child->nav_children.size() == 0) {
nav_parent->nav_children.pop_back();
}
} else {
child->build_navigation(nav_parent, cur_focus_element);
}
}
}
Element *Element::get_closest_element(std::vector<Element *> &elements) {
if (elements.empty()) {
return nullptr;
}
auto this_box = base->GetBox();
RmlPosSize this_pos_size(base->GetAbsoluteOffset(), this_box);
Element *closest = nullptr;
float closest_distance = std::numeric_limits<float>::max();
for (auto &element : elements) {
auto element_box = element->base->GetBox();
RmlPosSize element_pos_size(element->base->GetAbsoluteOffset(), element_box);
float distance = this_pos_size.get_distance_sq(element_pos_size);
if (distance < closest_distance) {
closest_distance = distance;
closest = element;
}
}
return closest;
}
bool Element::handle_navigation_event(Rml::Event &event) {
if (
is_root_document &&
event.GetId() == Rml::EventId::Keydown &&
event.GetPhase() == Rml::EventPhase::Bubble &&
event.IsPropagating()
) {
int key_identifier = event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN);
if (
key_identifier != Rml::Input::KI_LEFT &&
key_identifier != Rml::Input::KI_RIGHT &&
key_identifier != Rml::Input::KI_UP &&
key_identifier != Rml::Input::KI_DOWN
) {
return false;
}
std::string nav_key_name;
switch (key_identifier) {
case Rml::Input::KI_UP: nav_key_name = "up"; break;
case Rml::Input::KI_DOWN: nav_key_name = "down"; break;
case Rml::Input::KI_LEFT: nav_key_name = "left"; break;
case Rml::Input::KI_RIGHT: nav_key_name = "right"; break;
}
printf("Navigation key pressed: %-5s (%d)\n", nav_key_name.c_str(), key_identifier);
int vertical_nav = 0;
int horizontal_nav = 0;
switch (key_identifier) {
case Rml::Input::KI_UP: vertical_nav = -1; break;
case Rml::Input::KI_DOWN: vertical_nav = 1; break;
case Rml::Input::KI_LEFT: horizontal_nav = -1; break;
case Rml::Input::KI_RIGHT: horizontal_nav = 1; break;
}
auto ctx = get_current_context();
auto navigation_from_element = ctx.get_focused_element();
if (navigation_from_element == nullptr) {
printf(" No focused element, cannot navigate.\n");
return false;
}
nav_children.clear();
build_navigation(this, navigation_from_element);
printf("ROOT\n");
print_nav_hierarchy(0);
Element *nav_parent = navigation_from_element->get_nav_parent();
while (nav_parent != nullptr) {
Element *next_focus = nav_parent->try_get_nav_direction(
vertical_nav,
horizontal_nav,
navigation_from_element
);
// If found, dive into the next focus element until finding the bottom most nav container
if (next_focus != nullptr) {
while (next_focus->nav_children.size() > 0) {
bool found_primary = false;
for (auto &child : next_focus->nav_children) {
if (child->is_primary_focus) {
next_focus = child;
found_primary = true;
printf(" Found primary focus child: %s\n", child->get_id().c_str());
break;
}
}
if (!found_primary) {
next_focus = navigation_from_element->get_closest_element(next_focus->nav_children);
}
}
}
if (next_focus != nullptr) {
event.StopPropagation();
next_focus->focus();
printf(" Navigated to next focus element: %s\n", next_focus->get_id().c_str());
return true;
}
navigation_from_element = nav_parent;
nav_parent = nav_parent->get_nav_parent();
printf(" No next focus found, moving up to parent: %s\n", navigation_from_element->get_id().c_str());
}
printf(" No next focus found, reached root document.\n");
return false;
} else {
return false;
}
}
Element::Element(Rml::Element *base) {
assert(base != nullptr);
@@ -114,7 +559,7 @@ void Element::register_event_listeners(uint32_t events_enabled) {
base->AddEventListener(Rml::EventId::Change, this);
}
if (events_enabled & Events(EventType::Navigate)) {
if (events_enabled & Events(EventType::Navigate, EventType::MenuAction)) {
base->AddEventListener(Rml::EventId::Keydown, this);
}
}
@@ -146,7 +591,11 @@ void Element::propagate_disabled(bool disabled) {
bool attribute_state = disabled_from_parent || !enabled;
if (disabled_attribute != attribute_state) {
disabled_attribute = attribute_state;
base->SetAttribute("disabled", attribute_state);
if (disabled_attribute) {
base->SetAttribute("disabled", true);
} else {
base->RemoveAttribute("disabled");
}
if (events_enabled & Events(EventType::Enable)) {
handle_event(Event::enable_event(!attribute_state));
@@ -220,22 +669,39 @@ void Element::ProcessEvent(Rml::Event &event) {
}
}
break;
case Rml::EventId::Keydown:
switch ((Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0)) {
case Rml::Input::KeyIdentifier::KI_LEFT:
handle_event(Event::navigate_event(NavDirection::Left));
break;
case Rml::Input::KeyIdentifier::KI_UP:
handle_event(Event::navigate_event(NavDirection::Up));
break;
case Rml::Input::KeyIdentifier::KI_RIGHT:
handle_event(Event::navigate_event(NavDirection::Right));
break;
case Rml::Input::KeyIdentifier::KI_DOWN:
handle_event(Event::navigate_event(NavDirection::Down));
case Rml::EventId::Keydown: {
auto rml_key = (Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0);
if (events_enabled & Events(EventType::Navigate)) {
if (handle_navigation_event(event)) {
break;
}
switch (rml_key) {
case Rml::Input::KeyIdentifier::KI_LEFT:
handle_event(Event::navigate_event(NavDirection::Left));
break;
case Rml::Input::KeyIdentifier::KI_UP:
handle_event(Event::navigate_event(NavDirection::Up));
break;
case Rml::Input::KeyIdentifier::KI_RIGHT:
handle_event(Event::navigate_event(NavDirection::Right));
break;
case Rml::Input::KeyIdentifier::KI_DOWN:
handle_event(Event::navigate_event(NavDirection::Down));
printf("Navigate down\n");
break;
}
}
if (events_enabled & Events(EventType::MenuAction)) {
MenuAction action = menu_action_mapping::menu_action_from_rml_key(rml_key);
if (action != MenuAction::None) {
handle_event(Event::menu_action_event(action));
}
}
break;
}
case Rml::EventId::Drag:
handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Move));
break;
@@ -288,6 +754,14 @@ void Element::ProcessEvent(Rml::Event &event) {
}
}
void Element::set_as_root_document(NavigationType nav_type) {
is_root_document = true;
is_nav_container = true;
this->nav_type = nav_type;
register_event_listeners(recompui::Events(recompui::EventType::Navigate, recompui::EventType::Focus));
}
void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) {
base->SetAttribute(attribute_key, attribute_value);
}
@@ -405,6 +879,15 @@ void Element::set_text(std::string_view text) {
}
}
void Element::set_text_unsafe(std::string_view text) {
if (can_set_text) {
get_current_context().queue_set_text(this, std::string(text));
}
else {
assert(false && "Attempted to set text of an element that cannot have its text set.");
}
}
std::string Element::get_input_text() {
return base->GetAttribute("value", std::string{});
}
@@ -573,4 +1056,15 @@ bool Element::is_pseudo_class_set(Rml::String pseudo_class) {
return base->IsPseudoClassSet(pseudo_class);
}
void Element::scroll_into_view() {
if (base == nullptr) {
return;
}
Rml::ScrollIntoViewOptions options;
options.vertical = Rml::ScrollAlignment::Nearest;
base->ScrollIntoView(options);
}
} // namespace recompui
+53
View File
@@ -18,6 +18,14 @@ struct UICallback {
using ElementValue = std::variant<uint32_t, float, double, std::monostate>;
enum class NavigationType {
Auto,
Vertical,
Horizontal,
GridCol,
GridRow
};
class ContextId;
class Element : public Style, public Rml::EventListener {
friend ContextId create_context(const std::filesystem::path& path);
@@ -40,6 +48,13 @@ private:
bool disabled_attribute = false;
bool disabled_from_parent = false;
bool can_set_text = false;
bool is_root_document = false;
bool is_nav_container = false;
NavigationType nav_type = NavigationType::Auto;
bool is_primary_focus = false;
std::vector<Element *> nav_children;
void add_child(Element *child);
void register_event_listeners(uint32_t events_enabled);
@@ -47,12 +62,38 @@ private:
void propagate_disabled(bool disabled);
void handle_event(const Event &e);
void set_id(const std::string& new_id);
void set_as_root_document(NavigationType nav_type = NavigationType::Vertical);
Element *try_grid_navigation(
int nav_dir,
int cur_element_index
);
Element *try_get_nav_direction(
int vertical_nav,
int horizontal_nav,
Element *cur_nav_child
);
// Style overrides.
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override;
bool handle_navigation_event(Rml::Event &event);
// Rml::EventListener overrides.
void ProcessEvent(Rml::Event &event) override final;
Element *get_nav_parent();
void get_all_focusable_children(Element *nav_parent);
void build_navigation(Element *nav_parent, Element *cur_focus_element);
void print_nav_hierarchy(int depth) {
for (auto &child : nav_children) {
for (int i = 0; i <= depth; i++) {
printf(" ");
}
printf("%s\n", child->get_id().c_str());
child->print_nav_hierarchy(depth + 1);
}
}
Element *get_closest_element(std::vector<Element *> &elements);
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);
@@ -71,12 +112,15 @@ public:
bool remove_child(ResourceId child, bool remove_from_context = true);
bool remove_child(Element *child, bool remove_from_context = true) { return remove_child(child->get_resource_id(), remove_from_context); }
void set_parent(Element *new_parent);
Element *get_parent() const { return parent; }
void add_style(Style *style, std::string_view style_name);
void add_style(Style *style, const std::initializer_list<std::string_view> &style_names);
Element get_element_with_tag_name(std::string_view tag_name);
void set_enabled(bool enabled);
bool is_enabled() const;
void set_text(std::string_view text);
// Only use if you can ensure text is directly from a trusted source. Sets inner rml contents which can create new elements.
void set_text_unsafe(std::string_view text);
std::string get_input_text();
void set_input_text(std::string_view text);
void set_src(std::string_view src);
@@ -103,9 +147,18 @@ public:
void set_input_value_double(double val) { set_input_value(val); }
const std::string& get_id() { return id; }
bool is_pseudo_class_set(Rml::String pseudo_class);
void scroll_into_view();
void set_as_navigation_container(NavigationType nav_type);
void set_as_primary_focus(bool is_primary_focus = true);
Element *select_add_option(std::string_view text, std::string_view value);
void select_set_selection(std::string_view option_value);
std::vector<Element *> *get_nav_children() { return &nav_children; }
enum class CanFocus { No, Yes, NoAndNoChildren };
CanFocus is_focusable();
Element *get_first_focusable_child();
};
void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback);
+5
View File
@@ -1,4 +1,5 @@
#include "ui_icon_button.h"
#include "ui_pseudo_border.h"
#include <cassert>
@@ -12,6 +13,7 @@ namespace recompui {
enable_focus();
set_display(Display::Flex);
set_position(Position::Relative);
set_align_items(AlignItems::Center);
set_justify_content(JustifyContent::Center);
set_width(float_size_internal);
@@ -58,6 +60,9 @@ namespace recompui {
ContextId context = get_current_context();
auto focus_border = context.create_element<FocusBorder>(this, true);
focus_border->set_border_radius(static_cast<float>(size) + theme::border::width * 4.0f);
svg = context.create_element<Svg>(this, svg_src);
svg->set_width(icon_size);
svg->set_color(theme::color::TextDim);
+13 -18
View File
@@ -1,4 +1,5 @@
#include "ui_label.h"
#include "ui_theme.h"
#include <cassert>
@@ -7,36 +8,30 @@ namespace recompui {
Label::Label(Element *parent, LabelStyle label_style) : Element(parent, 0U, "div", true) {
switch (label_style) {
case LabelStyle::Annotation:
set_font_size(18.0f);
set_letter_spacing(2.52f);
set_line_height(18.0f);
set_font_weight(400);
set_typography(theme::Typography::LabelXS);
break;
case LabelStyle::Small:
set_font_size(20.0f);
set_letter_spacing(0.0f);
set_line_height(20.0f);
set_font_weight(400);
set_typography(theme::Typography::Body);
break;
case LabelStyle::Normal:
set_font_size(28.0f);
set_letter_spacing(3.08f);
set_line_height(28.0f);
set_font_weight(700);
set_typography(theme::Typography::LabelMD);
break;
case LabelStyle::Large:
set_font_size(36.0f);
set_letter_spacing(2.52f);
set_line_height(36.0f);
set_font_weight(700);
set_typography(theme::Typography::Header3);
break;
}
set_font_style(FontStyle::Normal);
}
Label::Label(Element *parent, const std::string &text, LabelStyle label_style) : Label(parent, label_style) {
set_text(text);
}
Label::Label(Element *parent, theme::Typography typography) : Element(parent, 0U, "div", true) {
set_typography(typography);
}
Label::Label(Element *parent, const std::string &text, theme::Typography typography) : Label(parent, typography) {
set_text(text);
}
};
+3 -1
View File
@@ -17,6 +17,8 @@ namespace recompui {
public:
Label(Element *parent, LabelStyle label_style);
Label(Element *parent, const std::string &text, LabelStyle label_style);
Label(Element *parent, theme::Typography typography);
Label(Element *parent, const std::string &text, theme::Typography typography);
};
} // namespace recompui
} // namespace recompui
+272
View File
@@ -0,0 +1,272 @@
#include "ui_modal.h"
#include "ui_element.h"
namespace recompui {
class ModalOverlay : public Element {
protected:
Modal *parent_modal;
void process_event(const Event &e) override {
if (e.type != EventType::Click) {
return;
}
// parent_modal->close();
}
std::string_view get_type_name() override { return "ModalOverlay"; }
public:
ModalOverlay(Modal *parent) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "div", false) {
this->parent_modal = parent;
set_display(Display::Block);
set_position(Position::Absolute);
set_top(0);
set_right(0);
set_bottom(0);
set_left(0);
set_background_color(theme::color::BGOverlay);
}
~ModalOverlay() {
}
};
constexpr float modal_page_padding = 64.0f;
constexpr float modal_page_base_height = 1080.0f;
constexpr float modal_height = modal_page_base_height - (modal_page_padding * 2.0f);
constexpr float modal_width = modal_page_base_height * 16.0f / 9.0f;
Modal::Modal(
Element *parent,
recompui::ContextId modal_root_context,
ModalType modal_type
) : Element(parent, Events(EventType::MenuAction), "div", false)
{
this->modal_root_context = modal_root_context;
this->modal_type = modal_type;
recompui::ContextId context = modal_root_context;
set_display(Display::None);
set_position(Position::Absolute);
set_top(0);
set_right(0);
set_bottom(0);
set_left(0);
ModalOverlay* modal_overlay = context.create_element<ModalOverlay>(this);
Element* modal_whole_page_wrapper = context.create_element<Element>(modal_overlay);
modal_whole_page_wrapper->set_display(Display::Flex);
modal_whole_page_wrapper->set_position(Position::Absolute);
modal_whole_page_wrapper->set_top(0);
modal_whole_page_wrapper->set_right(0);
modal_whole_page_wrapper->set_bottom(0);
modal_whole_page_wrapper->set_left(0);
modal_whole_page_wrapper->set_padding(modal_page_padding);
modal_whole_page_wrapper->set_align_items(AlignItems::Center);
modal_whole_page_wrapper->set_justify_content(JustifyContent::Center);
modal_element = context.create_element<Element>(modal_whole_page_wrapper);
modal_element->set_display(Display::Flex);
modal_element->set_position(Position::Relative);
modal_element->set_flex(1.0f, 1.0f);
modal_element->set_flex_basis(100, Unit::Percent);
modal_element->set_flex_direction(FlexDirection::Column);
modal_element->set_width(100, Unit::Percent);
modal_element->set_max_width(modal_width);
modal_element->set_height(100, Unit::Percent);
modal_element->set_margin_auto();
modal_element->set_border_width(theme::border::width);
modal_element->set_border_radius(theme::border::radius_lg);
modal_element->set_border_color(theme::color::Border);
modal_element->set_background_color(theme::color::ModalOverlay);
header = context.create_element<ConfigHeaderFooter>(modal_element, true);
header->set_padding_top(0.0f);
header->set_padding_bottom(0.0f);
header->set_padding_left(8.0f);
header->set_padding_right(8.0f);
header->set_border_top_left_radius(theme::border::radius_lg);
header->set_border_top_right_radius(theme::border::radius_lg);
body = context.create_element<Element>(modal_element);
body->set_display(Display::Flex);
body->set_position(Position::Relative);
body->set_flex_grow(1.0f);
body->set_flex_shrink(1.0f);
body->set_flex_basis_auto();
body->set_flex_direction(FlexDirection::Row);
body->set_width(100.0f, Unit::Percent);
}
Modal::~Modal() {
}
void Modal::open() {
if (!recompui::is_context_shown(modal_root_context)) {
recompui::show_context(modal_root_context, "");
}
is_open = true;
set_display(Display::Block);
// queue_update();
if (tabs != nullptr) {
tabs->focus_on_active_tab();
on_tab_change(tabs->get_active_tab());
}
}
void Modal::close() {
if (current_tab_index < tab_contexts.size() && current_tab_index >= 0) {
if (tab_contexts[current_tab_index]->can_be_closed()) {
tab_contexts[current_tab_index]->on_tab_close();
} else {
return;
}
}
if (recompui::is_context_shown(modal_root_context)) {
set_display(Display::None);
recompui::hide_context(modal_root_context);
}
is_open = false;
if (on_close_callback != nullptr) {
on_close_callback();
}
}
void Modal::process_event(const Event &e) {
switch (e.type) {
case EventType::Update: {
if (previous_tab_index != current_tab_index) {
body->clear_children();
Element* tab_contents = tab_contexts[current_tab_index]->create_tab_contents(modal_root_context, body);
previous_tab_index = current_tab_index;
}
queue_update();
break;
}
case EventType::MenuAction: {
auto action = std::get<EventMenuAction>(e.variant).action;
switch (action) {
case MenuAction::Accept:
break;
case MenuAction::Apply:
break;
case MenuAction::Back: {
if (tabs != nullptr && tabs->get_active_tab_element() != nullptr) {
tabs->get_active_tab_element()->focus();
}
break;
}
case MenuAction::Toggle: {
close();
break;
}
case MenuAction::TabLeft: {
navigate_tab_direction(-1);
break;
}
case MenuAction::TabRight: {
navigate_tab_direction(1);
break;
}
}
break;
}
}
}
void Modal::navigate_tab_direction(int direction) {
if (tab_contexts.size() == 0) {
return;
}
int next_index = current_tab_index + direction;
if (next_index < 0) {
next_index = tab_contexts.size() - 1;
} else if (next_index >= tab_contexts.size()) {
next_index = 0;
}
set_selected_tab(next_index);
if (tabs != nullptr && tabs->get_active_tab_element() != nullptr) {
tabs->get_active_tab_element()->focus();
}
}
void Modal::set_menu_action_callback(MenuAction action, std::function<void()> callback) {
menu_action_callbacks[action] = callback;
}
void Modal::on_tab_change(int tab_index) {
if (current_tab_index < tab_contexts.size() && current_tab_index >= 0) {
if (tab_contexts[current_tab_index]->can_be_closed()) {
tab_contexts[current_tab_index]->on_tab_close();
current_tab_index = tab_index;
} else if (tabs != nullptr) {
tabs->set_active_tab(current_tab_index);
}
} else {
current_tab_index = tab_index;
}
if (current_tab_index < tab_contexts.size() && current_tab_index >= 0 && tabs != nullptr) {
tab_contexts[current_tab_index]->set_nav_up(tabs->get_active_tab_element());
}
// body->clear_children();
// Element* tab_contents = tab_contexts[tab_index]->create_tab_contents(modal_root_context, body);
}
void Modal::set_selected_tab(int tab_index) {
if (tabs != nullptr) {
// calls Modal::on_tab_change internally
tabs->set_active_tab(tab_index);
}
}
void Modal::add_tab(TabContext *tab_context) {
tab_context->set_parent_modal(this);
tab_context->on_init();
tab_contexts.push_back(tab_context);
ContextId context = get_current_context();
if (tabs == nullptr) {
header->set_padding_left(0.0f); // tabs hug to left side
tabs = context.create_element<TabSet>(header->get_left());
tabs->set_change_tab_callback([this](int tab_index) {
this->on_tab_change(tab_index);
});
}
tabs->add_tab(tab_context->tab_name);
}
Modal *Modal::create_modal(ModalType modal_type) {
ContextId new_context = recompui::create_context();
new_context.open();
auto doc = new_context.get_root_element();
Modal *modal = new_context.create_element<Modal>(doc, new_context, modal_type);
new_context.close();
return modal;
}
void Modal::set_first_focusable_below_tabs(Element *element) {
if (tabs != nullptr) {
tabs->set_nav_down(element);
}
}
void Modal::set_on_close_callback(std::function<void()> callback) {
on_close_callback = callback;
}
void TabContext::apply_set_first_focusable_below_tabs_callback() {
set_first_focusable_below_tabs = [this](Element *element) {
if (this->parent_modal != nullptr) {
this->parent_modal->set_first_focusable_below_tabs(element);
}
};
}
} // namespace recompui
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include "recomp_ui.h"
#include "ui_element.h"
#include "ui_config_page.h"
#include "ui_tab_set.h"
namespace recompui {
class Modal;
enum class ModalType {
Fullscreen,
Prompt
};
// Function in Modal::set_first_focusable_below_tabs, reports the element to focus on when navigating down from the tabs
using set_first_focusable_below_tabs_t = std::function<void(Element*)>;
class TabContext {
private:
Element *upper_focusable = nullptr;
Modal *parent_modal = nullptr;
protected:
set_first_focusable_below_tabs_t set_first_focusable_below_tabs = nullptr;
public:
std::string tab_name;
TabContext(std::string name) : tab_name(name) {};
virtual ~TabContext() = default;
virtual Element* create_tab_contents(recompui::ContextId context, Element* parent) = 0;
virtual bool can_be_closed() { return true; }
virtual void on_tab_close() {}
virtual void on_init() {}
virtual void set_nav_up(Element *element) { upper_focusable = element; }
Element* get_nav_up() const { return upper_focusable; }
Modal *get_parent_modal() const { return parent_modal; }
void set_parent_modal(Modal *modal) { parent_modal = modal; }
void apply_set_first_focusable_below_tabs_callback();
};
class Modal : public Element {
protected:
bool is_open = false;
Element *modal_element = nullptr;
ConfigHeaderFooter *header = nullptr;
Element *body = nullptr;
ModalType modal_type;
std::vector<TabContext*> tab_contexts;
std::function<void()> on_close_callback;
std::unordered_map<MenuAction, std::function<void()>> menu_action_callbacks;
TabSet *tabs = nullptr;
int previous_tab_index = -1;
int current_tab_index = -1;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Modal"; }
void on_tab_change(int tab_index);
void navigate_tab_direction(int direction);
public:
recompui::ContextId modal_root_context;
Modal(Element *parent, recompui::ContextId modal_root_context, ModalType modal_type);
static Modal *create_modal(ModalType modal_type = ModalType::Fullscreen);
virtual ~Modal();
void open();
void close();
bool is_open_now() const { return is_open; }
void add_tab(TabContext *tab_context);
void set_selected_tab(int tab_index);
ConfigHeaderFooter *get_header() { return header; }
Element *get_body() { return body; }
Element *get_active_tab() { return tabs != nullptr ? static_cast<Tab*>(tabs->get_active_tab_element()) : nullptr; }
// set_first_focusable_below_tabs_t
void set_first_focusable_below_tabs(Element *element);
void set_on_close_callback(std::function<void()> callback);
void set_menu_action_callback(MenuAction action, std::function<void()> callback);
};
} // namespace recompui
+43
View File
@@ -0,0 +1,43 @@
#include "ui_pseudo_border.h"
#include "../ui_utils.h"
namespace recompui {
PseudoBorder::PseudoBorder(Element *parent, bool inset, float border_width, float border_outset_distance) : Element(parent) {
if (border_width > 0) {
set_border_width(border_width);
} else {
set_border_width(theme::border::width);
border_width = theme::border::width;
}
set_position(Position::Absolute);
if (inset) {
set_inset(border_width);
} else {
set_inset(-(border_width + border_outset_distance));
}
}
FocusBorder::FocusBorder(Element *parent, bool extra_dist) : PseudoBorder(parent, false, theme::border::width * 2.0f, theme::border::width * 2.0f * (extra_dist ? 2.0f : 1.0f)) {
set_border_color(theme::color::PrimaryL, 0);
}
void FocusBorder::process_event(const Event &e) {
switch (e.type) {
case EventType::Update: {
if (get_parent()->is_style_enabled(focus_state)) {
set_border_color(recompui::get_pulse_color(750));
} else {
set_border_color(theme::color::PrimaryL, 0);
}
queue_update();
break;
}
default:
break;
}
}
} // namespace recompui
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "ui_element.h"
namespace recompui {
class PseudoBorder : public Element {
protected:
std::string_view get_type_name() override { return "PseudoBorder"; }
public:
PseudoBorder(Element *parent, bool inset = true, float border_width = 0, float border_outset_distance = 0);
};
class FocusBorder : public PseudoBorder {
protected:
std::string_view get_type_name() override { return "FocusBorder"; }
virtual void process_event(const Event &e) override;
public:
FocusBorder(Element *parent, bool extra_dist = false);
};
} // namespace recompui
+49 -26
View File
@@ -12,25 +12,27 @@ namespace recompui {
enable_focus();
set_text(name);
set_cursor(Cursor::Pointer);
set_font_size(20.0f);
set_letter_spacing(2.8f);
set_line_height(20.0f);
set_font_weight(400);
set_font_style(FontStyle::Normal);
set_typography(theme::Typography::LabelSM);
set_border_color(theme::color::Text, 0);
set_border_bottom_width(1.0f);
set_border_bottom_width(theme::border::width);
set_color(theme::color::TextInactive);
set_padding_bottom(8.0f);
set_padding_top(8.0f);
set_padding_bottom(8.0f - theme::border::width);
set_text_transform(TextTransform::Uppercase);
set_height_auto();
set_opacity(1.0f);
hover_style.set_color(theme::color::WhiteA80);
checked_style.set_color(theme::color::White);
checked_style.set_border_color(theme::color::Text);
pulsing_style.set_border_color(theme::color::SecondaryA80);
disabled_style.set_color(theme::color::TextInactive);
disabled_style.set_opacity(0.5f);
disabled_style.set_cursor(Cursor::None);
add_style(&hover_style, { hover_state });
add_style(&checked_style, { checked_state });
add_style(&pulsing_style, { focus_state });
add_style(&disabled_style, { disabled_state });
}
void RadioOption::set_pressed_callback(std::function<void(uint32_t)> callback) {
@@ -43,27 +45,30 @@ namespace recompui {
void RadioOption::set_selected_state(bool enable) {
set_style_enabled(checked_state, enable);
set_as_primary_focus(enable);
}
void RadioOption::process_event(const Event &e) {
switch (e.type) {
case EventType::MouseButton:
{
const EventMouseButton &mousebutton = std::get<EventMouseButton>(e.variant);
if (mousebutton.button == MouseButton::Left && mousebutton.pressed) {
pressed_callback(index);
}
}
break;
case EventType::Click:
pressed_callback(index);
break;
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);
case EventType::Enable: {
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
}
else {
set_cursor(Cursor::None);
set_focusable(false);
}
break;
}
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
@@ -92,9 +97,13 @@ namespace recompui {
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);
if (this->index < static_cast<uint32_t>(options.size())) {
options[this->index]->set_selected_state(false);
}
if (index < static_cast<uint32_t>(options.size())) {
options[index]->set_selected_state(true);
}
this->index = index;
options[index]->set_selected_state(true);
if (trigger_callbacks) {
for (const auto &function : index_changed_callbacks) {
@@ -117,20 +126,21 @@ namespace recompui {
}, val);
}
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus, EventType::Update)) {
Radio::Radio(Element *parent) : Container(parent, FlexDirection::Row, JustifyContent::FlexStart, Events(EventType::Focus, EventType::Update, EventType::Enable)) {
set_gap(24.0f);
set_align_items(AlignItems::FlexStart);
enable_focus();
// enable_focus();
set_as_navigation_container(NavigationType::Horizontal);
}
void Radio::process_event(const Event &e) {
switch (e.type) {
case EventType::Focus:
if (!options.empty()) {
if (std::get<EventFocus>(e.variant).active) {
blur();
queue_child_focus();
}
// if (std::get<EventFocus>(e.variant).active) {
// blur();
// queue_child_focus();
// }
if (focus_callback != nullptr) {
focus_callback(std::get<EventFocus>(e.variant).active);
}
@@ -139,8 +149,21 @@ namespace recompui {
case EventType::Update:
if (child_focus_queued) {
child_focus_queued = false;
options[index]->focus();
if (index < static_cast<uint32_t>(options.size())) {
options[index]->focus();
} else if (!options.empty()) {
options.front()->focus();
}
}
break;
case EventType::Enable:
{
bool active = std::get<EventEnable>(e.variant).active;
for (auto &option : options) {
option->set_enabled(active);
}
}
break;
}
}
+1
View File
@@ -9,6 +9,7 @@ namespace recompui {
Style hover_style;
Style checked_style;
Style pulsing_style;
Style disabled_style;
std::function<void(uint32_t)> pressed_callback = nullptr;
std::function<void(bool)> focus_callback = nullptr;
uint32_t index = 0;
+11 -2
View File
@@ -53,7 +53,7 @@ namespace recompui {
char text_buffer[32];
if (type == SliderType::Double) {
std::snprintf(text_buffer, sizeof(text_buffer), "%.1f", value);
std::snprintf(text_buffer, sizeof(text_buffer), "%.*f", precision, value);
} else if (type == SliderType::Percent) {
std::snprintf(text_buffer, sizeof(text_buffer), "%d%%", static_cast<int>(value));
} else {
@@ -114,7 +114,8 @@ namespace recompui {
case EventType::Enable:
{
bool enable_active = std::get<EventEnable>(e.variant).active;
circle_element->set_enabled(enable_active);
// circle_element->set_enabled(enable_active);
// slider_element->set_enabled(enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
@@ -217,6 +218,14 @@ namespace recompui {
double Slider::get_step_value() const {
return step_value;
}
void Slider::set_precision(int p) {
precision = p;
}
int Slider::get_precision() const {
return precision;
}
void Slider::add_value_changed_callback(std::function<void(double)> callback) {
value_changed_callbacks.emplace_back(callback);
+3
View File
@@ -22,6 +22,7 @@ namespace recompui {
double min_value = 0.0;
double max_value = 100.0;
double step_value = 0.0;
int precision = 0;
std::vector<std::function<void(double)>> value_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
@@ -50,6 +51,8 @@ namespace recompui {
double get_max_value() const;
void set_step_value(double v);
double get_step_value() const;
void set_precision(int p);
int get_precision() const;
void add_value_changed_callback(std::function<void(double)> callback);
void do_step(bool increment);
void set_focus_callback(std::function<void(bool)> callback);
+16
View File
@@ -239,6 +239,13 @@ namespace recompui {
set_property(Rml::PropertyId::Bottom, Rml::Property(bottom, to_rml(unit)));
}
void Style::set_inset(float inset, Unit unit) {
set_top(inset, unit);
set_left(inset, unit);
set_right(inset, unit);
set_bottom(inset, unit);
}
void Style::set_width(float width, Unit unit) {
set_property(Rml::PropertyId::Width, Rml::Property(width, to_rml(unit)));
}
@@ -610,6 +617,15 @@ namespace recompui {
set_property(Rml::PropertyId::FontWeight, Rml::Style::FontWeight(weight));
}
void Style::set_typography(recompui::theme::Typography typography) {
const auto &preset = recompui::theme::get_typography_preset(typography);
set_font_size(preset.font_size);
set_letter_spacing(preset.letter_spacing);
set_line_height(preset.line_height);
set_font_weight(preset.font_weight);
set_font_style(preset.font_style);
}
void Style::set_text_align(TextAlign text_align) {
set_property(Rml::PropertyId::TextAlign, to_rml(text_align));
}
+2
View File
@@ -31,6 +31,7 @@ namespace recompui {
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_inset(float inset, 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);
@@ -101,6 +102,7 @@ namespace recompui {
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_typography(recompui::theme::Typography typography);
void set_text_align(TextAlign text_align);
void set_text_transform(TextTransform text_transform);
void set_gap(float size, Unit unit = Unit::Dp);
+168
View File
@@ -0,0 +1,168 @@
#pragma once
#include "ui_tab_set.h"
#include "../ui_utils.h"
namespace recompui {
Tab::Tab(Element *parent, int tab_index, std::string_view text, on_change_tab_callback on_select_tab) :
Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "button", false)
{
this->tab_index = tab_index;
this->on_select_tab = std::move(on_select_tab);
set_display(Display::Block);
set_position(Position::Relative);
set_margin(0);
set_padding_top(tab_vertical_padding);
set_padding_bottom(tab_vertical_padding);
set_padding_left(tab_horizontal_padding);
set_padding_right(tab_horizontal_padding);
set_focusable(true);
set_tab_index_auto();
set_nav_none(NavDirection::Left);
set_nav_none(NavDirection::Right);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_color(theme::color::TextInactive);
set_background_color(theme::color::Transparent);
hover_style.set_color(theme::color::WhiteA80);
checked_style.set_color(theme::color::White);
pulsing_style.set_color(theme::color::WhiteA80);
ContextId context = get_current_context();
label = context.create_element<Label>(this, std::string{text}, theme::Typography::Header3);
indicator = context.create_element<Element>(this, 0, "div", false);
indicator->set_display(Display::Block);
indicator->set_position(Position::Absolute);
indicator->set_height(2.0f);
indicator->set_bottom(2.0f);
indicator->set_left(0.0f);
indicator->set_right(0.0f);
indicator->set_background_color(theme::color::Transparent);
add_style(&hover_style, { hover_state });
add_style(&checked_style, { checked_state });
add_style(&pulsing_style, { focus_state });
}
void Tab::process_event(const Event &e) {
switch (e.type) {
case EventType::Click:
on_select_tab(tab_index);
break;
case EventType::Hover:
set_style_enabled(hover_state, std::get<EventHover>(e.variant).active);
break;
case EventType::Enable: {
bool active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !active);
indicator->set_background_color(active ? theme::color::BorderSolid : theme::color::Transparent);
break;
}
case EventType::Focus:
{
bool active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, active);
if (active) {
queue_update();
}
}
break;
case EventType::Update:
if (is_style_enabled(focus_state)) {
recompui::Color pulse_color = recompui::get_pulse_color(750);
pulsing_style.set_color(pulse_color);
if (is_selected) {
indicator->set_background_color(pulse_color);
} else {
indicator->set_background_color(theme::color::Transparent);
}
apply_styles();
queue_update();
}
break;
default:
break;
}
}
void Tab::set_selected(bool enable) {
is_selected = enable;
set_as_primary_focus(enable);
set_style_enabled(checked_state, enable);
indicator->set_background_color(enable ? theme::color::BorderSolid : theme::color::Transparent);
}
TabSet::TabSet(Element *parent) :
Element(parent, 0, "div", false)
{
set_display(Display::Flex);
set_flex_direction(FlexDirection::Row);
set_justify_content(JustifyContent::FlexStart);
set_align_items(AlignItems::Stretch);
set_gap(0.0f);
set_as_navigation_container(NavigationType::Horizontal);
}
void TabSet::set_change_tab_callback(on_change_tab_callback callback) {
change_tab_callback = std::move(callback);
}
int TabSet::add_tab(std::string_view text) {
ContextId context = get_current_context();
Tab *tab = context.create_element<Tab>(this, tabs.size(), text, [this](int tab_index) {
set_active_tab(tab_index);
});
tabs.push_back(tab);
if (active_tab < 0) {
active_tab = 0;
tab->set_selected(true);
} else {
tab->set_selected(active_tab == tabs.size() - 1);
}
if (tabs.size() > 1) {
tabs[tabs.size() - 2]->set_nav(NavDirection::Right, tab);
tab->set_nav(NavDirection::Left, tabs[tabs.size() - 2]);
tab->set_nav_none(NavDirection::Right);
}
return tabs.size() - 1;
}
void TabSet::set_active_tab(int tab_index) {
if (tab_index < 0 || tab_index >= static_cast<int>(tabs.size())) {
return;
}
if (active_tab != tab_index) {
active_tab = tab_index;
for (int i = 0; i < static_cast<int>(tabs.size()); i++) {
tabs[i]->set_selected(i == active_tab);
}
if (change_tab_callback) {
change_tab_callback(active_tab);
}
}
}
void TabSet::focus_on_active_tab() {
if (active_tab >= 0 && active_tab < static_cast<int>(tabs.size())) {
tabs[active_tab]->focus();
}
};
void TabSet::set_nav_down(Element *element) {
for (auto &tab : tabs) {
tab->set_nav(NavDirection::Down, element);
}
}
void TabSet::set_nav_up(Element *element) {
for (auto &tab : tabs) {
tab->set_nav(NavDirection::Up, element);
}
}
} // namespace recompui
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include "ui_element.h"
#include "ui_label.h"
namespace recompui {
using on_change_tab_callback = std::function<void(int tab_index)>;
class Tab : public Element {
private:
Style hover_style;
Style checked_style;
Style pulsing_style;
int tab_index;
bool is_selected = false;
on_change_tab_callback on_select_tab;
Element *label = nullptr;
Element *indicator = nullptr;
static constexpr float tab_vertical_padding = 20.0f;
static constexpr float tab_horizontal_padding = 24.0f;
protected:
std::string_view get_type_name() override { return "Tab"; }
virtual void process_event(const Event &e) override;
public:
Tab(Element *parent, int tab_index, std::string_view text, on_change_tab_callback on_select_tab);
void set_selected(bool selected);
};
class TabSet : public Element {
private:
std::vector<Tab *> tabs;
on_change_tab_callback change_tab_callback;
int active_tab = -1;
protected:
std::string_view get_type_name() override { return "TabSet"; }
public:
TabSet(Element *parent);
void set_change_tab_callback(on_change_tab_callback callback);
int add_tab(std::string_view text);
void set_active_tab(int tab_index);
void focus_on_active_tab();
Element *get_active_tab_element() const { return active_tab >= 0 && active_tab < static_cast<int>(tabs.size()) ? tabs[active_tab] : nullptr; };
void set_nav_up(Element *element);
void set_nav_down(Element *element);
int get_active_tab() const { return active_tab; }
};
} // namespace recompui
+7 -3
View File
@@ -21,6 +21,7 @@ namespace recompui {
if (focus_callback != nullptr) {
focus_callback(event.active);
}
set_style_enabled(focus_state, event.active);
break;
}
default:
@@ -33,13 +34,16 @@ namespace recompui {
set_attribute("type", "password");
}
set_min_width(60.0f);
set_border_color(theme::color::Text);
set_border_bottom_width(1.0f);
set_padding_bottom(6.0f);
set_border_color(theme::color::TextA50);
set_border_width(theme::border::width);
set_padding(8.0f - theme::border::width);
set_border_radius(theme::border::radius_sm);
set_focusable(true);
set_nav_auto(NavDirection::Up);
set_nav_auto(NavDirection::Down);
set_tab_index_auto();
focus_style.set_border_color(theme::color::Primary);
add_style(&focus_style, focus_state);
}
void TextInput::set_text(std::string_view text) {
+1
View File
@@ -9,6 +9,7 @@ namespace recompui {
std::string text;
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
std::function<void(bool)> focus_callback = nullptr;
Style focus_style;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "TextInput"; }
+38
View File
@@ -227,3 +227,41 @@ const recompui::Color &recompui::theme::get_theme_color(recompui::theme::color c
const char *recompui::theme::get_theme_color_name(recompui::theme::color color) {
return theme_color_names[(std::size_t)color];
}
using TypographyPreset = recompui::theme::TypographyPreset;
constexpr TypographyPreset create_typography_preset(float font_size, float letter_spacing_percentage, uint32_t font_weight = 400, recompui::FontStyle font_style = recompui::FontStyle::Normal) {
TypographyPreset preset;
preset.font_size = font_size;
preset.line_height = font_size;
preset.letter_spacing = font_size * letter_spacing_percentage;
preset.font_weight = font_weight;
preset.font_style = font_style;
return preset;
}
using TypographyArray = std::array<TypographyPreset, (std::size_t)(recompui::theme::Typography::size)>;
constexpr TypographyArray get_default_typography_array() {
TypographyArray typography = {};
typography[(std::size_t)recompui::theme::Typography::Header1] = create_typography_preset(68.0f, 0.07f, 700);
typography[(std::size_t)recompui::theme::Typography::Header2] = create_typography_preset(52.0f, 0.07f, 700);
typography[(std::size_t)recompui::theme::Typography::Header3] = create_typography_preset(36.0f, 0.07f, 700);
typography[(std::size_t)recompui::theme::Typography::LabelLG] = create_typography_preset(36.0f, 0.11f, 700);
typography[(std::size_t)recompui::theme::Typography::LabelMD] = create_typography_preset(28.0f, 0.11f, 700);
typography[(std::size_t)recompui::theme::Typography::LabelSM] = create_typography_preset(20.0f, 0.14f, 700);
typography[(std::size_t)recompui::theme::Typography::LabelXS] = create_typography_preset(18.0f, 0.14f);
typography[(std::size_t)recompui::theme::Typography::Body] = create_typography_preset(20.0f, 0);
return typography;
}
static TypographyArray typography_presets = get_default_typography_array();
void recompui::theme::set_typography_preset(Typography type, float font_size, float letter_spacing_percentage, uint32_t font_weight, recompui::FontStyle font_style) {
typography_presets[(std::size_t)type] = create_typography_preset(font_size, letter_spacing_percentage, font_weight, font_style);
}
TypographyPreset &recompui::theme::get_typography_preset(recompui::theme::Typography type) {
return typography_presets[(std::size_t)type];
}
+27 -3
View File
@@ -98,6 +98,10 @@ namespace recompui {
size,
};
const char *get_theme_color_name(theme::color color);
void set_theme_color(theme::color color, const recompui::Color &value);
const recompui::Color &get_theme_color(theme::color color);
namespace border {
extern float radius_sm;
extern float radius_md;
@@ -105,9 +109,29 @@ namespace recompui {
extern float width;
}
const char *get_theme_color_name(theme::color color);
void set_theme_color(theme::color color, const recompui::Color &value);
const recompui::Color &get_theme_color(theme::color color);
enum class Typography {
Header1,
Header2,
Header3,
LabelLG,
LabelMD,
LabelSM,
LabelXS,
Body,
size,
};
struct TypographyPreset {
float font_size;
float line_height;
float letter_spacing;
uint32_t font_weight = 400;
recompui::FontStyle font_style = recompui::FontStyle::Normal;
};
void set_typography_preset(Typography type, float font_size, float letter_spacing_percentage = 0.0f, uint32_t font_weight = 400, recompui::FontStyle font_style = recompui::FontStyle::Normal);
TypographyPreset &get_typography_preset(Typography type);
} // namespace theme
} // namespace recompui
+95 -23
View File
@@ -1,38 +1,86 @@
#include "ui_toggle.h"
#include "ui_pseudo_border.h"
#include "../ui_utils.h"
#include <cassert>
#include <ultramodern/ultramodern.hpp>
namespace recompui {
static constexpr float toggle_height = 72.0f;
static constexpr float toggle_width = 162.0f;
static constexpr float toggle_floater_height = 64.0f;
static constexpr float toggle_floater_width = 80.0f;
struct ToggleSizing {
float width;
float height;
float floater_width;
float floater_height;
float floater_margin;
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
ToggleSizing(float width, float height, float floater_margin)
: width(width), height(height),
floater_width(height - floater_margin * 2.0f),
floater_height(height - floater_margin * 2.0f),
floater_margin(floater_margin) {}
float left_offset(bool checked) const {
return checked ? width - floater_width - floater_margin : floater_margin;
}
};
static const ToggleSizing medium_toggle_sizing = ToggleSizing(64.0f, 36.0f, 6.0f);
static const ToggleSizing large_toggle_sizing = ToggleSizing(144.0f, 72.0f, 8.0f);
static const ToggleSizing &get_toggle_sizing(ToggleSize size) {
switch (size) {
case ToggleSize::Medium:
return medium_toggle_sizing;
case ToggleSize::Large:
return large_toggle_sizing;
default:
assert(false && "Invalid toggle size");
}
}
Toggle::Toggle(Element *parent, ToggleSize size) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
this->size = size;
const ToggleSizing &sizing = get_toggle_sizing(size);
enable_focus();
set_width(toggle_width);
set_height(toggle_height);
set_border_radius(toggle_height * 0.5f);
set_position(Position::Relative);
set_width(sizing.width);
set_height(sizing.height);
set_border_radius(sizing.height * 0.5f);
set_opacity(0.9f);
set_cursor(Cursor::Pointer);
set_border_width(theme::border::width);
// set_border_width(theme::border::width);
set_border_color(theme::color::BW50);
set_background_color(theme::color::Transparent);
checked_style.set_border_color(theme::color::Primary);
inner_border_styles.checked.set_border_color(theme::color::Primary);
hover_style.set_border_color(theme::color::BW50);
inner_border_styles.hover.set_border_color(theme::color::BW50);
hover_style.set_background_color(theme::color::WhiteA5);
focus_style.set_border_color(theme::color::BW50);
inner_border_styles.focus.set_border_color(theme::color::BW50);
focus_style.set_background_color(theme::color::WhiteA5);
checked_hover_style.set_border_color(theme::color::Primary);
inner_border_styles.checked_hover.set_border_color(theme::color::Primary);
checked_hover_style.set_background_color(theme::color::PrimaryA30);
checked_focus_style.set_border_color(theme::color::Primary);
inner_border_styles.checked_focus.set_border_color(theme::color::Primary);
checked_focus_style.set_background_color(theme::color::PrimaryA30);
disabled_style.set_border_color(theme::color::BW50, 128);
inner_border_styles.disabled.set_border_color(theme::color::BW50, 128);
checked_disabled_style.set_border_color(theme::color::PrimaryD, 128);
inner_border_styles.checked_disabled.set_border_color(theme::color::PrimaryD, 128);
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
@@ -43,12 +91,27 @@ namespace recompui {
ContextId context = get_current_context();
focus_border = context.create_element<FocusBorder>(this);
focus_border->set_border_radius((sizing.height + theme::border::width * 4.0f + theme::border::width * 4.0f) * 0.5f);
inner_border = context.create_element<PseudoBorder>(this);
inner_border->set_border_radius((sizing.height - theme::border::width * 2.0f) * 0.5f);
inner_border->set_border_color(theme::color::BW50);
inner_border->add_style(&inner_border_styles.checked, checked_state);
inner_border->add_style(&inner_border_styles.hover, hover_state);
inner_border->add_style(&inner_border_styles.focus, focus_state);
inner_border->add_style(&inner_border_styles.checked_hover, { checked_state, hover_state });
inner_border->add_style(&inner_border_styles.checked_focus, { checked_state, focus_state });
inner_border->add_style(&inner_border_styles.disabled, disabled_state);
inner_border->add_style(&inner_border_styles.checked_disabled, { checked_state, disabled_state });
floater = context.create_element<Element>(this);
floater->set_position(Position::Relative);
floater->set_top(2.0f);
floater->set_width(toggle_floater_width);
floater->set_height(toggle_floater_height);
floater->set_border_radius(toggle_height * 0.5f);
floater->set_position(Position::Absolute);
floater->set_top(50.0f, Unit::Percent);
floater->set_translate_2D(0, -50.0f, Unit::Percent);
floater->set_width(sizing.floater_width);
floater->set_height(sizing.floater_height);
floater->set_border_radius(sizing.floater_height * 0.5f);
floater->set_background_color(theme::color::TextDim);
floater_checked_style.set_background_color(theme::color::Primary);
floater_disabled_style.set_background_color(theme::color::TextDim, 128);
@@ -60,6 +123,12 @@ namespace recompui {
set_checked_internal(false, false, true, false);
}
void Toggle::set_all_style_enabled(std::string_view style_name, bool enabled) {
set_style_enabled(style_name, enabled);
inner_border->set_style_enabled(style_name, enabled);
floater->set_style_enabled(style_name, enabled);
}
void Toggle::set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks) {
if (this->checked != checked || setup) {
this->checked = checked;
@@ -80,13 +149,12 @@ namespace recompui {
}
}
set_style_enabled(checked_state, checked);
floater->set_style_enabled(checked_state, checked);
set_all_style_enabled(checked_state, checked);
}
}
float Toggle::floater_left_target() const {
return checked ? 78.0f : 4.0f;
return get_toggle_sizing(size).left_offset(checked);
}
void Toggle::process_event(const Event &e) {
@@ -99,19 +167,18 @@ namespace recompui {
break;
case EventType::Hover: {
bool hover_active = std::get<EventHover>(e.variant).active && is_enabled();
set_style_enabled(hover_state, hover_active);
floater->set_style_enabled(hover_state, hover_active);
set_all_style_enabled(hover_state, hover_active);
break;
}
case EventType::Focus: {
bool focus_active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, focus_active);
set_all_style_enabled(focus_state, focus_active);
queue_update();
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);
set_all_style_enabled(disabled_state, !enable_active);
if (enable_active) {
set_cursor(Cursor::Pointer);
set_focusable(true);
@@ -126,6 +193,7 @@ namespace recompui {
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;
bool should_queue_update = false;
constexpr float dp_speed = 740.0f;
const float target = floater_left_target();
@@ -140,11 +208,15 @@ namespace recompui {
floater_left = target;
}
else {
queue_update();
should_queue_update = true;
}
floater->set_left(floater_left, Unit::Dp);
if (should_queue_update) {
queue_update();
}
break;
}
default:
+23 -2
View File
@@ -4,9 +4,17 @@
namespace recompui {
enum class ToggleSize {
Medium,
Large,
Default = Large
};
class Toggle : public Element {
protected:
Element *floater;
Element *inner_border;
Element *focus_border;
float floater_left = 0.0f;
std::chrono::high_resolution_clock::duration last_time;
std::list<std::function<void(bool)>> checked_callbacks;
@@ -17,22 +25,35 @@ namespace recompui {
Style checked_focus_style;
Style disabled_style;
Style checked_disabled_style;
struct {
Style checked;
Style hover;
Style focus;
Style checked_hover;
Style checked_focus;
Style disabled;
Style checked_disabled;
} inner_border_styles;
Style floater_checked_style;
Style floater_disabled_style;
Style floater_disabled_checked_style;
bool checked = false;
ToggleSize size = ToggleSize::Default;
void set_checked_internal(bool checked, bool animate, bool setup, bool trigger_callbacks);
void set_all_style_enabled(std::string_view style_name, bool enabled);
float floater_left_target() const;
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Toggle"; }
public:
Toggle(Element *parent);
Toggle(Element *parent, ToggleSize size = ToggleSize::Default);
void set_checked(bool checked);
bool is_checked() const;
void add_checked_callback(std::function<void(bool)> callback);
};
} // namespace recompui
} // namespace recompui
+23 -1
View File
@@ -36,6 +36,7 @@ namespace recompui {
Update,
Navigate,
MouseButton,
MenuAction,
Count
};
@@ -65,6 +66,16 @@ namespace recompui {
Auto
};
enum class MenuAction {
None,
Accept,
Apply,
Back,
Toggle,
TabLeft,
TabRight,
};
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);
@@ -113,7 +124,11 @@ namespace recompui {
bool pressed;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, EventMouseButton, std::monostate>;
struct EventMenuAction {
MenuAction action;
};
using EventVariant = std::variant<EventClick, EventFocus, EventHover, EventEnable, EventDrag, EventText, EventNavigate, EventMouseButton, EventMenuAction, std::monostate>;
struct Event {
EventType type;
@@ -182,6 +197,13 @@ namespace recompui {
e.variant = EventMouseButton{ x, y, button, pressed };
return e;
}
static Event menu_action_event(MenuAction action) {
Event e;
e.type = EventType::MenuAction;
e.variant = EventMenuAction{ action };
return e;
}
};
enum class Display {
+6
View File
@@ -87,6 +87,12 @@ bool convert_event(const recompui::Event& in, RecompuiEventData& out) {
case recompui::EventType::Update:
// No data for an update event.
break;
case recompui::EventType::MenuAction:
{
const recompui::EventMenuAction &menu_action = std::get<recompui::EventMenuAction>(in.variant);
out.data.menu_action.action = static_cast<RecompuiMenuAction>(menu_action.action);
}
break;
}
return !skip;
+32
View File
@@ -0,0 +1,32 @@
#include "ui_color_element.h"
namespace recompui {
ElementColor::ElementColor(const Rml::String& tag) : Rml::Element(tag) {
SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Inline);
}
void ElementColor::check_color_attribute(const std::string &attr, const std::string &check, theme::color color) {
if (attr == check) {
recompui::Element this_compat(this);
this_compat.set_color(color);
}
}
void ElementColor::OnAttributeChange(const Rml::ElementAttributes& changed_attributes) {
Rml::Element::OnAttributeChange(changed_attributes);
for (auto &attr : changed_attributes) {
std::string name = attr.first;
check_color_attribute(name, "primary", theme::color::Primary);
check_color_attribute(name, "secondary", theme::color::Secondary);
check_color_attribute(name, "warning", theme::color::Warning);
check_color_attribute(name, "danger", theme::color::Danger);
check_color_attribute(name, "success", theme::color::Success);
}
}
ElementColor::~ElementColor() {
}
} // namespace recompui
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "recomp_input.h"
#include "elements/ui_element.h"
#include "elements/ui_theme.h"
namespace recompui {
class ElementColor : public Rml::Element {
private:
recompui::Element *element;
public:
ElementColor(const Rml::String& tag);
void OnAttributeChange(const Rml::ElementAttributes& changed_attributes) override;
virtual ~ElementColor();
void check_color_attribute(const std::string &attr, const std::string &check, theme::color color);
};
} // namespace recompui
+9 -9
View File
@@ -18,19 +18,19 @@ Rml::DataModelHandle general_model_handle;
Rml::DataModelHandle graphics_model_handle;
Rml::DataModelHandle sound_options_model_handle;
int recompui::config_tab_to_index(recompui::ConfigTab tab) {
int recompui::config_tab_to_index(recompui::ConfigTabId tab) {
switch (tab) {
case recompui::ConfigTab::General:
case recompui::ConfigTabId::General:
return 0;
case recompui::ConfigTab::Controls:
case recompui::ConfigTabId::Controls:
return 1;
case recompui::ConfigTab::Graphics:
case recompui::ConfigTabId::Graphics:
return 2;
case recompui::ConfigTab::Sound:
case recompui::ConfigTabId::Sound:
return 3;
case recompui::ConfigTab::Mods:
case recompui::ConfigTabId::Mods:
return 4;
case recompui::ConfigTab::Debug:
case recompui::ConfigTabId::Debug:
return 5;
default:
assert(false && "Unknown config tab.");
@@ -380,7 +380,7 @@ class ConfigTabsetListener : public Rml::EventListener {
void ProcessEvent(Rml::Event& event) override {
if (event.GetId() == Rml::EventId::Tabchange) {
int tab_index = event.GetParameter<int>("tab_index", 0);
bool in_mod_tab = (tab_index == recompui::config_tab_to_index(recompui::ConfigTab::Mods));
bool in_mod_tab = (tab_index == recompui::config_tab_to_index(recompui::ConfigTabId::Mods));
if (in_mod_tab) {
recompui::set_config_tabset_mod_nav();
}
@@ -703,7 +703,7 @@ void recompui::toggle_fullscreen() {
graphics_model_handle.DirtyVariable("wm_option");
}
void recompui::set_config_tab(ConfigTab tab) {
void recompui::set_config_tab(ConfigTabId tab) {
get_config_tabset()->SetActiveTab(config_tab_to_index(tab));
}
+238
View File
@@ -0,0 +1,238 @@
#include "ui_config_modal.h"
#include "config/ui_config_tab_graphics.h"
#include "config/ui_config_tab_controls.h"
#include "banjo_config.h"
namespace recompui {
Modal *config_modal = nullptr;
static recomp::config::Config example_manifest("Example Config", "example_config", true);
static ConfigTab example_tab(&example_manifest);
static recomp::config::Config example_manifest2("No apply", "example_config_no_apply", false);
static ConfigTab example_tab2(&example_manifest2);
// ConfigTabGeneral config_tab_general = ConfigTabGeneral();
static std::vector<TabContext *> config_tabs = {
// &config_tab_general,
&config_tab_graphics,
&config_tab_controls,
// other hardcoded tabs
};
enum class TheThings {
This,
That,
TheOther
};
const std::vector<recomp::config::ConfigOptionEnumOption> example_enum_options = {
{TheThings::TheOther, "theotherr", "The Other ñ"},
{TheThings::That, "that", "That"},
{TheThings::This, "this", "This"},
};
enum class TheUnsureThings {
Yes,
Maybe,
Unsure,
No,
Nah,
size
};
const std::vector<recomp::config::ConfigOptionEnumOption> example_enum2_options = {
{TheUnsureThings::Yes, "Yes?"},
{TheUnsureThings::Maybe, "Maybe"},
{TheUnsureThings::Unsure, "Unsure"},
{TheUnsureThings::No, "No"},
{TheUnsureThings::Nah, "Nah"}
};
// typedef std::variant<std::monostate, uint32_t, double, std::string> ConfigValueVariant;
void print_option_value_change_s(const std::string &option_id, std::string value, std::string prev_value) {
printf("Option '%s' changed from '%s' to '%s'\n",
option_id.c_str(),
prev_value.c_str(),
value.c_str()
);
}
void print_option_value_change_i(const std::string &option_id, int value, int prev_value) {
printf("Option '%s' changed from '%d' to '%d'\n",
option_id.c_str(),
prev_value,
value
);
}
void print_option_value_change_f(const std::string &option_id, double value, double prev_value) {
printf("Option '%s' changed from '%f' to '%f'\n",
option_id.c_str(),
prev_value,
value
);
}
void init_example_config() {
using OptionChangeContext = recomp::config::OptionChangeContext;
example_manifest.add_number_option(
"example_option_number",
"Rumble strength",
"This is just a little example hehe",
0.0,
10.0,
1.0,
0,
false,
5
);
example_manifest.add_option_change_callback("example_option_number", [](recomp::config::ConfigValueVariant value, recomp::config::ConfigValueVariant prev_value, OptionChangeContext change_context) {
print_option_value_change_f("example_option_number", std::get<double>(value), std::get<double>(prev_value));
});
example_manifest.add_enum_option(
"example_option_enum",
"Example Option Enum",
"This is just another little example haha!",
example_enum_options,
TheThings::TheOther
);
example_manifest.add_bool_option(
"example_option_bool",
"Option Bool",
"Just a stupid fucking bool",
true
);
example_manifest.add_bool_option(
"example_option_bool2",
"Option Bool 2",
"Just another stupid fucking bool",
false
);
// example_manifest.add_option_change_callback("example_option_enum", [](recomp::config::ConfigValueVariant value, recomp::config::ConfigValueVariant prev_value) {
// print_option_value_change_i("example_option_enum", std::get<uint32_t>(value), std::get<uint32_t>(prev_value));
// TheThings new_value = static_cast<TheThings>(std::get<uint32_t>(value));
// if (new_value == TheThings::This) {
// example_manifest.set_option_value("example_option_enum2", static_cast<uint32_t>(TheUnsureThings::Yes));
// example_manifest.update_option_enum_details("example_option_enum2", "");
// example_manifest.update_option_disabled("example_option_enum2", false);
// example_manifest.update_option_hidden("example_option_string", false);
// }
// if (new_value == TheThings::That) {
// example_manifest.update_option_enum_details("example_option_enum2", "surprise bitch");
// example_manifest.update_option_disabled("example_option_enum2", false);
// example_manifest.update_option_hidden("example_option_string", true);
// }
// if (new_value == TheThings::TheOther) {
// example_manifest.update_option_enum_details("example_option_enum2", "im disabled uwu");
// example_manifest.update_option_disabled("example_option_enum2", true);
// example_manifest.update_option_description("example_option_enum", "we did this on the flyyyyy");
// example_manifest.update_option_hidden("example_option_string", false);
// }
// });
example_manifest.add_string_option(
"example_option_string",
"Example Option String",
"string example...! description........!!!!",
"defaultma"
);
example_manifest.add_option_change_callback("example_option_string", [](recomp::config::ConfigValueVariant value, recomp::config::ConfigValueVariant prev_value, OptionChangeContext change_context) {
print_option_value_change_s("example_option_string", std::get<std::string>(value), std::get<std::string>(prev_value));
});
example_manifest.add_enum_option(
"example_option_enum2",
"Choosy",
"choosyyyyyy",
example_enum2_options,
TheUnsureThings::Unsure
);
example_manifest.add_option_change_callback("example_option_enum2", [](recomp::config::ConfigValueVariant value, recomp::config::ConfigValueVariant prev_value, OptionChangeContext change_context) {
print_option_value_change_i("example_option_enum2", std::get<uint32_t>(value), std::get<uint32_t>(prev_value));
});
example_manifest.set_apply_callback([]() {
printf("Example config applied!\n");
});
for (int i = 2; i < 10; ++i) {
example_manifest.add_string_option(
"example_option_string" + std::to_string(i),
"Example Option String " + std::to_string(i),
"string example...! description........!!!!",
"defaultma"
);
}
example_manifest.add_option_disable_dependency(
"example_option_enum",
"example_option_enum2",
TheUnsureThings::Unsure
);
example_manifest.add_option_hidden_dependency(
"example_option_string2",
"example_option_enum2",
TheUnsureThings::Unsure
);
example_manifest.add_option_hidden_dependency(
"example_option_string3",
"example_option_enum2",
TheUnsureThings::Unsure
);
example_manifest.add_option_hidden_dependency(
"example_option_string4",
"example_option_enum2",
TheUnsureThings::Unsure
);
example_manifest.load_config();
example_manifest2.add_number_option(
"example_option_number_no_apply",
"Example Option Number No Apply",
"words words words words PUNCHLINE",
0.0,
10.0,
1.0,
0,
false,
5
);
example_manifest2.add_option_change_callback("example_option_number_no_apply", [](recomp::config::ConfigValueVariant value, recomp::config::ConfigValueVariant prev_value, OptionChangeContext change_context) {
print_option_value_change_f("example_option_number_no_apply", std::get<double>(value), std::get<double>(prev_value));
});
example_manifest2.load_config();
add_config_modal_tab(&example_tab);
add_config_modal_tab(&example_tab2);
}
void add_config_modal_tab(TabContext *tab, bool add_to_front) {
if (add_to_front) {
config_tabs.insert(config_tabs.begin(), tab);
} else {
config_tabs.push_back(tab);
}
}
void init_config_modal() {
init_example_config();
config_modal = Modal::create_modal(ModalType::Fullscreen);
config_modal->modal_root_context.open();
for (auto tab : config_tabs) {
config_modal->add_tab(tab);
}
config_modal->set_selected_tab(0);
config_modal->set_on_close_callback([]() {
banjo::save_config();
});
config_modal->modal_root_context.close();
}
} // namespace recompui
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "elements/ui_element.h"
#include "elements/ui_modal.h"
#include "config/ui_config_tab_manifest.h"
namespace recompui {
extern Modal *config_modal;
void add_config_modal_tab(TabContext *tab, bool add_to_front = false);
void init_config_modal();
} // namespace recompui
+5 -7
View File
@@ -7,8 +7,6 @@ namespace recompinput {
namespace recompui {
ConfigPageControls *controls_page = nullptr;
static bool is_multiplayer_enabled() {
return true;
}
@@ -28,11 +26,11 @@ ElementConfigPageControls::ElementConfigPageControls(const Rml::String& tag) : R
recompui::Element this_compat(this);
recompui::ContextId context = get_current_context();
controls_page = context.create_element<ConfigPageControls>(
&this_compat,
recompinput::get_num_players(),
temp_game_input_contexts
);
// controls_page = context.create_element<ConfigPageControls>(
// &this_compat,
// recompinput::get_num_players(),
// temp_game_input_contexts
// );
}
ElementConfigPageControls::~ElementConfigPageControls() {
+1 -3
View File
@@ -2,12 +2,10 @@
#include "recomp_input.h"
#include "elements/ui_config_page.h"
#include "ui_config_page_controls.h"
#include "config/ui_config_page_controls.h"
namespace recompui {
extern ConfigPageControls *controls_page;
class ElementConfigPageControls : public Rml::Element {
public:
ElementConfigPageControls(const Rml::String& tag);
+1
View File
@@ -12,6 +12,7 @@ static RecompCustomElement custom_elements[] = {
CUSTOM_ELEMENT("recomp-config-sub-menu", recompui::ElementConfigSubMenu),
CUSTOM_ELEMENT("recomp-config-page-example", recompui::ElementConfigPageExample),
CUSTOM_ELEMENT("recomp-config-page-controls", recompui::ElementConfigPageControls),
CUSTOM_ELEMENT("recomp-color", recompui::ElementColor),
};
void recompui::register_custom_elements() {
+1
View File
@@ -8,6 +8,7 @@
#include "ui_config_sub_menu.h"
#include "ui_config_page_example.h"
#include "ui_config_page_controls_element.h"
#include "ui_color_element.h"
namespace recompui {
void register_custom_elements();
+3 -3
View File
@@ -85,21 +85,21 @@ public:
);
recompui::register_event(listener, "open_controls",
[](const std::string& param, Rml::Event& event) {
recompui::set_config_tab(recompui::ConfigTab::Controls);
recompui::set_config_tab(recompui::ConfigTabId::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::set_config_tab(recompui::ConfigTabId::General);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
);
recompui::register_event(listener, "open_mods",
[](const std::string &param, Rml::Event &event) {
recompui::set_config_tab(recompui::ConfigTab::Mods);
recompui::set_config_tab(recompui::ConfigTabId::Mods);
recompui::hide_all_contexts();
recompui::show_context(recompui::get_config_context_id(), "");
}
+1 -1
View File
@@ -340,4 +340,4 @@ namespace recompui {
}
}
}
};
};
+22 -14
View File
@@ -305,7 +305,7 @@ void ModMenu::mod_selected(uint32_t 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);
const recomp::config::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());
@@ -444,11 +444,15 @@ ContextId get_config_sub_menu_context_id() {
return sub_menu_context;
}
bool ModMenu::handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value) {
bool ModMenu::handle_special_config_options(const recomp::config::ConfigOption& option, const recomp::config::ConfigValueVariant& config_value) {
if (banjo::renderer::is_texture_pack_enable_config_option(option, true)) {
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
const recomp::config::ConfigOptionEnum &option_enum = std::get<recomp::config::ConfigOptionEnum>(option.variant);
std::vector<std::string> option_names;
for (const auto &opt : option_enum.options) {
option_names.push_back(opt.name);
}
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_enum.options,
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_names,
[this](const std::string &id, uint32_t value) {
mod_enum_option_changed(id, value);
mod_hd_textures_enabled_changed(value);
@@ -470,9 +474,9 @@ void ModMenu::mod_configure_requested() {
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);
const recomp::config::ConfigSchema &config_schema = recomp::mods::get_mod_config_schema(mod_details[active_mod_index].mod_id);
for (const recomp::config::ConfigOption &option : config_schema.options) {
recomp::config::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;
}
@@ -482,19 +486,23 @@ void ModMenu::mod_configure_requested() {
}
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,
case recomp::config::ConfigOptionType::Enum: {
const recomp::config::ConfigOptionEnum &option_enum = std::get<recomp::config::ConfigOptionEnum>(option.variant);
std::vector<std::string> option_names;
for (const auto &opt : option_enum.options) {
option_names.push_back(opt.name);
}
config_sub_menu->add_radio_option(option.id, option.name, option.description, std::get<uint32_t>(config_value), option_names,
[this](const std::string &id, uint32_t value){ mod_enum_option_changed(id, value); });
break;
}
case recomp::mods::ConfigOptionType::Number: {
const recomp::mods::ConfigOptionNumber &option_number = std::get<recomp::mods::ConfigOptionNumber>(option.variant);
case recomp::config::ConfigOptionType::Number: {
const recomp::config::ConfigOptionNumber &option_number = std::get<recomp::config::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,
[this](const std::string &id, double value){ mod_number_option_changed(id, value); });
break;
}
case recomp::mods::ConfigOptionType::String: {
case recomp::config::ConfigOptionType::String: {
config_sub_menu->add_text_option(option.id, option.name, option.description, std::get<std::string>(config_value),
[this](const std::string &id, const std::string &value){ mod_string_option_changed(id, value); });
break;
@@ -587,7 +595,7 @@ void ModMenu::create_mod_list() {
}
Rml::ElementTabSet* tabset = recompui::get_config_tabset();
if (tabset && tabset->GetActiveTab() == recompui::config_tab_to_index(ConfigTab::Mods)) {
if (tabset && tabset->GetActiveTab() == recompui::config_tab_to_index(ConfigTabId::Mods)) {
recompui::set_config_tabset_mod_nav();
}
+1 -1
View File
@@ -87,7 +87,7 @@ private:
void mod_selected(uint32_t mod_index);
void mod_dragged(uint32_t mod_index, EventDrag drag);
void mod_configure_requested();
bool handle_special_config_options(const recomp::mods::ConfigOption& option, const recomp::mods::ConfigValueVariant& config_value);
bool handle_special_config_options(const recomp::config::ConfigOption& option, const recomp::config::ConfigValueVariant& config_value);
void mod_enum_option_changed(const std::string &id, uint32_t value);
void mod_string_option_changed(const std::string &id, const std::string &value);
void mod_number_option_changed(const std::string &id, double value);
+67 -27
View File
@@ -27,6 +27,7 @@
#include "ui_mod_installer.h"
#include "ui_renderer.h"
#include "ui_assign_players_modal.h"
#include "ui_config_modal.h"
bool can_focus(Rml::Element* element) {
return element->GetOwnerDocument() != nullptr && element->GetProperty(Rml::PropertyId::TabIndex)->Get<Rml::Style::TabIndex>() != Rml::Style::TabIndex::None;
@@ -239,6 +240,7 @@ public:
config_menu_controller->load_document();
recompui::init_prompt_context();
recompui::init_assign_players_modal();
recompui::init_config_modal();
}
void unload() {
@@ -428,6 +430,12 @@ public:
context_details.context.close();
}
}
void report_removed_element(Rml::Element* element) {
if (prev_focused == element) {
prev_focused = nullptr;
}
}
};
std::unique_ptr<UIState> ui_state;
@@ -440,6 +448,11 @@ void recompui::get_window_size(int& width, int& height) {
SDL_GetWindowSizeInPixels(window, &width, &height);
}
void recompui::report_removed_element(Rml::Element* element) {
std::lock_guard lock{ ui_state_mutex };
ui_state->report_removed_element(element);
}
inline const std::string read_file_to_string(std::filesystem::path path) {
std::ifstream stream = std::ifstream{path};
std::ostringstream ss;
@@ -465,33 +478,52 @@ bool recompui::try_deque_event(SDL_Event& out) {
return ui_event_queue.try_dequeue(out);
}
int cont_button_to_key(SDL_ControllerButtonEvent& button) {
// TODO: Needs the profile index.
// Configurable accept button in menu
auto menuAcceptBinding0 = recomp::get_input_binding(0, recomp::GameInput::ACCEPT_MENU, 0);
auto menuAcceptBinding1 = recomp::get_input_binding(0, recomp::GameInput::ACCEPT_MENU, 1);
// note - magic number: 0 is InputType::None
if ((menuAcceptBinding0.input_type != recomp::InputType::None && button.button == menuAcceptBinding0.input_id) ||
(menuAcceptBinding1.input_type != recomp::InputType::None && button.button == menuAcceptBinding1.input_id)) {
return SDLK_RETURN;
recompui::MenuAction recompui::menu_action_mapping::menu_action_from_rml_key(const Rml::Input::KeyIdentifier& key) {
auto it = recompui::menu_action_mapping::rml_key_to_action.find(key);
if (it != recompui::menu_action_mapping::rml_key_to_action.end()) {
return it->second.action;
}
return recompui::MenuAction::None;
}
// Configurable apply button in menu
auto menuApplyBinding0 = recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 0);
auto menuApplyBinding1 = recomp::get_input_binding(0, recomp::GameInput::APPLY_MENU, 1);
// note - magic number: 0 is InputType::None
if ((menuApplyBinding0.input_type != recomp::InputType::None && button.button == menuApplyBinding0.input_id) ||
(menuApplyBinding1.input_type != recomp::InputType::None && button.button == menuApplyBinding1.input_id)) {
return SDLK_f;
}
// Extends RmlSDL::ConvertKey for some extra mapping slots
Rml::Input::KeyIdentifier convert_sdl_to_rml(int sdl_key) {
Rml::Input::KeyIdentifier identifier = RmlSDL::ConvertKey(sdl_key);
if (identifier == Rml::Input::KeyIdentifier::KI_UNKNOWN) {
switch (sdl_key) {
case SDLK_F15: return Rml::Input::KI_F15;
case SDLK_F16: return Rml::Input::KI_F16;
case SDLK_F17: return Rml::Input::KI_F17;
case SDLK_F18: return Rml::Input::KI_F18;
case SDLK_F19: return Rml::Input::KI_F19;
case SDLK_F20: return Rml::Input::KI_F20;
case SDLK_F21: return Rml::Input::KI_F21;
case SDLK_F22: return Rml::Input::KI_F22;
case SDLK_F23: return Rml::Input::KI_F23;
case SDLK_F24: return Rml::Input::KI_F24;
}
}
return identifier;
}
// Allows closing the menu
auto menuToggleBinding0 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 0);
auto menuToggleBinding1 = recomp::get_input_binding(0, recomp::GameInput::TOGGLE_MENU, 1);
// note - magic number: 0 is InputType::None
if ((menuToggleBinding0.input_type != recomp::InputType::None && button.button == menuToggleBinding0.input_id) ||
(menuToggleBinding1.input_type != recomp::InputType::None && button.button == menuToggleBinding1.input_id)) {
return SDLK_ESCAPE;
bool check_menu_button_pressed(int profile_index, recomp::GameInput input, int32_t event_button) {
auto menuBinding0 = recomp::get_input_binding(profile_index, input, 0);
auto menuBinding1 = recomp::get_input_binding(profile_index, input, 1);
if ((menuBinding0.input_type != recomp::InputType::None && event_button == menuBinding0.input_id) ||
(menuBinding1.input_type != recomp::InputType::None && event_button == menuBinding1.input_id)) {
return true;
}
return false;
}
int cont_button_to_key(SDL_ControllerButtonEvent& button) {
int player1_profile = recomp::get_input_profile_for_player(0, recomp::InputDevice::Controller);
for (const auto& mapping_pair : recompui::menu_action_mapping::rml_key_to_action) {
const auto& mapping = mapping_pair.second;
if (check_menu_button_pressed(player1_profile, mapping.input, button.button)) {
return mapping.sdl;
}
}
switch (button.button) {
@@ -635,7 +667,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
int sdl_key = cont_button_to_key(cur_event.cbutton);
if (context_capturing_input && sdl_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
ui_state->context->ProcessKeyDown(convert_sdl_to_rml(sdl_key), 0);
latest_controller_key_pressed = sdl_key;
next_repeat_time = clock::now() + start_repeat_delay;
}
@@ -651,6 +683,14 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
}
}
if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_F6) {
// recompui::show_context(recompui::config_modal->modal_root_context, "");
if (recompui::config_modal->is_open_now()) {
recompui::config_modal->close();
} else {
recompui::config_modal->open();
}
}
break;
case SDL_EventType::SDL_USEREVENT:
if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
@@ -675,7 +715,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
non_mouse_interacted = true;
int sdl_key = cont_axis_to_key(cur_event.caxis, axis_value);
if (context_capturing_input && sdl_key) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(sdl_key), 0);
ui_state->context->ProcessKeyDown(convert_sdl_to_rml(sdl_key), 0);
latest_controller_key_pressed = sdl_key;
next_repeat_time = clock::now() + start_repeat_delay;
}
@@ -739,7 +779,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
if (latest_controller_key_pressed != SDLK_UNKNOWN) {
clock::time_point now = clock::now();
if (now >= next_repeat_time) {
ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(latest_controller_key_pressed), 0);
ui_state->context->ProcessKeyDown(convert_sdl_to_rml(latest_controller_key_pressed), 0);
next_repeat_time += repeat_rate;
}
}