Files
BanjoRecomp/src/ui/config/ui_config_page_controls.cpp
T
2025-09-09 08:23:28 -05:00

514 lines
18 KiB
C++

#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"
namespace recompui {
ConfigPageControls *controls_page = nullptr;
const std::string_view active_state_style_name = "cont_opt_active";
GameInputRow::GameInputRow(
Element *parent,
GameInputContext *input_ctx,
std::function<void()> on_hover_callback,
on_bind_click_callback on_bind_click,
on_clear_or_reset_callback on_clear_or_reset
) : Element(parent, Events(EventType::Hover), "div", false) {
this->input_id = input_ctx->input_id;
this->on_hover_callback = on_hover_callback;
set_display(Display::Flex);
set_position(Position::Relative);
set_flex_direction(FlexDirection::Row);
set_align_items(AlignItems::Center);
set_justify_content(JustifyContent::SpaceBetween);
set_width(100.0f, Unit::Percent);
set_height_auto();
set_padding_top(4.0f);
set_padding_right(16.0f);
set_padding_bottom(4.0f);
set_padding_left(20.0f);
set_border_radius(theme::border::radius_sm);
set_background_color(theme::color::Transparent);
active_style.set_background_color(theme::color::BGOverlay);
add_style(&active_style, active_state_style_name);
recompui::ContextId context = get_current_context();
auto label = context.create_element<Label>(this, input_ctx->name, LabelStyle::Normal);
label->set_flex_grow(2.0f);
label->set_flex_shrink(1.0f);
label->set_flex_basis(300.0f);
label->set_height_auto();
// TODO: whitespace nowrap impl
auto bindings_container = context.create_element<Element>(this, 0, "div", false);
{
bindings_container->set_display(Display::Flex);
bindings_container->set_position(Position::Relative);
bindings_container->set_flex_grow(2.0f);
bindings_container->set_flex_shrink(1.0f);
bindings_container->set_flex_basis(400.0f);
bindings_container->set_flex_direction(FlexDirection::Row);
bindings_container->set_align_items(AlignItems::Center);
bindings_container->set_justify_content(JustifyContent::SpaceBetween);
bindings_container->set_width(100.0f, Unit::Percent);
bindings_container->set_height(56.0f);
bindings_container->set_padding_right(12.0f);
bindings_container->set_padding_left(4.0f);
bindings_container->set_gap(4.0f);
for (size_t i = 0; i < recomp::bindings_per_input; i++) {
BindingButton *binding_button = context.create_element<BindingButton>(bindings_container, "");
binding_button->add_pressed_callback([this, i, on_bind_click]() {
on_bind_click(this->input_id, i);
});
binding_buttons.push_back(binding_button);
}
}
if (input_ctx->clearable) {
auto clear_button = context.create_element<IconButton>(this, "icons/Trash.svg", ButtonStyle::Danger, IconButtonSize::Large);
clear_button->add_pressed_callback([this, on_clear_or_reset]() {
on_clear_or_reset(this->input_id, false);
});
} else {
auto reset_button = context.create_element<IconButton>(this, "icons/Reset.svg", ButtonStyle::Warning, IconButtonSize::Large);
reset_button->add_pressed_callback([this, on_clear_or_reset]() {
on_clear_or_reset(this->input_id, true);
});
}
bindings.resize(recomp::bindings_per_input);
for (size_t i = 0; i < recomp::bindings_per_input; i++) {
bindings[i] = recompinput::InputField();
}
}
GameInputRow::~GameInputRow() {
}
void GameInputRow::update_bindings(BindingList &new_bindings) {
for (size_t i = 0; i < new_bindings.size(); i++) {
binding_buttons[i]->set_is_binding(false);
// skip update if no changes
if (
new_bindings[i].input_id == bindings[i].input_id &&
new_bindings[i].input_type == bindings[i].input_type) {
continue;
}
binding_buttons[i]->set_binding(new_bindings[i].to_string());
bindings[i] = new_bindings[i];
}
}
void GameInputRow::process_event(const Event &e) {
switch (e.type) {
case EventType::Hover:
{
bool hover_active = std::get<EventHover>(e.variant).active;
set_style_enabled(active_state_style_name, hover_active);
if (hover_active && on_hover_callback) {
on_hover_callback();
}
}
break;
default:
break;
}
}
ConfigPageControls::ConfigPageControls(
Element *parent,
int num_players,
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->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);
recompui::ContextId context = get_current_context();
render_all();
}
void ConfigPageControls::process_event(const Event &e) {
switch (e.type) {
case EventType::Update:
if (awaiting_binding && !recompinput::is_binding()) {
awaiting_binding = false;
update_control_mappings();
}
if (last_update_index != update_index) {
last_update_index = update_index;
render_all();
}
queue_update();
break;
default:
break;
}
}
void ConfigPageControls::force_update() {
update_index++;
}
void ConfigPageControls::render_all() {
set_first_navigation_element(nullptr);
Element *header_focusable = render_header();
render_body(header_focusable);
render_footer();
confirm_first_navigation_element();
}
Element *ConfigPageControls::render_header() {
if (!multiplayer_enabled) {
hide_header();
return nullptr;
}
Element *header_focusable = nullptr;
recompui::ContextId context = get_current_context();
add_header();
// header left
{
auto header_left = header->get_left();
header_left->clear_children();
if (multiplayer_view_mappings) {
auto profile_name = context.create_element<Label>(
header_left,
"Editing: " + recomp::get_input_profile_name(selected_profile_index),
LabelStyle::Normal
);
} else {
// Nothing rendered here as of now.. maybe single player toggle
}
}
// header right
{
auto header_right = header->get_right();
header_right->clear_children();
if (multiplayer_view_mappings) {
Button* go_back_button = context.create_element<Button>(header_right, "Go back", ButtonStyle::Tertiary);
go_back_button->add_pressed_callback([this]() {
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(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(header_focusable);
} else {
body->get_right()->set_display(Display::None);
render_body_players(header_focusable);
}
}
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(header_focusable);
}
// right side
{
body->get_right()->clear_children();
description_container = context.create_element<Element>(body->get_right(), 0, "p", true);
description_container->set_text(
"Sometimes, the windows combine with the seams in a way\n"
"That twitches on a peak at the place where the spirit was slain\n"
"Hey, one foot leads to another\n"
"Night's for sleep, blue curtains, covers, sequins in the eyes\n"
"That's a fine time to dine\n"
"Divine who's circling, feeding the cards to the midwives"
);
}
}
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);
player_grid->set_justify_content(JustifyContent::SpaceBetween);
player_grid->set_align_items(AlignItems::Center);
player_grid->set_width(100.0f, Unit::Percent);
player_grid->set_height_auto();
player_grid->set_gap(64.0f);
for (int i = 0; i < num_players; i++) {
auto player_card = context.create_element<PlayerCard>(
player_grid,
i,
false
);
player_card->set_on_select_profile_callback([this](int player_index, int profile_index) {
this->on_select_player_profile(player_index, profile_index);
});
player_card->set_on_edit_profile_callback([this](int player_index) {
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);
// }
}
}
}
void ConfigPageControls::on_select_player_profile(int player_index, int profile_index) {
auto& assigned_player = recompinput::get_assigned_player(player_index);
recomp::InputDevice device = recompinput::get_assigned_player_input_device(player_index);
if (device != recomp::InputDevice::COUNT) {
recomp::set_input_profile_for_player(player_index, profile_index, device);
}
}
void ConfigPageControls::on_edit_player_profile(int player_index) {
selected_player = player_index;
recomp::InputDevice device = recompinput::get_assigned_player_input_device(player_index);
if (device != recomp::InputDevice::COUNT) {
selected_profile_index = recomp::get_input_profile_for_player(player_index, device);
multiplayer_view_mappings = true;
force_update();
}
}
void ConfigPageControls::render_footer() {
if (multiplayer_enabled && !multiplayer_view_mappings) {
hide_footer();
return;
}
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();
if (!multiplayer_enabled) {
keyboard_toggle = context.create_element<Toggle>(footer_left);
keyboard_toggle->set_checked(single_player_show_keyboard_mappings);
keyboard_toggle->add_checked_callback([this](bool checked) {
this->single_player_show_keyboard_mappings = checked;
this->update_control_mappings();
});
Label *kb_label = context.create_element<Label>(footer_left, "Enable keyboard", LabelStyle::Normal);
kb_label->set_margin_left(12.0f);
}
}
{
auto footer_right = footer->get_right();
footer_right->clear_children();
auto reset_to_defaults_button = context.create_element<Button>(footer_right, "Reset to defaults", ButtonStyle::Warning);
reset_to_defaults_button->add_pressed_callback([this]() {
recomp::reset_profile_bindings(this->selected_profile_index, this->get_player_input_device());
this->update_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();
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_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++) {
auto &ctx = game_input_contexts[i];
GameInputRow *row = context.create_element<GameInputRow>(
body_left_scroll,
&ctx,
[this, i]() {
this->on_option_hover(i);
},
[this](recompinput::GameInput game_input, int input_index) {
this->on_bind_click(game_input, input_index);
},
[this](recompinput::GameInput game_input, bool reset) {
this->on_clear_or_reset_game_input(game_input, reset);
}
);
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();
}
void ConfigPageControls::update_control_mappings() {
if (!multiplayer_enabled) {
selected_player = 0;
selected_profile_index = single_player_show_keyboard_mappings
? recomp::get_sp_keyboard_profile_index()
: recomp::get_sp_controller_profile_index();
} else if (!multiplayer_view_mappings) {
return;
}
game_input_bindings.clear();
for (int i = 0; i < game_input_contexts.size(); i++) {
GameInputContext &ctx = game_input_contexts[i];
game_input_bindings[ctx.input_id] = {};
for (int j = 0; j < recomp::bindings_per_input; j++) {
game_input_bindings[ctx.input_id].push_back(recomp::get_input_binding(selected_profile_index, ctx.input_id, j));
}
}
for (size_t i = 0; i < game_input_rows.size(); i++) {
game_input_rows[i]->update_bindings(
game_input_bindings.at(game_input_rows[i]->get_input_id())
);
}
}
ConfigPageControls::~ConfigPageControls() {
controls_page = nullptr;
}
recompinput::InputDevice ConfigPageControls::get_player_input_device() {
if (multiplayer_enabled) {
return recompinput::get_assigned_player_input_device(this->selected_player);
}
return single_player_show_keyboard_mappings
? recomp::InputDevice::Keyboard
: recomp::InputDevice::Controller;
}
void ConfigPageControls::on_bind_click(recompinput::GameInput game_input, int input_index) {
recompinput::InputDevice device = get_player_input_device();
recompinput::start_scanning_for_binding(this->selected_player, game_input, input_index, device);
awaiting_binding = true;
}
void ConfigPageControls::on_clear_or_reset_game_input(recompinput::GameInput game_input, bool reset) {
if (!reset) {
recomp::clear_input_binding(selected_profile_index, game_input);
} else {
recompinput::InputDevice device = get_player_input_device();
recomp::reset_input_binding(selected_profile_index, device, game_input);
}
update_control_mappings();
}
void ConfigPageControls::set_selected_player(int player) {
selected_player = player;
}
void ConfigPageControls::on_option_hover(uint8_t index) {
if (description_container) {
description_container->set_text(game_input_contexts[index].description);
}
}
} // namespace recompui